"""Generate and insert SIFEN QR data into a signed DE XML.

This ports the behavior of the existing Node/TS `qrgen` package:
- Requires an XML that already contains `ds:Signature` and `DigestValue`.
- Builds `dCarQR` URL using SIFEN rules and inserts it into `gCamFuFD`.

References used in URL:
- `nVersion`, `Id` (CDC), `dFeEmiDE` (hex), `dRucRec` or `dNumIDRec`,
  `dTotGralOpe`, `dTotIVA`, `cItems`, `DigestValue` (hex), `IdCSC`, `cHashQR`.

`cHashQR` is computed as SHA256(qr_query_string + CSC) in hex.
"""

from __future__ import annotations

import hashlib
import urllib.parse

from lxml import etree


_NS_SIFEN = "http://ekuatia.set.gov.py/sifen/xsd"
_NS_DS = "http://www.w3.org/2000/09/xmldsig#"


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

    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 add_qr_to_signed_de_xml(
    xml_signed: str,
    *,
    id_csc: int,
    csc: str,
    test: bool = True,
) -> str:
    """Insert `gCamFuFD/dCarQR` into an already signed DE XML.

    Args:
        xml_signed: Signed DE XML as string (must contain `ds:Signature`).
        id_csc: CSC identifier (`IdCSC`).
        csc: CSC secret value (must not be committed).
        test: If True, uses consultas-test base URL.

    Returns:
        Updated XML string including `gCamFuFD/dCarQR`.

    Raises:
        ValueError: if required fields are missing (Signature/DigestValue, DE@Id, etc.).
    """

    root = etree.fromstring(xml_signed.lstrip().encode("utf-8"))
    ns = {"s": _NS_SIFEN, "ds": _NS_DS}

    # Ensure signature exists and fetch DigestValue
    digest_value = root.xpath("string(//ds:DigestValue)", namespaces=ns)
    if not digest_value:
        raise ValueError("XML firmado no contiene ds:DigestValue; no se puede generar QR")

    # Extract DE and required values
    de_id = root.xpath("string(/s:rDE/s:DE/@Id)", namespaces=ns)
    if not de_id:
        raise ValueError("No se encontró DE@Id")

    dfe_emi = root.xpath("string(/s:rDE/s:DE/s:gDatGralOpe/s:dFeEmiDE)", namespaces=ns)
    if not dfe_emi:
        raise ValueError("No se encontró dFeEmiDE")

    # Receiver identifier
    druc_rec = root.xpath("string(/s:rDE/s:DE/s:gDatGralOpe/s:gDatRec/s:dRucRec)", namespaces=ns)
    dnumid_rec = root.xpath("string(/s:rDE/s:DE/s:gDatGralOpe/s:gDatRec/s:dNumIDRec)", namespaces=ns)

    # Totals
    dtot_gral_ope = root.xpath("string(/s:rDE/s:DE/s:gTotSub/s:dTotGralOpe)", namespaces=ns)
    dtot_iva = root.xpath("string(/s:rDE/s:DE/s:gTotSub/s:dTotIVA)", namespaces=ns)
    # Algunos tipos de documento no incluyen gTotSub. Para no bloquear el envío,
    # usamos 0 como valor por defecto en la URL del QR.
    if dtot_gral_ope == "":
        dtot_gral_ope = "0"
    if dtot_iva == "":
        dtot_iva = "0"

    # Count items
    items_count = int(
        root.xpath("count(/s:rDE/s:DE/s:gDtipDE/s:gCamItem)", namespaces=ns)
    )

    base_url = "https://ekuatia.set.gov.py/consultas-test/qr" if test else "https://ekuatia.set.gov.py/consultas/qr"

    # Hex encoding (matches Node: Buffer.from(str).toString('hex'))
    dfe_emi_hex = dfe_emi.encode("utf-8").hex()
    digest_hex = digest_value.encode("utf-8").hex()

    # Orden exacto de parámetros según especificación SIFEN
    # El orden afecta el cálculo del hash cHashQR
    query_parts = [
        ("nVersion", "150"),
        ("Id", de_id),
        ("dFeEmiDE", dfe_emi_hex),
    ]

    if druc_rec:
        query_parts.append(("dRucRec", druc_rec))
    elif dnumid_rec:
        query_parts.append(("dNumIDRec", dnumid_rec))

    query_parts.extend([
        ("dTotGralOpe", dtot_gral_ope),
        ("dTotIVA", dtot_iva),
        ("cItems", str(items_count)),
        ("DigestValue", digest_hex),
        ("IdCSC", str(id_csc)),
    ])

    # Build query string (sin URL base, como lo hace facturacionelectronicapy-qrgen)
    # El hash se calcula sobre los parámetros solamente, no sobre la URL completa
    query_string = "&".join(f"{k}={v}" for k, v in query_parts)

    # cHashQR = sha256(query_string + CSC)
    h = hashlib.sha256()
    h.update((query_string + csc).encode("utf-8"))
    chash = h.hexdigest()

    full_url = f"{base_url}?{query_string}&cHashQR={chash}"

    # Insert into XML: rDE/gCamFuFD/dCarQR
    gcamf = root.xpath("/s:rDE/s:gCamFuFD", namespaces=ns)
    if gcamf:
        gcamf_node = gcamf[0]
    else:
        gcamf_node = etree.SubElement(root, f"{{{_NS_SIFEN}}}gCamFuFD")

    dcar = gcamf_node.xpath("./s:dCarQR", namespaces=ns)
    if dcar:
        dcar_node = dcar[0]
    else:
        dcar_node = etree.SubElement(gcamf_node, f"{{{_NS_SIFEN}}}dCarQR")

    # Usar CDATA para evitar escape de & como &amp;
    dcar_node.text = etree.CDATA(full_url)

    # Preserve/approximate pretty formatting if input already contains indentation.
    # We do NOT re-indent the whole tree (would change signed subtree). We only
    # set tails for newly inserted nodes when the document appears formatted.
    if (root.text or "").startswith("\n"):
        if gcamf_node.tail is None:
            gcamf_node.tail = "\n"
        if dcar_node.tail is None:
            dcar_node.tail = "\n  "

    return _serialize_with_decl(root)
