"""Generación de XML DE (Documento Electrónico).

Este módulo construye el XML principal `rDE` siguiendo la estructura de SIFEN.
Se mantiene el orden de nodos de forma determinística para facilitar la firma
y reproducibilidad.
"""

from __future__ import annotations

import secrets
import datetime as _dt
from lxml import etree

from ..algorithms import generate_cdc
from ..condition import build_gcamcond
from ..config import XmlgenConfig
from ..condition import build_gcamcond
from ..items import build_gcamitem_nodes
from ..totals import build_gtotsub
from ..utils import left_zero, normalize_xml
from ..utils import to_json_datetime, to_iso_datetime_no_offset
from ..errors import ErrorCollector
from .validator import validate_de
from ..catalogs import (
    descripcion_condicion_anticipo,
    descripcion_pais,
    descripcion_moneda,
    descripcion_tipo_documento,
    descripcion_tipo_emision,
    descripcion_tipo_impuesto,
    descripcion_tipo_documento_identidad_receptor,
    descripcion_tipo_transaccion,
    descripcion_indicador_presencia,
    descripcion_departamento,
    descripcion_distrito,
    descripcion_ciudad,
    descripcion_motivo_emision_ncde,
    descripcion_motivo_traslado_nre,
    descripcion_resp_emi_nre,
    descripcion_tipo_doc_asociado,
)


def _build_rde_root(params: dict) -> etree._Element:
    """Crea el nodo raíz `rDE` con namespaces y schemaLocation."""
    nsmap = {
        None: "http://ekuatia.set.gov.py/sifen/xsd",
        "xsi": "http://www.w3.org/2001/XMLSchema-instance",
    }
    rde = etree.Element("rDE", nsmap=nsmap)
    rde.set(
        "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation",
        "http://ekuatia.set.gov.py/sifen/xsd siRecepDE_v150.xsd",
    )
    etree.SubElement(rde, "dVerFor").text = str(params.get("version", ""))
    return rde


def _build_de_element(
    rde: etree._Element, params: dict, data: dict, cdc: str
) -> etree._Element:
    """Crea el nodo `DE` y campos base (Id, dDVId, dFecFirma, dSisFact)."""
    de = etree.SubElement(rde, "DE")
    de.set("Id", cdc)

    etree.SubElement(de, "dDVId").text = str(cdc)[-1:]

    fecha_firma = data.get("fechaFirmaDigital")

    # Obtenemos la fecha de emisión para comparar
    fe_emi = data.get("fecha")
    dt_emi = None
    if fe_emi:
        dt_emi = _dt.datetime.fromisoformat(str(fe_emi).replace(" ", "T")[:19])

    if fecha_firma is None:
        # SIFEN requiere dFeEmiDE <= dFecEm <= dFecFirma
        # Usamos la hora local del sistema
        now_py = _dt.datetime.now()

        # Obtenemos dFecEm calculado más adelante (o su entrada)
        # Para simplificar, comparamos contra dt_emi + un margen seguro.
        if dt_emi and now_py < dt_emi:
            dt_firma = dt_emi + _dt.timedelta(seconds=20)
        else:
            dt_firma = now_py
    elif isinstance(fecha_firma, str):
        dt_firma = _dt.datetime.fromisoformat(fecha_firma.replace(" ", "T")[:19])
    else:
        dt_firma = fecha_firma

    etree.SubElement(de, "dFecFirma").text = to_json_datetime(dt_firma)
    etree.SubElement(de, "dSisFact").text = "1"
    return de


def _build_goped(de: etree._Element, data: dict, cdc: str) -> None:
    """Construye `gOpeDE` (tipo de emisión, código de seguridad, info)."""
    goped = etree.SubElement(de, "gOpeDE")
    tipo_emision = data.get("tipoEmision")
    if tipo_emision is not None:
        etree.SubElement(goped, "iTipEmi").text = str(tipo_emision)
        etree.SubElement(goped, "dDesTipEmi").text = descripcion_tipo_emision(
            int(tipo_emision)
        )

    etree.SubElement(goped, "dCodSeg").text = str(cdc)[34:43]

    obs = data.get("observacion")
    if obs:
        etree.SubElement(goped, "dInfoEmi").text = str(obs)

    desc = data.get("descripcion")
    if desc:
        etree.SubElement(goped, "dInfoFisc").text = str(desc)


def _build_gtimb(de: etree._Element, params: dict, data: dict) -> None:
    """Construye `gTimb` (timbrado y numeración del documento)."""
    gtimb = etree.SubElement(de, "gTimb")
    if data.get("tipoDocumento") is not None:
        etree.SubElement(gtimb, "iTiDE").text = str(data.get("tipoDocumento"))
    tipo_doc_desc = data.get("tipoDocumentoDescripcion")
    if tipo_doc_desc is None and data.get("tipoDocumento") is not None:
        tipo_doc_desc = descripcion_tipo_documento(int(data.get("tipoDocumento")))
    if tipo_doc_desc:
        etree.SubElement(gtimb, "dDesTiDE").text = str(tipo_doc_desc)

    if params.get("timbradoNumero") is not None:
        etree.SubElement(gtimb, "dNumTim").text = str(params.get("timbradoNumero"))

    if data.get("establecimiento") is not None:
        etree.SubElement(gtimb, "dEst").text = left_zero(data.get("establecimiento"), 3)
    if data.get("punto") is not None:
        etree.SubElement(gtimb, "dPunExp").text = left_zero(data.get("punto"), 3)
    if data.get("numero") is not None:
        etree.SubElement(gtimb, "dNumDoc").text = left_zero(data.get("numero"), 7)

    serie = data.get("numeroSerie") or data.get("serie")
    if serie:
        etree.SubElement(gtimb, "dSerieNum").text = str(serie)

    timb_fecha = params.get("timbradoFecha")
    if timb_fecha:
        etree.SubElement(gtimb, "dFeIniT").text = str(timb_fecha)[:10]


def _build_gdatgralope(
    de: etree._Element, params: dict, data: dict, config: XmlgenConfig
) -> None:
    """Construye `gDatGralOpe` (operación comercial, emisor y receptor)."""
    gdat = etree.SubElement(de, "gDatGralOpe")
    if data.get("fecha") is not None:
        val_fe = str(data.get("fecha")).replace(" ", "T")
        if "T" in val_fe:
            # Eliminar microsegundos si existen (SIFEN solo acepta hasta segundos)
            if "." in val_fe:
                val_fe = val_fe.split(".")[0]
            # Eliminar offset de zona horaria si existe (SIFEN acepta formato sin offset)
            if "+" in val_fe or ("-" in val_fe and val_fe.count("-") > 2):
                # Formato: 2026-02-16T19:10:20-03:00 -> 2026-02-16T19:10:20
                val_fe = val_fe.split("+")[0].split("-03:00")[0].split("-04:00")[0]
        etree.SubElement(gdat, "dFeEmiDE").text = val_fe

    # gOpeCom (inherentes a la operación)
    if data.get("tipoDocumento") != 7:
        gopecom = etree.SubElement(gdat, "gOpeCom")

        if (
            data.get("tipoDocumento") in (1, 4)
            and data.get("tipoTransaccion") is not None
        ):
            etree.SubElement(gopecom, "iTipTra").text = str(data.get("tipoTransaccion"))
            tip_tra_desc = data.get("tipoTransaccionDescripcion")
            if tip_tra_desc is None:
                tip_tra_desc = descripcion_tipo_transaccion(
                    int(data.get("tipoTransaccion"))
                )
            if tip_tra_desc:
                etree.SubElement(gopecom, "dDesTipTra").text = str(tip_tra_desc)

        if data.get("tipoImpuesto") is not None:
            etree.SubElement(gopecom, "iTImp").text = str(data.get("tipoImpuesto"))
            tip_imp_desc = data.get("tipoImpuestoDescripcion")
            if tip_imp_desc is None:
                tip_imp_desc = descripcion_tipo_impuesto(int(data.get("tipoImpuesto")))
            if tip_imp_desc:
                etree.SubElement(gopecom, "dDesTImp").text = str(tip_imp_desc)

        moneda = data.get("moneda")
        if not moneda and config.default_values:
            moneda = "PYG"
        if moneda:
            etree.SubElement(gopecom, "cMoneOpe").text = str(moneda)
            mon_desc = data.get("monedaDescripcion")
            if mon_desc is None:
                mon_desc = descripcion_moneda(str(moneda))
            if mon_desc:
                etree.SubElement(gopecom, "dDesMoneOpe").text = str(mon_desc)

        if data.get("condicionAnticipo") is not None:
            etree.SubElement(gopecom, "iCondAnt").text = str(
                data.get("condicionAnticipo")
            )
            cond_ant_desc = data.get("condicionAnticipoDescripcion")
            if cond_ant_desc is None:
                cond_ant_desc = descripcion_condicion_anticipo(
                    int(data.get("condicionAnticipo"))
                )
                if cond_ant_desc:
                    cond_ant_desc = "Anticipo " + cond_ant_desc
            if cond_ant_desc:
                etree.SubElement(gopecom, "dDesCondAnt").text = str(cond_ant_desc)

    # gEmis (emisor)
    emis = etree.SubElement(gdat, "gEmis")
    ruc = params.get("ruc")
    if ruc and "-" in str(ruc):
        etree.SubElement(emis, "dRucEm").text = str(ruc).split("-")[0]
        etree.SubElement(emis, "dDVEmi").text = str(ruc).split("-")[1]

    if params.get("tipoContribuyente") is not None:
        etree.SubElement(emis, "iTipCont").text = str(params.get("tipoContribuyente"))
    if params.get("tipoRegimen") is not None:
        etree.SubElement(emis, "cTipReg").text = str(params.get("tipoRegimen"))

    if params.get("razonSocial") is not None:
        etree.SubElement(emis, "dNomEmi").text = str(params.get("razonSocial"))
    if params.get("nombreFantasia"):
        etree.SubElement(emis, "dNomFanEmi").text = str(params.get("nombreFantasia"))

    # Dirección emisor desde establecimiento
    est = (
        left_zero(data.get("establecimiento"), 3)
        if data.get("establecimiento") is not None
        else None
    )
    establecimientos = params.get("establecimientos") or []
    est_obj = None
    if est is not None:
        for e in establecimientos:
            if str(e.get("codigo")) == str(est):
                est_obj = e
                break

    if est_obj is not None:
        # Dirección emisor (ORDEN XSD ESTRICTO)
        if est_obj.get("direccion"):
            etree.SubElement(emis, "dDirEmi").text = str(est_obj.get("direccion"))
        else:
            etree.SubElement(emis, "dDirEmi").text = "S/N"

        # dNumCas ES OBLIGATORIO si hay dirección
        num_casa = est_obj.get("numeroCasa")
        if num_casa is not None and str(num_casa).strip():
            etree.SubElement(emis, "dNumCas").text = str(num_casa)
        else:
            etree.SubElement(emis, "dNumCas").text = "0"

        # Complementos (opcionales)
        if est_obj.get("complementoDireccion1"):
            etree.SubElement(emis, "dCompDir1").text = str(
                est_obj.get("complementoDireccion1")
            )

        if est_obj.get("complementoDireccion2"):
            etree.SubElement(emis, "dCompDir2").text = str(
                est_obj.get("complementoDireccion2")
            )

        if est_obj.get("departamento") is not None:
            dep_code = int(est_obj.get("departamento"))
            etree.SubElement(emis, "cDepEmi").text = str(dep_code)
            dep_desc = descripcion_departamento(dep_code) or (
                est_obj.get("departamentoDescripcion") or ""
            )
            if dep_desc:
                etree.SubElement(emis, "dDesDepEmi").text = str(dep_desc)

        if est_obj.get("distrito") is not None:
            dis_code = int(est_obj.get("distrito"))
            etree.SubElement(emis, "cDisEmi").text = str(dis_code)
            dis_desc = descripcion_distrito(dis_code) or (
                est_obj.get("distritoDescripcion") or ""
            )
            if dis_desc:
                etree.SubElement(emis, "dDesDisEmi").text = str(dis_desc)

        if est_obj.get("ciudad") is not None:
            ciu_code = int(est_obj.get("ciudad"))
            etree.SubElement(emis, "cCiuEmi").text = str(ciu_code)
            ciu_desc = descripcion_ciudad(ciu_code) or (
                est_obj.get("ciudadDescripcion") or ""
            )
            if ciu_desc:
                etree.SubElement(emis, "dDesCiuEmi").text = str(ciu_desc)

        if est_obj.get("telefono") is not None:
            etree.SubElement(emis, "dTelEmi").text = str(est_obj.get("telefono"))

        email = est_obj.get("email")
        if email:
            email_str = str(email)
            if "," in email_str:
                email_str = email_str.split(",")[0].strip()
            etree.SubElement(emis, "dEmailE").text = email_str.strip()

        if est_obj.get("denominacion") is not None:
            etree.SubElement(emis, "dDenSuc").text = str(est_obj.get("denominacion"))
        elif est_obj.get("nombre"):
            etree.SubElement(emis, "dDenSuc").text = str(est_obj.get("nombre"))

    # gActEco
    act = params.get("actividadesEconomicas") or []
    for actividad in act:
        gacteco = etree.SubElement(emis, "gActEco")
        if actividad.get("codigo") is not None:
            etree.SubElement(gacteco, "cActEco").text = str(actividad.get("codigo"))
        if actividad.get("descripcion") is not None:
            etree.SubElement(gacteco, "dDesActEco").text = str(
                actividad.get("descripcion")
            )

    # gRespDE (Responsable de la generación del DE)
    resp = data.get("responsable")
    if resp:
        gresp = etree.SubElement(emis, "gRespDE")
        etree.SubElement(gresp, "iTipIDRespDE").text = str(resp.get("tipoID", 1))
        # TODO: Add catalog for dDTipIDRespDE if needed, for now use default or provided
        etree.SubElement(gresp, "dDTipIDRespDE").text = str(
            resp.get("tipoIDDescripcion") or "Cédula paraguaya"
        )
        etree.SubElement(gresp, "dNumIDRespDE").text = str(resp.get("numeroID"))
        etree.SubElement(gresp, "dNomRespDE").text = str(resp.get("nombre"))
        etree.SubElement(gresp, "dCarRespDE").text = str(resp.get("cargo"))

    # gDatRec (receptor)
    cliente = data.get("cliente") or {}
    gdatrec = etree.SubElement(gdat, "gDatRec")
    etree.SubElement(gdatrec, "iNatRec").text = (
        "1" if cliente.get("contribuyente") else "2"
    )
    if cliente.get("tipoOperacion") is not None:
        etree.SubElement(gdatrec, "iTiOpe").text = str(
            int(cliente.get("tipoOperacion"))
        )

    if cliente.get("pais") is not None:
        etree.SubElement(gdatrec, "cPaisRec").text = str(cliente.get("pais"))
    pais_rec_desc = cliente.get("paisDescripcion")
    if pais_rec_desc is None and cliente.get("pais") is not None:
        pais_rec_desc = descripcion_pais(str(cliente.get("pais")))
    if pais_rec_desc:
        etree.SubElement(gdatrec, "dDesPaisRe").text = str(pais_rec_desc)

    if cliente.get("contribuyente"):
        if cliente.get("tipoContribuyente") is not None:
            etree.SubElement(gdatrec, "iTiContRec").text = str(
                cliente.get("tipoContribuyente")
            )
        if cliente.get("ruc") and "-" in str(cliente.get("ruc")):
            etree.SubElement(gdatrec, "dRucRec").text = (
                str(cliente.get("ruc")).split("-")[0].strip()
            )
            etree.SubElement(gdatrec, "dDVRec").text = (
                str(cliente.get("ruc")).split("-")[1].strip()
            )
    else:
        if cliente.get("documentoTipo") is not None:
            doc_tipo = int(cliente.get("documentoTipo"))
            etree.SubElement(gdatrec, "iTipIDRec").text = str(doc_tipo)

            # SIFEN valida que la descripción corresponda al código.
            # Para evitar inconsistencias, derivamos la descripción desde el código.
            doc_tipo_desc = descripcion_tipo_documento_identidad_receptor(doc_tipo)
            if doc_tipo_desc:
                etree.SubElement(gdatrec, "dDTipIDRec").text = str(doc_tipo_desc)
            else:
                # Fallback: mantener el comportamiento anterior si no tenemos catálogo.
                if cliente.get("documentoTipoDescripcion") is not None:
                    etree.SubElement(gdatrec, "dDTipIDRec").text = str(
                        cliente.get("documentoTipoDescripcion")
                    )
        if cliente.get("documentoNumero") is not None:
            etree.SubElement(gdatrec, "dNumIDRec").text = str(
                cliente.get("documentoNumero")
            ).strip()

    if cliente.get("razonSocial") is not None:
        etree.SubElement(gdatrec, "dNomRec").text = str(
            cliente.get("razonSocial")
        ).strip()
    if cliente.get("nombreFantasia") is not None:
        etree.SubElement(gdatrec, "dNomFanRec").text = str(
            cliente.get("nombreFantasia")
        ).strip()
    dir_rec = (
        str(cliente.get("direccion")).strip()
        if cliente.get("direccion") is not None
        else ""
    )
    etree.SubElement(gdatrec, "dDirRec").text = dir_rec or "S/N"
    if cliente.get("numeroCasa") is not None:
        etree.SubElement(gdatrec, "dNumCasRec").text = str(
            cliente.get("numeroCasa")
        ).strip()

    # Ubicación receptor si no es tipoOperacion=4
    try:
        tipo_ope = (
            int(cliente.get("tipoOperacion"))
            if cliente.get("tipoOperacion") is not None
            else None
        )
    except Exception:
        tipo_ope = None

    if dir_rec and tipo_ope != 4:
        if cliente.get("departamento") is not None:
            dep_code = int(cliente.get("departamento"))
            etree.SubElement(gdatrec, "cDepRec").text = str(dep_code)
            dep_desc = descripcion_departamento(dep_code) or (
                cliente.get("departamentoDescripcion") or ""
            )
            if dep_desc:
                etree.SubElement(gdatrec, "dDesDepRec").text = str(dep_desc)

        if cliente.get("distrito") is not None:
            dis_code = int(cliente.get("distrito"))
            etree.SubElement(gdatrec, "cDisRec").text = str(dis_code)
            dis_desc = descripcion_distrito(dis_code) or (
                cliente.get("distritoDescripcion") or ""
            )
            if dis_desc:
                etree.SubElement(gdatrec, "dDesDisRec").text = str(dis_desc)

        if cliente.get("ciudad") is not None:
            ciu_code = int(cliente.get("ciudad"))
            etree.SubElement(gdatrec, "cCiuRec").text = str(ciu_code)
            ciu_desc = descripcion_ciudad(ciu_code) or (
                cliente.get("ciudadDescripcion") or ""
            )
            if ciu_desc:
                etree.SubElement(gdatrec, "dDesCiuRec").text = str(ciu_desc)

    tel = cliente.get("telefono")
    if tel and str(tel).strip():
        etree.SubElement(gdatrec, "dTelRec").text = str(tel).strip()
    email = cliente.get("email")
    if email and str(email).strip():
        etree.SubElement(gdatrec, "dEmailRec").text = str(email).strip()


def generate_de(params: dict, data: dict, config: XmlgenConfig) -> str:
    """Genera el XML del DE en forma de string (normalizado para firma)."""
    errors = ErrorCollector(
        error_separator=config.error_separator, error_limit=config.error_limit
    )
    validate_de(params=params, data=data, config=config, errors=errors)
    errors.raise_if_any()

    if data.get("cdc") and len(str(data["cdc"])) == 44:
        cdc = str(data["cdc"])
    else:
        codigo_seguridad_aleatorio = data.get("codigoSeguridadAleatorio")
        if codigo_seguridad_aleatorio is None:
            codigo_seguridad_aleatorio = f"{secrets.randbelow(10**9):09d}"
        else:
            codigo_seguridad_aleatorio = str(codigo_seguridad_aleatorio).zfill(9)

        cdc = generate_cdc(
            params=params, data=data, codigo_seguridad=codigo_seguridad_aleatorio
        )

    root = _build_rde_root(params=params)
    de = _build_de_element(rde=root, params=params, data=data, cdc=cdc)
    _build_goped(de=de, data=data, cdc=cdc)
    _build_gtimb(de=de, params=params, data=data)
    _build_gdatgralope(de=de, params=params, data=data, config=config)

    # gDtipDE
    gdtip = etree.SubElement(de, "gDtipDE")

    # gCamAE (obligatorio para Autofactura Electrónica iTiDE=4)
    if data.get("tipoDocumento") == 4:
        af = data.get("autoFactura") or {}
        gcamae = etree.SubElement(gdtip, "gCamAE")

        nat_ven = int(af.get("naturaleza") or 1)
        etree.SubElement(gcamae, "iNatVen").text = str(nat_ven)
        etree.SubElement(gcamae, "dDesNatVen").text = (
            "Persona Física" if nat_ven == 1 else "Persona Jurídica"
        )

        tip_id_ven = int(af.get("tipoDocumento") or 1)
        etree.SubElement(gcamae, "iTipIDVen").text = str(tip_id_ven)
        tip_id_ven_desc = (
            descripcion_tipo_documento_identidad_receptor(tip_id_ven)
            or "Cédula paraguaya"
        )
        etree.SubElement(gcamae, "dDTipIDVen").text = str(tip_id_ven_desc)

        etree.SubElement(gcamae, "dNumIDVen").text = str(
            af.get("documentoNumero") or ""
        )
        etree.SubElement(gcamae, "dNomVen").text = str(af.get("nombre") or "")
        etree.SubElement(gcamae, "dDirVen").text = str(af.get("direccion") or "S/N")
        etree.SubElement(gcamae, "dNumCasVen").text = str(af.get("numeroCasa") or "0")

        if af.get("departamento"):
            dep_code = int(af.get("departamento"))
            etree.SubElement(gcamae, "cDepVen").text = str(dep_code)
            dep_desc = descripcion_departamento(dep_code)
            if dep_desc:
                etree.SubElement(gcamae, "dDesDepVen").text = str(dep_desc)

        if af.get("distrito"):
            dis_code = int(af.get("distrito"))
            etree.SubElement(gcamae, "cDisVen").text = str(dis_code)
            dis_desc = descripcion_distrito(dis_code)
            if dis_desc:
                etree.SubElement(gcamae, "dDesDisVen").text = str(dis_desc)

        if af.get("ciudad"):
            ciu_code = int(af.get("ciudad"))
            etree.SubElement(gcamae, "cCiuVen").text = str(ciu_code)
            ciu_desc = descripcion_ciudad(ciu_code)
            if ciu_desc:
                etree.SubElement(gcamae, "dDesCiuVen").text = str(ciu_desc)

    # gCamFE (obligatorio para Factura Electrónica iTiDE=1)
    if data.get("tipoDocumento") == 1:
        gcamfe = etree.SubElement(gdtip, "gCamFE")
        ind_pres = data.get("indicadorPresencia")
        if ind_pres is None:
            ind_pres = 1
        ind_pres_i = int(ind_pres)
        etree.SubElement(gcamfe, "iIndPres").text = str(ind_pres_i)
        des_ind = data.get("indicadorPresenciaDescripcion")
        if des_ind is None:
            des_ind = descripcion_indicador_presencia(ind_pres_i)
        if des_ind:
            etree.SubElement(gcamfe, "dDesIndPres").text = str(des_ind)

    # gCamNCDE (obligatorio para Nota de crédito/débito electrónica iTiDE=5/6)
    if data.get("tipoDocumento") in (5, 6):
        gcamncde = etree.SubElement(gdtip, "gCamNCDE")
        ncde = data.get("notaCreditoDebito") or {}
        motivo = ncde.get("motivo")
        if motivo is None:
            motivo = 2 if data.get("tipoDocumento") == 5 else 8
        motivo_i = int(motivo)
        etree.SubElement(gcamncde, "iMotEmi").text = str(motivo_i)
        mot_desc = ncde.get("motivoDescripcion")
        if mot_desc is None:
            mot_desc = descripcion_motivo_emision_ncde(motivo_i)
        if mot_desc:
            etree.SubElement(gcamncde, "dDesMotEmi").text = str(mot_desc)

    # gCamNRE (obligatorio para Nota de remisión electrónica iTiDE=7)
    if data.get("tipoDocumento") == 7:
        gcamnre = etree.SubElement(gdtip, "gCamNRE")
        nre = (
            data.get("remision")
            or data.get("notaRemision")
            or data.get("complementoRemision")
            or {}
        )

        motivo = nre.get("motivo")
        if motivo is None:
            motivo = 1
        motivo_i = int(motivo)
        etree.SubElement(gcamnre, "iMotEmiNR").text = str(motivo_i)
        mot_desc = nre.get("motivoDescripcion")
        if mot_desc is None:
            mot_desc = descripcion_motivo_traslado_nre(motivo_i)
        if mot_desc:
            etree.SubElement(gcamnre, "dDesMotEmiNR").text = str(mot_desc)

        resp = nre.get("responsableEmi") or nre.get("responsable")
        if resp is None:
            resp = 1
        resp_i = int(resp)
        etree.SubElement(gcamnre, "iRespEmiNR").text = str(resp_i)
        resp_desc = nre.get("responsableDescripcion")
        if resp_desc is None:
            resp_desc = descripcion_resp_emi_nre(resp_i)
        if resp_desc:
            etree.SubElement(gcamnre, "dDesRespEmiNR").text = str(resp_desc)

        km = nre.get("kmEstimado") or nre.get("km") or nre.get("kilometros")
        if km is None:
            km = 1
        etree.SubElement(gcamnre, "dKmR").text = str(int(km))

        # dFecEm (Fecha inicio traslado) - SIFEN requiere solo fecha YYYY-MM-DD
        tp_list = data.get("transporte")
        trans_first = (
            tp_list[0] if isinstance(tp_list, list) and tp_list else tp_list
        ) or {}
        fec = nre.get("fechaInicio") or trans_first.get("iniFechaEstimadaTrans")
        val_fec_em = None
        fec_fin = nre.get("fechaFin") or trans_first.get("finFechaEstimadaTrans")
        val_fec_fin = None
        if fec:
            val_fec_em = str(fec)[:10]  # Solo YYYY-MM-DD
            etree.SubElement(gcamnre, "dFecEm").text = val_fec_em
        if fec_fin:
            val_fec_fin = str(fec_fin)[:10]  # Solo YYYY-MM-DD

    # gCamCond (condición de operación — no aplica para Nota de Remisión iTiDE=7 ni NC/ND iTiDE=5,6)
    if data.get("tipoDocumento") not in (5, 6, 7):
        build_gcamcond(parent=gdtip, data=data)

    # Ítems
    items_summary = build_gcamitem_nodes(
        parent=gdtip, params=params, data=data, config=config
    )

    # gTransp (Transporte - Debe ir al final de gDtipDE según XSD)
    nre = (
        data.get("remision")
        or data.get("notaRemision")
        or data.get("complementoRemision")
        or {}
    )
    tp_list = data.get("transporte")
    trans = (
        (tp_list[0] if isinstance(tp_list, list) and tp_list else tp_list)
        or nre.get("transportista")
        or {}
    )
    if data.get("tipoDocumento") == 7 or trans:
        # Re-identificar establecimiento para gCamSal
        est = (
            left_zero(data.get("establecimiento"), 3)
            if data.get("establecimiento") is not None
            else None
        )
        establecimientos = params.get("establecimientos") or []
        est_obj = None
        if est is not None:
            for e in establecimientos:
                if str(e.get("codigo")) == str(est):
                    est_obj = e
                    break

        gtransp = etree.SubElement(gdtip, "gTransp")

        # iTipTrans (1: Propio, 2: Tercero)
        tip_trans = trans.get("tipoTransporte") or trans.get("tipo")
        if tip_trans is None:
            es_tercero = bool(
                data.get("tipoDocumento") == 7
                and (
                    trans.get("transportista", {}).get("ruc")
                    or trans.get("ruc")
                    or trans.get("razonSocial")
                )
            )
            tip_trans = 2 if es_tercero else 1

        i_tip_trans = int(tip_trans)
        etree.SubElement(gtransp, "iTipTrans").text = str(i_tip_trans)
        etree.SubElement(gtransp, "dDesTipTrans").text = (
            "Tercero" if i_tip_trans == 2 else "Propio"
        )

        # iModTrans (1: Terrestre)
        mod_trans = int(trans.get("modalidad", 1))
        etree.SubElement(gtransp, "iModTrans").text = str(mod_trans)
        etree.SubElement(gtransp, "dDesModTrans").text = (
            "Terrestre" if mod_trans == 1 else "Otros"
        )

        # iRespFlete (1: Emisor, 2: Receptor, 3: Tercero, 4: Agente)
        resp_flete = int(
            trans.get("tipoResponsable") or trans.get("responsableFlete") or 1
        )
        etree.SubElement(gtransp, "iRespFlete").text = str(resp_flete)

        # cCondNeg (Incoterms)
        cond_neg = trans.get("condicionNegociacion")
        if cond_neg:
            etree.SubElement(gtransp, "cCondNeg").text = str(cond_neg)

        # Fechas de traslado (Re-usar val_fec_em y val_fec_fin si existen)
        if "val_fec_em" in locals() and val_fec_em:
            etree.SubElement(gtransp, "dIniTras").text = val_fec_em[:10]
        if "val_fec_fin" in locals() and val_fec_fin:
            etree.SubElement(gtransp, "dFinTras").text = val_fec_fin[:10]

        # gCamSal (Punto de salida)
        salida = nre.get("localSalida") or trans.get("salida") or {}
        gcamsal = etree.SubElement(gtransp, "gCamSal")

        # dDirLocSal - Dirección del local de salida (obligatorio, va primero según XSD)
        etree.SubElement(gcamsal, "dDirLocSal").text = str(
            salida.get("direccion")
            or (est_obj.get("direccion") if est_obj else "S/N")
            or "S/N"
        )
        etree.SubElement(gcamsal, "dNumCasSal").text = str(
            salida.get("numeroCasa")
            or (est_obj.get("numeroCasa") if est_obj else "0")
            or "0"
        )
        comp1_sal = salida.get("complemento1") or (
            est_obj.get("complementoDireccion1") if est_obj else None
        )
        if comp1_sal:
            etree.SubElement(gcamsal, "dComp1Sal").text = str(comp1_sal)

        dep_sal_code = int(
            salida.get("departamento")
            or (est_obj.get("departamento") if est_obj else 1)
            or 1
        )
        etree.SubElement(gcamsal, "cDepSal").text = str(dep_sal_code)
        dep_sal_desc = (
            descripcion_departamento(dep_sal_code)
            or salida.get("departamentoDescripcion")
            or (est_obj.get("departamentoDescripcion") if est_obj else "CAPITAL")
        )
        etree.SubElement(gcamsal, "dDesDepSal").text = str(dep_sal_desc or "CAPITAL")

        dis_sal_code = int(
            salida.get("distrito") or (est_obj.get("distrito") if est_obj else 1) or 1
        )
        etree.SubElement(gcamsal, "cDisSal").text = str(dis_sal_code)
        dis_sal_desc = (
            descripcion_distrito(dis_sal_code)
            or salida.get("distritoDescripcion")
            or (
                est_obj.get("distritoDescripcion") if est_obj else "ASUNCION (DISTRITO)"
            )
        )
        etree.SubElement(gcamsal, "dDesDisSal").text = str(
            dis_sal_desc or "ASUNCION (DISTRITO)"
        )

        ciu_sal_code = int(
            salida.get("ciudad") or (est_obj.get("ciudad") if est_obj else 1) or 1
        )
        etree.SubElement(gcamsal, "cCiuSal").text = str(ciu_sal_code)
        ciu_sal_desc = (
            descripcion_ciudad(ciu_sal_code)
            or salida.get("ciudadDescripcion")
            or (est_obj.get("ciudadDescripcion") if est_obj else "ASUNCION (DISTRITO)")
        )
        etree.SubElement(gcamsal, "dDesCiuSal").text = str(
            ciu_sal_desc or "ASUNCION (DISTRITO)"
        )

        # gCamEnt (Punto de entrega)
        entrega = nre.get("localEntrega") or trans.get("entrega") or {}
        gcament = etree.SubElement(gtransp, "gCamEnt")

        cliente = data.get("cliente") or {}

        # dDirLocEnt - Dirección del local de entrega (obligatorio, va primero según XSD)
        etree.SubElement(gcament, "dDirLocEnt").text = str(
            entrega.get("direccion") or cliente.get("direccion") or "S/N"
        )
        etree.SubElement(gcament, "dNumCasEnt").text = str(
            entrega.get("numeroCasa") or cliente.get("numeroCasa") or "0"
        )
        comp1_ent = entrega.get("complemento1")
        if comp1_ent:
            etree.SubElement(gcament, "dComp1Ent").text = str(comp1_ent)

        dep_ent_code = int(
            entrega.get("departamento") or cliente.get("departamento") or 1
        )
        etree.SubElement(gcament, "cDepEnt").text = str(dep_ent_code)
        dep_ent_desc = (
            descripcion_departamento(dep_ent_code)
            or entrega.get("departamentoDescripcion")
            or cliente.get("departamentoDescripcion")
            or "CAPITAL"
        )
        etree.SubElement(gcament, "dDesDepEnt").text = str(dep_ent_desc or "CAPITAL")

        dis_ent_code = int(entrega.get("distrito") or cliente.get("distrito") or 1)
        etree.SubElement(gcament, "cDisEnt").text = str(dis_ent_code)
        dis_ent_desc = (
            descripcion_distrito(dis_ent_code)
            or entrega.get("distritoDescripcion")
            or cliente.get("distritoDescripcion")
            or "ASUNCION (DISTRITO)"
        )
        etree.SubElement(gcament, "dDesDisEnt").text = str(
            dis_ent_desc or "ASUNCION (DISTRITO)"
        )

        ciu_ent_code = int(entrega.get("ciudad") or cliente.get("ciudad") or 1)
        etree.SubElement(gcament, "cCiuEnt").text = str(ciu_ent_code)
        ciu_ent_desc = (
            descripcion_ciudad(ciu_ent_code)
            or entrega.get("ciudadDescripcion")
            or cliente.get("ciudadDescripcion")
            or "ASUNCION (DISTRITO)"
        )
        etree.SubElement(gcament, "dDesCiuEnt").text = str(
            ciu_ent_desc or "ASUNCION (DISTRITO)"
        )

        veh = trans.get("vehiculo")
        if veh:
            gvehtras = etree.SubElement(gtransp, "gVehTras")
            etree.SubElement(gvehtras, "dTiVehTras").text = str(
                veh.get("tipo") or "Otros"
            )
            mar_veh = str(veh.get("marca") or "Sin Marca").strip().upper()
            mar_veh = mar_veh.replace(" ", "")
            etree.SubElement(gvehtras, "dMarVeh").text = mar_veh[:15]

            # Agregar modelo si existe
            if veh.get("modelo"):
                etree.SubElement(gvehtras, "dModVeh").text = str(veh.get("modelo"))[:30]

            tip_id_veh = int(
                veh.get("documentoTipo") or veh.get("tipoIdentificacion") or 2
            )
            etree.SubElement(gvehtras, "dTipIdenVeh").text = str(tip_id_veh)
            etree.SubElement(gvehtras, "dNroMatVeh").text = (
                str(
                    veh.get("numeroIden")
                    or veh.get("matricula")
                    or veh.get("chasis")
                    or "000000"
                )
                .strip()
                .upper()
            )

            # Color y número de registro (opcionales)
            if veh.get("color"):
                etree.SubElement(gvehtras, "dColVeh").text = str(veh.get("color"))[:20]
            if veh.get("numeroRegistro"):
                etree.SubElement(gvehtras, "dNroRegVeh").text = str(
                    veh.get("numeroRegistro")
                )[:20]

        # gCamTrans (Datos Transportista) - Obligatorio para NRE
        # Siempre generar aunque sea transporte propio
        gcamtrans = etree.SubElement(gtransp, "gCamTrans")
        tr_info = trans.get("transportista") or {}

        # E981 - Naturaleza del transportista (1=Contribuyente, 2=No contribuyente)
        nat_trans = int(tr_info.get("tipo") or trans.get("naturaleza") or 1)
        etree.SubElement(gcamtrans, "iNatTrans").text = str(nat_trans)

        # E982 - Nombre o razón social
        etree.SubElement(gcamtrans, "dNomTrans").text = str(
            tr_info.get("nombreTr") or trans.get("razonSocial") or "Transportista"
        )

        # Si es contribuyente (E981=1), incluir RUC
        if nat_trans == 1:
            ruc_tr = tr_info.get("rucTr") or tr_info.get("numeroTr") or trans.get("ruc")
            if ruc_tr:
                if "-" in str(ruc_tr):
                    etree.SubElement(gcamtrans, "dRucTrans").text = str(ruc_tr).split(
                        "-"
                    )[0]
                    etree.SubElement(gcamtrans, "dDVTrans").text = str(ruc_tr).split(
                        "-"
                    )[1]
                else:
                    etree.SubElement(gcamtrans, "dRucTrans").text = str(ruc_tr)
                    etree.SubElement(gcamtrans, "dDVTrans").text = str(
                        tr_info.get("dvTr") or "0"
                    )

            nac_trans = (
                tr_info.get("nacionalidad") or trans.get("nacionalidad") or "PRY"
            )
            etree.SubElement(gcamtrans, "cNacTrans").text = str(nac_trans)
            etree.SubElement(gcamtrans, "dDesNacTrans").text = descripcion_pais(
                str(nac_trans)
            )
        else:
            # E985 - Tipo de documento de identidad (para no contribuyente)
            tipo_id = tr_info.get("tipoDocumentoTr") or tr_info.get("tipoId")
            if tipo_id is not None:
                etree.SubElement(gcamtrans, "iTipIDTrans").text = str(tipo_id)
                etree.SubElement(
                    gcamtrans, "dDTipIDTrans"
                ).text = descripcion_tipo_documento_identidad_receptor(tipo_id)

                # E987 - Número de documento de identidad
                num_id = (
                    tr_info.get("numeroTr")
                    or tr_info.get("numeroId")
                    or tr_info.get("numeroIdTrans")
                )
                if num_id:
                    etree.SubElement(gcamtrans, "dNumIDTrans").text = str(num_id)

            # E988 - Nacionalidad
            nac_trans = (
                tr_info.get("nacionalidad") or trans.get("nacionalidad") or "PRY"
            )
            etree.SubElement(gcamtrans, "cNacTrans").text = str(nac_trans)
            etree.SubElement(gcamtrans, "dDesNacTrans").text = descripcion_pais(
                str(nac_trans)
            )

        # E990 y E991 - Datos del chofer (obligatorios)
        chof_name = tr_info.get("nombreCh")
        chof_id = tr_info.get("numeroCh")
        chof_dir = tr_info.get("direccionCh")

        chof_obj = trans.get("chofer")
        if not chof_name and chof_obj:
            chof_name = chof_obj.get("nombre")
            chof_id = chof_obj.get("documentoNumero") or chof_obj.get("id")
            chof_dir = chof_obj.get("direccion")

        if chof_id:
            etree.SubElement(gcamtrans, "dNumIDChof").text = str(chof_id)
        if chof_name:
            etree.SubElement(gcamtrans, "dNomChof").text = str(chof_name)

        # E992 - Domicilio fiscal del transportista
        dir_trans = (
            tr_info.get("direccionTr")
            or trans.get("domicilio")
            or trans.get("direccion")
            or "S/N"
        )
        etree.SubElement(gcamtrans, "dDomFisc").text = str(dir_trans)

        # E993 - Dirección del chofer
        if chof_dir:
            etree.SubElement(gcamtrans, "dDirChof").text = str(chof_dir)

    # Totales (no aplica si tipoDocumento == 7)
    if data.get("tipoDocumento") != 7:
        build_gtotsub(parent=de, data=data, items_summary=items_summary, config=config)

    # gCamDEAsoc (documento asociado)
    doc_aso = data.get("documentoAsociado") or None
    if doc_aso:
        gdoc = etree.SubElement(de, "gCamDEAsoc")
        tip = doc_aso.get("tipoDocumentoAsoc") or doc_aso.get("tipo")
        if tip is None:
            tip = 1
        tip_i = int(tip)
        etree.SubElement(gdoc, "iTipDocAso").text = str(tip_i)
        etree.SubElement(gdoc, "dDesTipDocAso").text = descripcion_tipo_doc_asociado(
            tip_i
        )
        cdc_aso = doc_aso.get("cdcAsociado") or doc_aso.get("cdc")
        if cdc_aso:
            etree.SubElement(gdoc, "dCdCDERef").text = str(cdc_aso).strip()

        nro = (doc_aso.get("numero") or "").strip()
        if nro:
            parts = [p.strip() for p in nro.split("-") if p.strip()]
            # Solo agregar establecimiento y punto si el tipo de documento asociado es 3 o 4 (Factura o similar)
            if len(parts) >= 3 and tip_i in (
                3,
                4,
            ):  # Solo para tipos que requieren número
                etree.SubElement(gdoc, "dEstDocAso").text = left_zero(parts[0], 3)
                etree.SubElement(gdoc, "dPExpDocAso").text = left_zero(parts[1], 3)
                etree.SubElement(gdoc, "dNumDocAso").text = left_zero(parts[2], 7)

    # Signature (Placeholder required for XSD before gCamFuFD)
    # En producción esto lo hace el firmante, pero para estructura DE:
    sig = etree.SubElement(root, "{http://www.w3.org/2000/09/xmldsig#}Signature")

    # gCamFuFD (QR data)
    qr_data = data.get("qrData") or "urlSifen"
    gcamfufd = etree.SubElement(root, "gCamFuFD")
    etree.SubElement(gcamfufd, "dCarQR").text = str(qr_data)

    xml_bytes = etree.tostring(
        root,
        xml_declaration=True,
        encoding="UTF-8",
        standalone=True,
    )
    return normalize_xml(xml_bytes.decode("utf-8"))
