from xml.etree.ElementTree import Element, SubElement, tostring
from xml.dom import minidom
import math
import re


def clean_string(text):
    return (
        str(text)
        .replace('ä', 'ae')
        .replace('ö', 'oe')
        .replace('ü', 'ue')
        .replace('Ä', 'Ae')
        .replace('Ö', 'Oe')
        .replace('Ü', 'Ue')
        .replace('ß', 'ss')
        .replace(' ', '-')
    )


def _split_plz_ort(value):
    """Trennt einen String wie "3340 Waidhofen an der Ybbs" in (plz, ort).
    Toleriert führende/trailing-Whitespaces. Wenn kein 4-stelliger PLZ-Präfix
    vorhanden ist, wird (None, gesamter_string) zurückgegeben."""
    if value is None:
        return None, ""
    s = str(value).strip()
    m = re.match(r"^(\d{4,5})\s+(.+)$", s)
    if m:
        return m.group(1), m.group(2).strip()
    return None, s


def _split_street(value):
    """Trennt "Sergius-Pauser-Straße 19/1" in ("Sergius-Pauser-Straße", "19/1").
    Wenn keine Hausnummer erkennbar, Hausnummer = ''."""
    if value is None:
        return "", ""
    s = str(value).strip()
    parts = s.split()
    if len(parts) > 1:
        return " ".join(parts[:-1]), parts[-1]
    return s, ""

class SepaCT:
    def __init__(self, config):
        self.config = config
        self.transactions = []
        self.root = Element("Document", xmlns="urn:iso:std:iso:20022:tech:xsd:pain.001.001.09")
        self.cstmr_cdt_trf_initn = SubElement(self.root, "CstmrCdtTrfInitn")
        self.group_header = None
        self.payment_info = None

    def add_group_header(self, message_id, creation_datetime):
        self.group_header = SubElement(self.cstmr_cdt_trf_initn, "GrpHdr")
        SubElement(self.group_header, "MsgId").text = str(message_id)
        SubElement(self.group_header, "CreDtTm").text = creation_datetime.strftime("%Y-%m-%dT%H:%M:%S")

    def add_payment_info(self, payment_id, requested_execution_date, debtor_address, debtor_name, debtor_iban, debtor_bic):
        self.payment_info = SubElement(self.cstmr_cdt_trf_initn, "PmtInf")
        SubElement(self.payment_info, "PmtInfId").text = str(payment_id)
        SubElement(self.payment_info, "PmtMtd").text = "TRF"
        SubElement(self.payment_info, "BtchBookg").text = "true"
        reqdexctndt = SubElement(self.payment_info, "ReqdExctnDt")
        SubElement(reqdexctndt, "Dt").text = str(requested_execution_date.strftime("%Y-%m-%d"))

        dbtr = SubElement(self.payment_info, "Dbtr")
        SubElement(dbtr, "Nm").text = str(debtor_name)

        pstl_adr = SubElement(dbtr, "PstlAdr")
        # Straße + Hausnummer: bevorzugt 'Vereinsadresse' (neu), fällt auf
        # 'Strasse' zurück (alt). Leere Eingaben werden robust behandelt.
        street_src = debtor_address.get("Vereinsadresse") or debtor_address.get("Strasse") or ""
        street_name, building_number = _split_street(street_src)

        if street_name:
            SubElement(pstl_adr, "StrtNm").text = street_name
        if building_number:
            SubElement(pstl_adr, "BldgNb").text = building_number

        # PLZ + Ort: 'Vereinsort' ist oft "3340 Waidhofen an der Ybbs".
        # Wenn separates PLZ-Feld gesetzt ist, hat das Vorrang; ansonsten aus
        # Vereinsort splitten (Bug-Fix: früher stand der ganze String in PstCd).
        plz_raw  = debtor_address.get("PLZ") or ""
        ort_src  = debtor_address.get("Vereinsort") or debtor_address.get("Ort") or ""
        plz_from_ort, ort_clean = _split_plz_ort(ort_src)
        plz = str(plz_raw).strip() or (plz_from_ort or "")
        if plz:
            SubElement(pstl_adr, "PstCd").text = plz
        if ort_clean:
            SubElement(pstl_adr, "TwnNm").text = ort_clean
        SubElement(pstl_adr, "Ctry").text = "AT"  # Ländercode

        # IBAN im SEPA-Format (verschachtelt)
        dbtr_acct = SubElement(self.payment_info, "DbtrAcct")
        id_element = SubElement(dbtr_acct, "Id")
        SubElement(id_element, "IBAN").text = str(debtor_iban)

        dbtr_agt = SubElement(self.payment_info, "DbtrAgt")
        id_element = SubElement(dbtr_agt, "FinInstnId")
        SubElement(id_element, "BICFI").text = str(debtor_bic)

    def add_transaction(self, amount, creditor_name, creditor_address, creditor_iban, creditor_bic, reference, id):
        # Betrag validieren
        try:
            amount_float = float(amount)
            if math.isnan(amount_float):
                raise ValueError(f"Betrag ist NaN für {creditor_name}")
        except (ValueError, TypeError) as e:
            raise ValueError(f"Ungültiger Betrag {amount} für {creditor_name}: {e}")

        tx = SubElement(self.payment_info, "CdtTrfTxInf")
        id_element = SubElement(tx, "PmtId")
        SubElement(id_element, "EndToEndId").text = f"{clean_string(creditor_name)[:30]}-MG{str(id)}"

        amt = SubElement(tx, "Amt")
        instd_amt = SubElement(amt, "InstdAmt")
        instd_amt.set("Ccy", "EUR")
        instd_amt.text = f"{amount_float:.2f}"  # Formatiert als String mit 2 Dezimalstellen

        cdtr = SubElement(tx, "Cdtr")
        SubElement(cdtr, "Nm").text = str(creditor_name)

        pstl_adr = SubElement(cdtr, "PstlAdr")
        street_name, building_number = _split_street(creditor_address.get("Strasse", ""))
        if street_name:
            SubElement(pstl_adr, "StrtNm").text = street_name
        if building_number:
            SubElement(pstl_adr, "BldgNb").text = building_number

        plz_val = str(creditor_address.get("PLZ", "") or "").strip()
        ort_val = str(creditor_address.get("Ort", "") or "").strip()
        if plz_val:
            SubElement(pstl_adr, "PstCd").text = plz_val
        if ort_val:
            SubElement(pstl_adr, "TwnNm").text = ort_val
        SubElement(pstl_adr, "Ctry").text = "AT"  # Ländercode

        # IBAN im SEPA-Format (verschachtelt)
        cdtr_acct = SubElement(tx, "CdtrAcct")
        id_element = SubElement(cdtr_acct, "Id")
        SubElement(id_element, "IBAN").text = str(creditor_iban)

        rmt_inf = SubElement(tx, "RmtInf")
        SubElement(rmt_inf, "Ustrd").text = str(reference)

    def export(self, file_path, initiating_party_name, initiating_party_address, pretty_print=True):
        # Anzahl der Transaktionen
        tx_elements = list(self.payment_info.findall("CdtTrfTxInf"))
        nb_of_txs = len(tx_elements)

        # Summe der Beträge (mit NaN-Prüfung)
        ctrl_sum = 0.0
        for tx in tx_elements:
            amt_text = tx.find(".//InstdAmt").text
            try:
                amt = float(amt_text)
                if math.isnan(amt):
                    raise ValueError(f"NaN-Betrag in Transaktion {tx.find('PmtId').get('EndToEndId')}")
                ctrl_sum += amt
            except (ValueError, AttributeError) as e:
                raise ValueError(f"Fehler beim Parsen des Betrags {amt_text}: {e}")

        # CtrlSum und NbOfTxs setzen (als String!)
        SubElement(self.group_header, "NbOfTxs").text = str(nb_of_txs)
        SubElement(self.group_header, "CtrlSum").text = f"{ctrl_sum:.2f}"  # Formatiert als String

        # InitgPty mit Adresse (PstlAdr)
        initg_pty = SubElement(self.group_header, "InitgPty")
        SubElement(initg_pty, "Nm").text = str(initiating_party_name)

        # Postanschrift (PstlAdr) hinzufügen
        pstl_adr = SubElement(initg_pty, "PstlAdr")
        street_src = (initiating_party_address.get("Vereinsadresse")
                      or initiating_party_address.get("Strasse") or "")
        street_name, building_number = _split_street(street_src)
        if street_name:
            SubElement(pstl_adr, "StrtNm").text = street_name
        if building_number:
            SubElement(pstl_adr, "BldgNb").text = building_number

        # "3340 Waidhofen an der Ybbs" → ("3340", "Waidhofen an der Ybbs")
        plz_ort_src = initiating_party_address.get("Vereinsort", "") or ""
        plz_from_ort, ort_clean = _split_plz_ort(plz_ort_src)
        plz_explicit = str(initiating_party_address.get("PLZ", "") or "").strip()
        plz_final = plz_explicit or (plz_from_ort or "")
        if plz_final:
            SubElement(pstl_adr, "PstCd").text = plz_final
        if ort_clean:
            SubElement(pstl_adr, "TwnNm").text = ort_clean
        SubElement(pstl_adr, "Ctry").text = "AT"  # Ländercode

        # XML serialisieren
        try:
            # Debug: Roh-XML anzeigen
            raw_xml = tostring(self.root, encoding="unicode")
            # Pretty-Print
            xml_str = minidom.parseString(raw_xml).toprettyxml(indent="  ", encoding="utf-8")
            with open(file_path, "wb") as f:
                f.write(xml_str)
        except Exception as e:
            raise ValueError(f"Fehler beim Export der XML-Datei: {e}")


class SepaDD:
    def __init__(self, config):
        self.config = config
        self.transactions = []
        self.root = Element("Document", xmlns="urn:iso:std:iso:20022:tech:xsd:pain.008.001.02")
        self.cstmr_drct_dbt_initn = SubElement(self.root, "CstmrDrctDbtInitn")
        self.group_header = None
        self.payment_info = None

    def add_group_header(self, message_id, creation_datetime, initiating_party_name, ZVR, nb_of_txs=0, ctrl_sum=0.0):
        self.group_header = SubElement(self.cstmr_drct_dbt_initn, "GrpHdr")
        SubElement(self.group_header, "MsgId").text = str(message_id)
        SubElement(self.group_header, "CreDtTm").text = creation_datetime.strftime("%Y-%m-%dT%H:%M:%S")
        SubElement(self.group_header, "NbOfTxs").text = str(nb_of_txs)
        SubElement(self.group_header, "CtrlSum").text = f"{ctrl_sum:.2f}"

        # Initiating Party (optional, aber empfohlen)
        initg_pty = SubElement(self.group_header, "InitgPty")
        SubElement(initg_pty, "Nm").text = str(initiating_party_name)
        Id = SubElement(initg_pty, "Id")
        OrgId = SubElement(Id, "OrgId")
        Othr = SubElement(OrgId, "Othr")
        SubElement(Othr, "Id").text = str(ZVR)

    def add_payment_info(
        self,
        payment_id,
        requested_execution_date,
        creditor_name,
        creditor_iban,
        creditor_bic,
        creditor_id,
        nb_of_txs=0,
        ctrl_sum=0.0,
        seq_type="FRST",
    ):
        self.payment_info = SubElement(self.cstmr_drct_dbt_initn, "PmtInf")
        SubElement(self.payment_info, "PmtInfId").text = str(payment_id)
        SubElement(self.payment_info, "PmtMtd").text = "DD"
        SubElement(self.payment_info, "BtchBookg").text = "true"
        # Platzhalter – werden in export() mit den echten Zahlen überschrieben.
        # text= verlangt str, sonst wirft xml.etree beim Serialisieren TypeError.
        SubElement(self.payment_info, "NbOfTxs").text = str(nb_of_txs)
        SubElement(self.payment_info, "CtrlSum").text = f"{float(ctrl_sum):.2f}"
        PmtTpInf = SubElement(self.payment_info, "PmtTpInf")
        SvcLvl = SubElement(PmtTpInf, "SvcLvl")

        SubElement(SvcLvl, "Cd").text = "SEPA"
        LclInstrm = SubElement(PmtTpInf, "LclInstrm")
        SubElement(LclInstrm, "Cd").text = "CORE"
        SubElement(PmtTpInf, "SeqTp").text = "OOFF"

        SubElement(self.payment_info, "ReqdColltnDt").text = requested_execution_date.strftime("%Y-%m-%d")

        # Creditor (Gläubiger)
        cdtr = SubElement(self.payment_info, "Cdtr")
        SubElement(cdtr, "Nm").text = str(creditor_name)

        # Creditor Account (IBAN)
        cdtr_acct = SubElement(self.payment_info, "CdtrAcct")
        id_element = SubElement(cdtr_acct, "Id")
        SubElement(id_element, "IBAN").text = str(creditor_iban)

        # Creditor Agent (BIC)
        cdtr_agt = SubElement(self.payment_info, "CdtrAgt")
        fin_instn_id = SubElement(cdtr_agt, "FinInstnId")
        SubElement(fin_instn_id, "BIC").text = str(creditor_bic)

        # ChrgBr
        SubElement(self.payment_info, "ChrgBr").text = "SLEV"

        # Creditor Scheme Identification
        cdtr_schme_id = SubElement(self.payment_info, "CdtrSchmeId")
        id_element = SubElement(cdtr_schme_id, "Id")
        PrvtId = SubElement(id_element, "PrvtId")
        Othr = SubElement(PrvtId, "Othr")
        SubElement(Othr, "Id").text = creditor_id
        SchmeNm = SubElement(Othr, "SchmeNm")
        SubElement(SchmeNm, "Prtry").text = "SEPA"


    def add_transaction(
        self,
        amount,
        debtor_name,
        debtor_iban,
        debtor_bic,
        mandate_id,
        mandate_date,
        reference,
        id,
    ):
        # Betrag validieren
        try:
            amount_float = float(amount)
            if math.isnan(amount_float):
                raise ValueError(f"Betrag ist NaN für {debtor_name}")
        except (ValueError, TypeError) as e:
            raise ValueError(f"Ungültiger Betrag {amount} für {debtor_name}: {e}")

        # Transaktion hinzufügen
        drct_dbt_tx = SubElement(self.payment_info, "DrctDbtTxInf")
        id_element = SubElement(drct_dbt_tx, "PmtId")
        SubElement(id_element, "EndToEndId").text = f"{clean_string(debtor_name)}-MG{str(id)}"

        # Betrag
        instd_amt = SubElement(drct_dbt_tx, "InstdAmt")
        instd_amt.set("Ccy", "EUR")
        instd_amt.text = f"{amount_float:.2f}"

        # DrctDbtTx
        drct_dbt_tx_info = SubElement(drct_dbt_tx, "DrctDbtTx")
        mndt_rltd_inf = SubElement(drct_dbt_tx_info, "MndtRltdInf")
        SubElement(mndt_rltd_inf, "MndtId").text = str(mandate_id)
        SubElement(mndt_rltd_inf, "DtOfSgntr").text = mandate_date.strftime("%Y-%m-%d")

        # DrctDbtTx
        DbtrAgt = SubElement(drct_dbt_tx, "DbtrAgt")
        FinInstnId = SubElement(DbtrAgt, "FinInstnId")
        SubElement(FinInstnId, "BIC").text = debtor_bic

        # Debtor (Schuldner)
        dbtr = SubElement(drct_dbt_tx, "Dbtr")
        SubElement(dbtr, "Nm").text = str(debtor_name)

        # Debtor Account (IBAN)
        dbtr_acct = SubElement(drct_dbt_tx, "DbtrAcct")
        id_element = SubElement(dbtr_acct, "Id")
        SubElement(id_element, "IBAN").text = str(debtor_iban)

        # Remittance Information (Verwendungszweck)
        rmt_inf = SubElement(drct_dbt_tx, "RmtInf")
        SubElement(rmt_inf, "Ustrd").text = str(reference)

    def export(self, file_path, pretty_print=True):
        # Anzahl der Transaktionen und Summe aktualisieren
        tx_elements = list(self.payment_info.findall("DrctDbtTxInf"))
        nb_of_txs = len(tx_elements)
        ctrl_sum = 0.0
        for tx in tx_elements:
            amt_text = tx.find(".//InstdAmt").text
            try:
                amt = float(amt_text)
                if math.isnan(amt):
                    raise ValueError(f"NaN-Betrag in Transaktion {tx.find('PmtId').get('EndToEndId')}")
                ctrl_sum += amt
            except (ValueError, AttributeError) as e:
                raise ValueError(f"Fehler beim Parsen des Betrags {amt_text}: {e}")

        # GrpHdr aktualisieren
        self.group_header.find("NbOfTxs").text = str(nb_of_txs)
        self.payment_info.find("NbOfTxs").text = str(nb_of_txs)
        self.group_header.find("CtrlSum").text = f"{ctrl_sum:.2f}"
        self.payment_info.find("CtrlSum").text = f"{ctrl_sum:.2f}"

        # XML serialisieren
        try:
            raw_xml = tostring(self.root, encoding="unicode")
            xml_str = minidom.parseString(raw_xml).toprettyxml(indent="  ", encoding="utf-8")
            with open(file_path, "wb") as f:
                f.write(xml_str)
        except Exception as e:
            raise ValueError(f"Fehler beim Export der XML-Datei: {e}")