"""Public API for XML signing.

This module provides a minimal, pure-library API (no CLI) to sign SIFEN XML.
"""

from __future__ import annotations

import datetime as _dt
import hashlib
import base64
from typing import Optional

from lxml import etree

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import Encoding

from .pkcs12 import load_pkcs12


_DS_NS = "http://www.w3.org/2000/09/xmldsig#"


def _c14n_exc(el: etree._Element) -> bytes:
    return etree.tostring(el, method="c14n", exclusive=True, with_comments=False)


def _find_by_id(root: etree._Element, id_value: str) -> etree._Element:
    nodes = root.xpath(f"//*[@Id='{id_value}']")
    if not nodes:
        raise ValueError("No se encontró el nodo con atributo Id para firmar")
    return nodes[0]


def _remove_existing_signatures(el: etree._Element) -> None:
    for sig in el.xpath(".//*[local-name()='Signature' and namespace-uri()='http://www.w3.org/2000/09/xmldsig#']"):
        parent = sig.getparent()
        if parent is not None:
            parent.remove(sig)


def _build_signature_template(reference_uri: str) -> etree._Element:
    ds = "{http://www.w3.org/2000/09/xmldsig#}"

    signature = etree.Element(ds + "Signature", nsmap={"ds": _DS_NS})
    signed_info = etree.SubElement(signature, ds + "SignedInfo")

    etree.SubElement(
        signed_info,
        ds + "CanonicalizationMethod",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        signed_info,
        ds + "SignatureMethod",
        Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    )

    reference = etree.SubElement(signed_info, ds + "Reference", URI=reference_uri)
    transforms = etree.SubElement(reference, ds + "Transforms")
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature",
    )
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        reference,
        ds + "DigestMethod",
        Algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
    )
    etree.SubElement(reference, ds + "DigestValue").text = ""

    etree.SubElement(signature, ds + "SignatureValue").text = ""
    key_info = etree.SubElement(signature, ds + "KeyInfo")
    x509_data = etree.SubElement(key_info, ds + "X509Data")
    etree.SubElement(x509_data, ds + "X509Certificate").text = ""
    return signature


def _build_signature_template_default_ns(reference_uri: str) -> etree._Element:
    ds = "{http://www.w3.org/2000/09/xmldsig#}"

    signature = etree.Element(ds + "Signature", nsmap={None: _DS_NS})
    signed_info = etree.SubElement(signature, ds + "SignedInfo")

    etree.SubElement(
        signed_info,
        ds + "CanonicalizationMethod",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        signed_info,
        ds + "SignatureMethod",
        Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    )

    reference = etree.SubElement(signed_info, ds + "Reference", URI=reference_uri)
    transforms = etree.SubElement(reference, ds + "Transforms")
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature",
    )
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        reference,
        ds + "DigestMethod",
        Algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
    )
    etree.SubElement(reference, ds + "DigestValue").text = ""

    etree.SubElement(signature, ds + "SignatureValue").text = ""
    key_info = etree.SubElement(signature, ds + "KeyInfo")
    x509_data = etree.SubElement(key_info, ds + "X509Data")
    etree.SubElement(x509_data, ds + "X509Certificate").text = ""
    return signature


def _build_signature_template_detached(reference_uri: str) -> etree._Element:
    ds = "{http://www.w3.org/2000/09/xmldsig#}"

    # Usar namespace sin prefijo (como lo hace l10n_py_sifen/sifen_node)
    signature = etree.Element(ds + "Signature", nsmap={None: _DS_NS})
    signed_info = etree.SubElement(signature, ds + "SignedInfo")

    etree.SubElement(
        signed_info,
        ds + "CanonicalizationMethod",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        signed_info,
        ds + "SignatureMethod",
        Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
    )

    reference = etree.SubElement(signed_info, ds + "Reference", URI=reference_uri)
    transforms = etree.SubElement(reference, ds + "Transforms")
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature",
    )
    etree.SubElement(
        transforms,
        ds + "Transform",
        Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
    )
    etree.SubElement(
        reference,
        ds + "DigestMethod",
        Algorithm="http://www.w3.org/2001/04/xmlenc#sha256",
    )
    etree.SubElement(reference, ds + "DigestValue").text = ""

    etree.SubElement(signature, ds + "SignatureValue").text = ""
    key_info = etree.SubElement(signature, ds + "KeyInfo")
    x509_data = etree.SubElement(key_info, ds + "X509Data")
    etree.SubElement(x509_data, ds + "X509Certificate").text = ""
    return signature


def _sign_xml_enveloped(*, root: etree._Element, id_value: str, material) -> etree._Element:
    target = _find_by_id(root, id_value)

    # Safety: if user re-signs a document, remove previous signatures
    _remove_existing_signatures(target)

    signature = _build_signature_template(reference_uri=f"#{id_value}")

    # Digest over canonicalized referenced node (with enveloped transform applied)
    target_for_digest = etree.fromstring(etree.tostring(target))
    _remove_existing_signatures(target_for_digest)
    digest = hashlib.sha256(_c14n_exc(target_for_digest)).digest()
    digest_b64 = base64.b64encode(digest).decode("ascii")

    digest_value_node = signature.xpath(".//*[local-name()='DigestValue']")[0]
    digest_value_node.text = digest_b64

    # Sign canonicalized SignedInfo
    signed_info = signature.xpath(".//*[local-name()='SignedInfo']")[0]
    signed_info_c14n = _c14n_exc(signed_info)

    signature_bytes = material.private_key.sign(
        signed_info_c14n,
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    signature_value_b64 = base64.b64encode(signature_bytes).decode("ascii")
    sig_value_node = signature.xpath(".//*[local-name()='SignatureValue']")[0]
    sig_value_node.text = signature_value_b64

    # Embed certificate (DER without PEM headers)
    cert_der = material.certificate.public_bytes(Encoding.DER)
    cert_b64 = base64.b64encode(cert_der).decode("ascii")
    cert_node = signature.xpath(".//*[local-name()='X509Certificate']")[0]
    cert_node.text = cert_b64

    # Insert signature into the signed element (enveloped)
    target.append(signature)
    return root


def _sign_event_sibling(*, root: etree._Element, id_value: str, material) -> etree._Element:
    target = _find_by_id(root, id_value)
    _remove_existing_signatures(root)

    signature = _build_signature_template_default_ns(reference_uri=f"#{id_value}")

    digest = hashlib.sha256(_c14n_exc(target)).digest()
    signature.xpath(".//*[local-name()='DigestValue']")[0].text = base64.b64encode(digest).decode("ascii")

    signed_info = signature.xpath(".//*[local-name()='SignedInfo']")[0]
    signed_info_c14n = _c14n_exc(signed_info)
    signature_bytes = material.private_key.sign(
        signed_info_c14n,
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    signature.xpath(".//*[local-name()='SignatureValue']")[0].text = base64.b64encode(signature_bytes).decode("ascii")

    cert_der = material.certificate.public_bytes(Encoding.DER)
    signature.xpath(".//*[local-name()='X509Certificate']")[0].text = base64.b64encode(cert_der).decode("ascii")

    parent = target.getparent()
    if parent is None:
        raise ValueError("No se encontró el padre del nodo firmado")
    idx = parent.index(target)
    parent.insert(idx + 1, signature)
    return root


def _sign_de_detached(*, root: etree._Element, de_id: str, material) -> etree._Element:
    """Sign DE as detached signature (Signature is sibling of DE under rDE).

    This matches SIFEN schema where `ds:Signature` is expected directly under `rDE`
    (before `gCamFuFD`).
    """

    # Remove any existing Signature at root level
    _remove_existing_signatures(root)

    de = _find_by_id(root, de_id)
    signature = _build_signature_template_detached(reference_uri=f"#{de_id}")

    # Digest over the referenced DE node (canonicalized)
    digest = hashlib.sha256(_c14n_exc(de)).digest()
    digest_b64 = base64.b64encode(digest).decode("ascii")
    signature.xpath(".//*[local-name()='DigestValue']")[0].text = digest_b64

    signed_info = signature.xpath(".//*[local-name()='SignedInfo']")[0]
    signed_info_c14n = _c14n_exc(signed_info)
    signature_bytes = material.private_key.sign(
        signed_info_c14n,
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    signature.xpath(".//*[local-name()='SignatureValue']")[0].text = base64.b64encode(signature_bytes).decode("ascii")

    cert_der = material.certificate.public_bytes(Encoding.DER)
    signature.xpath(".//*[local-name()='X509Certificate']")[0].text = base64.b64encode(cert_der).decode("ascii")

    # Insert Signature after DE
    parent = de.getparent()
    if parent is None:
        raise ValueError("No se encontró el padre de DE")
    idx = parent.index(de)
    parent.insert(idx + 1, signature)
    return root


def _serialize_with_decl(root: etree._Element) -> str:
    """Serialize XML with double-quote declaration.

    lxml defaults to single quotes in the declaration; this matches the user's
    preferred formatting.
    """

    body = etree.tostring(root, xml_declaration=False, encoding="UTF-8")
    return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + body.decode("utf-8")


def _serialize_with_decl_event(root: etree._Element) -> str:
    body = etree.tostring(root, xml_declaration=False, encoding="UTF-8")
    return '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' + body.decode("utf-8")


def sign_de_xml(xml: str, pfx_bytes: bytes, password: Optional[str] = None) -> str:
    """Sign a DE XML string using a PKCS#12 (.p12/.pfx) container.

    The signature is created with:
    - Enveloped signature transform
    - Exclusive canonicalization (exc-c14n)
    - RSA-SHA256
    - SHA256 digest

    The reference URI will be `#<DE@Id>`.

    Args:
        xml: Unsigned XML string (`rDE`).
        pfx_bytes: PKCS#12 container bytes.
        password: PKCS#12 password.

    Returns:
        Signed XML string.
    """

    material = load_pkcs12(pfx_bytes=pfx_bytes, password=password)

    # Some pretty printers may leave leading newlines/spaces before the XML declaration.
    # lxml requires the declaration to be the first bytes of the document.
    root = etree.fromstring(xml.lstrip().encode("utf-8"))

    # Find the DE element and its Id
    de_nodes = root.xpath("//*[local-name()='DE']")
    if not de_nodes:
        raise ValueError("No se encontró el nodo DE en el XML")
    de = de_nodes[0]

    de_id = de.get("Id")
    if not de_id:
        raise ValueError("El nodo DE no tiene atributo Id")

    signed_root = _sign_de_detached(root=root, de_id=de_id, material=material)
    return _serialize_with_decl(signed_root)


def get_certificate_expiration(pfx_bytes: bytes, password: Optional[str] = None) -> _dt.datetime:
    """Return certificate expiration datetime from a PKCS#12 container."""

    material = load_pkcs12(pfx_bytes=pfx_bytes, password=password)
    return material.not_valid_after


def sign_event_xml(xml: str, pfx_bytes: bytes, password: Optional[str] = None) -> str:
    """Sign an Evento XML (SOAP envelope) using a PKCS#12 container.

    This function expects the XML created by `sifen.xmlgen.events.generate_event_xml`:
    - SOAP 1.2 envelope
    - `rEnviEventoDe` body containing `dEvReg` with the event payload

    The signature is generated as an enveloped XMLDSig inside `rEve` (Id="1"),
    using RSA-SHA256 + SHA256 digest + exc-c14n.

    Args:
        xml: SOAP envelope XML.
        pfx_bytes: PKCS#12 bytes.
        password: PKCS#12 password.

    Returns:
        Signed SOAP XML.
    """
    material = load_pkcs12(pfx_bytes=pfx_bytes, password=password)

    root = etree.fromstring(xml.encode("utf-8"))

    # Find rEve element (payload is inside SOAP body)
    r_eve_nodes = root.xpath("//*[local-name()='rEve']")
    if not r_eve_nodes:
        raise ValueError("No se encontró el nodo rEve en el evento")

    r_eve = r_eve_nodes[0]
    eve_id = r_eve.get("Id")
    if not eve_id:
        raise ValueError("El nodo rEve no tiene atributo Id")

    signed_root = _sign_event_sibling(root=root, id_value=eve_id, material=material)
    return _serialize_with_decl_event(signed_root)
