"""
Fusion Accounting - SEPA Credit Transfer (pain.001)

Generates ISO 20022 ``pain.001.001.03`` XML documents for SEPA Credit
Transfers.  The XML structure follows the published
`ISO 20022 message definition
<https://www.iso20022.org/catalogue-messages/iso-20022-messages-archive>`_
and uses ``xml.etree.ElementTree`` for construction.

Namespace
---------
``urn:iso:std:iso:20022:tech:xsd:pain.001.001.03``
"""

import hashlib
import re
import xml.etree.ElementTree as ET
from datetime import datetime, date

from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError

# ISO 20022 pain.001.001.03 namespace
PAIN_001_NS = 'urn:iso:std:iso:20022:tech:xsd:pain.001.001.03'

# IBAN validation pattern (basic check: 2-letter country + 2 check digits + BBAN)
_IBAN_RE = re.compile(r'^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$')

# BIC / SWIFT pattern (8 or 11 alphanumeric characters)
_BIC_RE = re.compile(r'^[A-Z0-9]{8}([A-Z0-9]{3})?$')


def _sanitise_text(value, max_len=140):
    """Remove characters not allowed in SEPA XML text fields.

    The SEPA character set is a subset of ASCII (Latin characters,
    digits, and a small set of special characters).

    :param str value: Original text.
    :param int max_len: Maximum allowed length.
    :return: Sanitised string.
    :rtype: str
    """
    if not value:
        return ''
    # Keep only allowed characters (letters, digits, spaces, common punctuation)
    cleaned = re.sub(r"[^A-Za-z0-9 +\-/?.,:;'()\n]", '', value)
    return cleaned[:max_len].strip()


def _validate_iban(iban):
    """Validate an IBAN string (basic structural check).

    :param str iban: The IBAN to validate.
    :raises ValidationError: If the IBAN format is invalid.
    :return: Normalised IBAN (upper-case, no spaces).
    :rtype: str
    """
    if not iban:
        raise ValidationError(_("IBAN is required for SEPA transfers."))
    normalised = iban.upper().replace(' ', '')
    if not _IBAN_RE.match(normalised):
        raise ValidationError(_(
            "'%(iban)s' does not look like a valid IBAN.",
            iban=iban,
        ))
    return normalised


def _validate_bic(bic):
    """Validate a BIC / SWIFT code.

    :param str bic: The BIC to validate.
    :raises ValidationError: If the BIC format is invalid.
    :return: Normalised BIC (upper-case, no spaces).
    :rtype: str
    """
    if not bic:
        raise ValidationError(_("BIC/SWIFT code is required for SEPA transfers."))
    normalised = bic.upper().replace(' ', '')
    if not _BIC_RE.match(normalised):
        raise ValidationError(_(
            "'%(bic)s' does not look like a valid BIC/SWIFT code.",
            bic=bic,
        ))
    return normalised


class FusionSEPACreditTransfer(models.Model):
    """Provides SEPA Credit Transfer XML generation (pain.001.001.03).

    This model adds company-level BIC storage and a utility method
    ``generate_pain_001`` that builds a compliant XML file from a set
    of ``account.payment`` records.
    """

    _inherit = 'res.company'

    fusion_company_bic = fields.Char(
        string='Company BIC/SWIFT',
        size=11,
        help="BIC (Business Identifier Code) of the company's main bank "
             "account, used as the debtor agent in SEPA transfers.",
    )
    fusion_company_iban = fields.Char(
        string='Company IBAN',
        size=34,
        help="IBAN of the company's main bank account, used as the "
             "debtor account in SEPA transfers.",
    )

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def generate_pain_001(self, payments):
        """Build an ISO 20022 pain.001.001.03 XML for SEPA Credit Transfers.

        :param payments: ``account.payment`` recordset to include.
        :return: UTF-8 encoded XML bytes.
        :rtype: bytes
        :raises UserError: when mandatory data is missing.
        """
        self.ensure_one()

        if not payments:
            raise UserError(_("No payments provided for the SEPA file."))

        company_iban = _validate_iban(self.fusion_company_iban)
        company_bic = _validate_bic(self.fusion_company_bic)

        # Root element with namespace
        root = ET.Element('Document', xmlns=PAIN_001_NS)
        cstmr_cdt_trf = ET.SubElement(root, 'CstmrCdtTrfInitn')

        # -- Group Header (GrpHdr) --
        grp_hdr = ET.SubElement(cstmr_cdt_trf, 'GrpHdr')
        msg_id = self._pain001_message_id()
        ET.SubElement(grp_hdr, 'MsgId').text = msg_id
        ET.SubElement(grp_hdr, 'CreDtTm').text = datetime.utcnow().strftime(
            '%Y-%m-%dT%H:%M:%S'
        )
        ET.SubElement(grp_hdr, 'NbOfTxs').text = str(len(payments))
        ctrl_sum = sum(payments.mapped('amount'))
        ET.SubElement(grp_hdr, 'CtrlSum').text = f'{ctrl_sum:.2f}'

        initg_pty = ET.SubElement(grp_hdr, 'InitgPty')
        ET.SubElement(initg_pty, 'Nm').text = _sanitise_text(
            self.name, max_len=70
        )

        # -- Payment Information (PmtInf) --
        pmt_inf = ET.SubElement(cstmr_cdt_trf, 'PmtInf')
        ET.SubElement(pmt_inf, 'PmtInfId').text = msg_id[:35]
        ET.SubElement(pmt_inf, 'PmtMtd').text = 'TRF'  # Transfer
        ET.SubElement(pmt_inf, 'NbOfTxs').text = str(len(payments))
        ET.SubElement(pmt_inf, 'CtrlSum').text = f'{ctrl_sum:.2f}'

        # Payment Type Information
        pmt_tp_inf = ET.SubElement(pmt_inf, 'PmtTpInf')
        svc_lvl = ET.SubElement(pmt_tp_inf, 'SvcLvl')
        ET.SubElement(svc_lvl, 'Cd').text = 'SEPA'

        ET.SubElement(pmt_inf, 'ReqdExctnDt').text = (
            fields.Date.context_today(self).isoformat()
        )

        # Debtor (company)
        dbtr = ET.SubElement(pmt_inf, 'Dbtr')
        ET.SubElement(dbtr, 'Nm').text = _sanitise_text(self.name, 70)

        dbtr_acct = ET.SubElement(pmt_inf, 'DbtrAcct')
        dbtr_acct_id = ET.SubElement(dbtr_acct, 'Id')
        ET.SubElement(dbtr_acct_id, 'IBAN').text = company_iban

        dbtr_agt = ET.SubElement(pmt_inf, 'DbtrAgt')
        dbtr_agt_id = ET.SubElement(dbtr_agt, 'FinInstnId')
        ET.SubElement(dbtr_agt_id, 'BIC').text = company_bic

        ET.SubElement(pmt_inf, 'ChrgBr').text = 'SLEV'  # Service Level

        # -- Individual transactions --
        for payment in payments:
            self._add_pain001_transaction(pmt_inf, payment)

        # Build and return the XML
        tree = ET.ElementTree(root)
        ET.indent(tree, space='  ')
        xml_bytes = ET.tostring(
            root, encoding='UTF-8', xml_declaration=True
        )
        return xml_bytes

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _pain001_message_id(self):
        """Generate a unique message identifier for the pain.001 file.

        Uses a hash of the company id and the current timestamp to
        produce a reference that is unique and fits the 35-character
        SEPA limit.

        :return: Message ID string (max 35 chars).
        :rtype: str
        """
        raw = f'{self.id}-{datetime.utcnow().isoformat()}'
        digest = hashlib.sha256(raw.encode()).hexdigest()[:16]
        return f'FUSION-SCT-{digest}'.upper()[:35]

    def _add_pain001_transaction(self, pmt_inf, payment):
        """Append a ``CdtTrfTxInf`` element for a single payment.

        :param pmt_inf: The ``PmtInf`` XML element.
        :param payment: An ``account.payment`` record.
        :raises UserError: when creditor bank details are missing.
        """
        partner = payment.partner_id
        if not partner:
            raise UserError(_(
                "Payment '%(payment)s' has no partner.",
                payment=payment.name,
            ))

        partner_bank = payment.partner_bank_id
        if not partner_bank or not partner_bank.acc_number:
            raise UserError(_(
                "Payment '%(payment)s' has no bank account set for "
                "partner '%(partner)s'.",
                payment=payment.name,
                partner=partner.display_name,
            ))

        creditor_iban = _validate_iban(partner_bank.acc_number)

        cdt_trf = ET.SubElement(pmt_inf, 'CdtTrfTxInf')

        # Payment Identification
        pmt_id = ET.SubElement(cdt_trf, 'PmtId')
        ET.SubElement(pmt_id, 'EndToEndId').text = _sanitise_text(
            payment.name or 'NOTPROVIDED', 35
        )

        # Amount
        amt = ET.SubElement(cdt_trf, 'Amt')
        instd_amt = ET.SubElement(amt, 'InstdAmt')
        instd_amt.text = f'{payment.amount:.2f}'
        instd_amt.set('Ccy', payment.currency_id.name or 'EUR')

        # Creditor Agent (BIC) - optional but recommended
        if partner_bank.bank_bic:
            cdtr_agt = ET.SubElement(cdt_trf, 'CdtrAgt')
            cdtr_agt_fin = ET.SubElement(cdtr_agt, 'FinInstnId')
            ET.SubElement(cdtr_agt_fin, 'BIC').text = (
                partner_bank.bank_bic.upper().replace(' ', '')
            )

        # Creditor
        cdtr = ET.SubElement(cdt_trf, 'Cdtr')
        ET.SubElement(cdtr, 'Nm').text = _sanitise_text(
            partner.name or '', 70
        )

        # Creditor Account
        cdtr_acct = ET.SubElement(cdt_trf, 'CdtrAcct')
        cdtr_acct_id = ET.SubElement(cdtr_acct, 'Id')
        ET.SubElement(cdtr_acct_id, 'IBAN').text = creditor_iban

        # Remittance Information
        if payment.ref or payment.name:
            rmt_inf = ET.SubElement(cdt_trf, 'RmtInf')
            ET.SubElement(rmt_inf, 'Ustrd').text = _sanitise_text(
                payment.ref or payment.name or '', 140
            )
