"""Generación de ítems (gCamItem) y cálculo de IVA por ítem.

Este módulo construye el bloque `gDtipDE/gCamItem` a partir de `data["items"]`.
Incluye el cálculo del IVA por ítem según NT13 para `ivaTipo=1` (gravado) y
`ivaTipo=4` (gravado parcial).
"""

from __future__ import annotations

from typing import Any, List, Optional

from lxml import etree

from .config import XmlgenConfig
from .catalogs import descripcion_pais, descripcion_unidad_medida


def _descripcion_afectacion_iva(i_afec_iva: int) -> str:
    """Devuelve la descripción del código de afectación IVA (E731).

    Catálogo mínimo para mantener el XML legible sin depender aún del catálogo completo.
    """
    if i_afec_iva == 1:
        return "Gravado IVA"
    if i_afec_iva == 2:
        return "Exonerado (Art. 100 - Ley 6380/2019)"
    if i_afec_iva == 3:
        return "Exento"
    if i_afec_iva == 4:
        return "Gravado parcial (Grav- Exento)"
    return ""


def _descripcion_unidad_medida(item: dict) -> Optional[str]:
    """Devuelve la descripción de unidad de medida.

    - Si viene `unidadMedidaDescripcion` en el item, se respeta.
    - Si no, se intenta inferir desde un catálogo mínimo.
    """
    # Mientras no migremos catálogos completos, permitimos que el usuario provea la descripción.
    desc = item.get("unidadMedidaDescripcion")
    if desc is not None:
        return desc
    if item.get("unidadMedida") is None:
        return None
    return descripcion_unidad_medida(int(item.get("unidadMedida")))


def build_gcamitem_nodes(parent: etree._Element, params: dict, data: dict, config: XmlgenConfig) -> List[dict[str, Any]]:
    """Construye nodos `gCamItem` y retorna un resumen numérico por ítem.

    El resumen retornado es consumido por `totals.build_gtotsub()` para calcular totales.
    Campos del resumen:
    - `d_tot_ope_item` (EA008 / dTotOpeItem)
    - `iva_tipo`, `iva_tasa`
    - `d_bas_grav_iva` (E735), `d_liq_iva_item` (E736), `d_bas_exe` (E737)
    """
    items_out: List[dict[str, Any]] = []

    items_in = data.get("items") or []

    total_general = 0.0
    for it in items_in:
        total_general += float(it.get("cantidad") or 0) * float(it.get("precioUnitario") or 0)

    moneda = data.get("moneda") or ("PYG" if config.default_values else None)

    for item in items_in:
        gcam = etree.SubElement(parent, "gCamItem")

        etree.SubElement(gcam, "dCodInt").text = str(item.get("codigo", ""))
        etree.SubElement(gcam, "dDesProSer").text = str(item.get("descripcion", ""))

        if item.get("unidadMedida") is not None:
            etree.SubElement(gcam, "cUniMed").text = str(int(item.get("unidadMedida")))

        uni_desc = _descripcion_unidad_medida(item)
        if uni_desc is not None:
            etree.SubElement(gcam, "dDesUniMed").text = str(uni_desc)

        cantidad = item.get("cantidad", 0)
        etree.SubElement(gcam, "dCantProSer").text = str(cantidad)

        if item.get("pais") is not None:
            etree.SubElement(gcam, "cPaisOrig").text = str(item.get("pais"))
        pais_desc = item.get("paisDescripcion")
        if pais_desc is None and item.get("pais") is not None:
            pais_desc = descripcion_pais(str(item.get("pais")))
        if pais_desc:
            etree.SubElement(gcam, "dDesPaisOrig").text = str(pais_desc)

        precio_unit = float(item.get("precioUnitario") or 0)
        tot_bru = precio_unit * float(cantidad)
        d_tot_ope_item = tot_bru

        # gValorItem
        if data.get("tipoDocumento") != 7:
            gvaloritem = etree.SubElement(gcam, "gValorItem")

            etree.SubElement(gvaloritem, "dPUniProSer").text = _fmt_money(precio_unit, data, config)
            etree.SubElement(gvaloritem, "dTotBruOpeItem").text = _fmt_money(tot_bru, data, config)

            gresta = etree.SubElement(gvaloritem, "gValorRestaItem")

            # EA002 descuento particular
            d_desc_item = 0.0
            if item.get("descuento") is not None and float(item.get("descuento") or 0) > 0:
                # TS: respeta el valor enviado por el usuario
                d_desc_item = float(item.get("descuento") or 0)

            # EA004 descuento global prorrateado (unitario)
            d_desc_glo_item = 0.0
            if data.get("descuentoGlobal") is not None and float(data.get("descuentoGlobal") or 0) > 0 and total_general > 0:
                subtotal = float(cantidad) * precio_unit
                peso_porc = (100.0 * subtotal) / total_general
                descuento_global_aplicado = (float(data.get("descuentoGlobal")) * peso_porc) / 100.0
                d_desc_glo_item = descuento_global_aplicado / float(cantidad)

            # EA006 anticipo particular
            d_ant_pre_uni_it = 0.0
            if item.get("anticipo") is not None and float(item.get("anticipo") or 0) > 0:
                d_ant_pre_uni_it = float(item.get("anticipo") or 0)

            # EA007 anticipo global prorrateado (unitario)
            d_ant_glo_pre_uni_it = 0.0
            if data.get("anticipoGlobal") is not None and float(data.get("anticipoGlobal") or 0) > 0 and total_general > 0:
                subtotal = float(cantidad) * precio_unit
                peso_porc = (100.0 * subtotal) / total_general
                anticipo_global_aplicado = (float(data.get("anticipoGlobal")) * peso_porc) / 100.0
                d_ant_glo_pre_uni_it = anticipo_global_aplicado / float(cantidad)

            # EA008 dTotOpeItem
            precio_unit_neto = (
                precio_unit
                - float(d_desc_item or 0)
                - float(d_desc_glo_item or 0)
                - float(d_ant_pre_uni_it or 0)
                - float(d_ant_glo_pre_uni_it or 0)
            )
            d_tot_ope_item = precio_unit_neto * float(cantidad)

            if config.sum0_000001_suffix_before_to_fixed:
                d_tot_ope_item += 0.000001

            if float(d_desc_glo_item or 0) == 0.0:
                # TS: sin descuento global => usa redondeo normal
                if (data.get("moneda") or ("PYG" if config.default_values else None)) == "PYG":
                    d_tot_ope_item = float(f"{d_tot_ope_item:.{config.pyg_decimals}f}")
                else:
                    d_tot_ope_item = float(f"{d_tot_ope_item:.{config.decimals}f}")
            else:
                # TS: con descuento global => 8 decimales para exactitud
                d_tot_ope_item = float(f"{d_tot_ope_item:.8f}")

            etree.SubElement(gresta, "dDescItem").text = _fmt_money(d_desc_item, data, config)
            # TS: dDescGloItem se guarda con 8 decimales (máximo permitido).
            # Para compatibilidad con validadores estrictos, si es 0 emitimos "0".
            if float(d_desc_glo_item or 0) == 0.0:
                etree.SubElement(gresta, "dDescGloItem").text = "0"
            else:
                etree.SubElement(gresta, "dDescGloItem").text = _fmt_fixed(d_desc_glo_item, 8)
            etree.SubElement(gresta, "dAntPreUniIt").text = _fmt_money(d_ant_pre_uni_it, data, config)
            # TS: para PYG lo fuerza a pygDecimals
            if (data.get("moneda") or ("PYG" if config.default_values else None)) == "PYG":
                etree.SubElement(gresta, "dAntGloPreUniIt").text = _fmt_fixed(d_ant_glo_pre_uni_it, config.pyg_decimals)
            else:
                etree.SubElement(gresta, "dAntGloPreUniIt").text = _fmt_fixed(d_ant_glo_pre_uni_it, 8)
            etree.SubElement(gresta, "dTotOpeItem").text = _fmt_money(d_tot_ope_item, data, config, partial=False)

        # gCamIVA
        if data.get("tipoImpuesto") in (1, 3, 4, 5) and data.get("tipoDocumento") not in (4, 7):
            iva_tipo = int(item.get("ivaTipo") or 0)
            iva_prop = item.get("ivaBase")
            if iva_prop is None:
                iva_prop = item.get("ivaProporcion")
            if iva_prop is None:
                iva_prop = 0

            iva_tasa = float(item.get("iva") or 0)

            giva = etree.SubElement(gcam, "gCamIVA")
            etree.SubElement(giva, "iAfecIVA").text = str(iva_tipo)
            etree.SubElement(giva, "dDesAfecIVA").text = _descripcion_afectacion_iva(iva_tipo)
            etree.SubElement(giva, "dPropIVA").text = str(iva_prop)
            etree.SubElement(giva, "dTasaIVA").text = _fmt_tax_rate(iva_tasa)

            # Para IVA, EA008 es el neto de operación por ítem.
            d_tot_ope_item = float(locals().get("d_tot_ope_item", tot_bru))

            d_bas_grav_iva = 0.0
            d_liq_iva_item = 0.0
            d_bas_exe = 0.0

            # NT13 vigente: aplica para test y producción luego del 2023-06-17.
            # Fórmulas oficiales SIFEN:
            # E735 dBasGravIVA = [100 * EA008 * E733] / [10000 + (E734 * E733)]
            # E736 dLiqIVAItem = E735 * (E734 / 100)  -- o por diferencia para PYG
            # E737 dBasExe (solo si iAfecIVA=4) = [100 * EA008 * (100 - E733)] / [10000 + (E734 * E733)]
            if iva_tipo in (1, 4):
                iva_prop_f = float(iva_prop)
                denom = 10000.0 + (iva_tasa * iva_prop_f)

                if denom != 0:
                    # Calcular base gravada con fórmula NT13
                    d_bas_grav_iva = (100.0 * d_tot_ope_item * iva_prop_f) / denom

                    # Para PYG: redondear a entero y calcular IVA por diferencia
                    # Para otras monedas: usar partialTaxDecimals y calcular IVA por multiplicación
                    if moneda == "PYG" and config.pyg_tax_decimals == 0:
                        d_bas_grav_iva = float(round(d_bas_grav_iva))
                        # IVA por diferencia (evita errores de redondeo)
                        d_liq_iva_item = float(d_tot_ope_item - d_bas_grav_iva)
                    else:
                        d_bas_grav_iva = float(f"{d_bas_grav_iva:.{config.partial_tax_decimals}f}")
                        d_liq_iva_item = (d_bas_grav_iva * iva_tasa) / 100.0
                        d_liq_iva_item = float(f"{d_liq_iva_item:.{config.partial_tax_decimals}f}")

                    # Base exenta para gravado parcial (iAfecIVA=4)
                    if iva_tipo == 4:
                        d_bas_exe = (100.0 * d_tot_ope_item * (100.0 - iva_prop_f)) / denom
                        if moneda == "PYG" and config.pyg_tax_decimals == 0:
                            d_bas_exe = float(round(d_bas_exe))
                        else:
                            d_bas_exe = float(f"{d_bas_exe:.{config.partial_tax_decimals}f}")

            etree.SubElement(giva, "dBasGravIVA").text = _fmt_money(d_bas_grav_iva, data, config, partial=True)
            etree.SubElement(giva, "dLiqIVAItem").text = _fmt_money(d_liq_iva_item, data, config, partial=True)
            etree.SubElement(giva, "dBasExe").text = _fmt_money(d_bas_exe, data, config, partial=True)

        items_out.append(
            {
                "cantidad": float(cantidad),
                "precio_unitario": float(item.get("precioUnitario") or 0),
                "d_tot_ope_item": float(locals().get("d_tot_ope_item", tot_bru)),
                "d_tot_ope_sin_desc": float(tot_bru),
                "iva_tipo": int(item.get("ivaTipo") or 0),
                "iva_tasa": float(item.get("iva") or 0),
                "d_bas_grav_iva": float(locals().get("d_bas_grav_iva", 0.0)),
                "d_liq_iva_item": float(locals().get("d_liq_iva_item", 0.0)),
                "d_bas_exe": float(locals().get("d_bas_exe", 0.0)),
                "d_desc_item": float(locals().get("d_desc_item", 0.0)),
                "d_desc_glo_item": float(locals().get("d_desc_glo_item", 0.0)),
                "d_ant_pre_uni_it": float(locals().get("d_ant_pre_uni_it", 0.0)),
                "d_ant_glo_pre_uni_it": float(locals().get("d_ant_glo_pre_uni_it", 0.0)),
            }
        )

    return items_out


def _fmt_tax_rate(value: float) -> str:
    """Formatea la tasa de IVA (5/10/0) evitando decimales cuando es entero."""
    if value.is_integer():
        return str(int(value))
    return str(value)


def _fmt_money(value: float, data: dict, config: XmlgenConfig, partial: bool = False) -> str:
    """Formatea valores monetarios.

    - Para `partial=True`, usa `partial_tax_decimals` y quita ceros finales para imitar
      el comportamiento TS (`parseFloat(toFixed(n))`).
    - Para moneda `PYG` usa `pyg_decimals`.
    """
    moneda = data.get("moneda") or ("PYG" if config.default_values else None)
    if partial:
        decimals = config.partial_tax_decimals
    else:
        if moneda == "PYG":
            decimals = config.pyg_decimals
        else:
            decimals = config.decimals

    if decimals == 0:
        return str(int(round(value)))

    # TS hace parseFloat(toFixed(n)), que quita ceros finales.
    s = f"{float(value):.{decimals}f}"
    if partial:
        s = s.rstrip("0").rstrip(".")
        if s == "":
            s = "0"
    return s


def _fmt_fixed(value: float, decimals: int) -> str:
    """Formatea con `decimals` decimales (similar a `toFixed` de TS).

    Se usa para campos donde el TS utiliza explícitamente `toFixed(8)` y **no** aplica
    `parseFloat()` inmediatamente.
    """
    return f"{float(value):.{decimals}f}"
