"""
Validaciones CSV para Encabezados de Facturas de Compras Mexicanas.
Paridad Clarion: VALIDA_TODA_FAC_COM_MEX, VALIDA_PARCIAL_FAC_COM_MEX, VALIDACIONES_FAC_COM_MEX.
Sin pedimento, remesa, agente aduanal ni aduana de cruce.
Estructura CSV: NUMERO FACTURA (C), FECHA FACTURA (D), TIPO DE CAMBIO (E), CLAVE PROVEEDOR (G), ...
"""
from datetime import datetime
from decimal import Decimal
from typing import Any, Dict, List, Optional, Set

from .encabezados_impo_temp import (
    MAX_LEN_FACTURA,
    TIPO_PESO_VALIDOS,
    TIPOS_MONEDA_VALIDOS,
    _clip,
    _err,
    _get,
    _parse_decimal,
    _parse_int,
    _validaciones_factura_longitud,
    _validaciones_moneda,
    _validaciones_tipo_cambio,
    _validaciones_tipo_peso,
)

# Clarion Col M: Compras Mexicanas incluye "FERRO BARCAZA" (con espacio) y "PLATAFORMA"
TIPO_TRANSPORTE_VALIDOS_CMEX = frozenset({
    "NINGUNO", "TRANSPORTE", "CAJA", "PLACAS", "CAMION", "BUQUE",
    "FERROBARCAZA", "FERRO BARCAZA", "CONTENEDOR", "PLATAFORMA", "AVION",
})


def _validaciones_transporte_cmex(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Tipo transporte (Col M) y número (Col N). Acepta FERRO BARCAZA y FERROBARCAZA."""
    m_raw = _get(row, "TIPO TRANSPORTE")
    m = m_raw.upper().replace(" ", "") if m_raw else ""
    m_with_space = m_raw.upper() if m_raw else ""
    n = _get(row, "NUMERO TRANSPORTE")
    if m_raw and m_with_space not in TIPO_TRANSPORTE_VALIDOS_CMEX and m not in TIPO_TRANSPORTE_VALIDOS_CMEX:
        return _err(
            line_num,
            "TIPO TRANSPORTE",
            "Error: (Celda M{}) El Tipo de Transporte: {} no es válido. "
            "Válidos: NINGUNO, TRANSPORTE, CAJA, PLACAS, CAMION, BUQUE, FERRO BARCAZA, CONTENEDOR, PLATAFORMA, AVION.".format(
                line_num, m_raw
            ),
        )
    if not m_raw and n:
        return _err(
            line_num,
            "NUMERO TRANSPORTE",
            "Error: (Celda N{}) El Tipo de Transporte está vacío y está capturado un número de transporte.".format(line_num),
        )
    return None


def _validaciones_obligatorios_toda_cmex(
    row: Dict[str, Any],
    line_num: int,
    actualizar: bool,
) -> Optional[Dict[str, Any]]:
    """Obligatorios VALIDA_TODA_FAC_COM_MEX: C siempre; si no es actualizar, también D, G, H, I."""
    obligatorios: List[str] = []
    if not _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA"):
        obligatorios.append("(Col.C) Factura")
    if not actualizar:
        if not _get(row, "FECHA FACTURA", "FECHA"):
            obligatorios.append("(Col.D) Fecha de la Factura")
        if not _get(row, "CLAVE PROVEEDOR"):
            obligatorios.append("(Col.G) Clave del Proveedor")
        if not _get(row, "CLAVE VENDIDO A"):
            obligatorios.append("(Col.H) Clave del Vendido A")
        if not _get(row, "CLAVE ENVIADO A"):
            obligatorios.append("(Col.I) Clave del Enviado A")
    if obligatorios:
        return _err(
            line_num,
            "ARCHIVO CSV",
            "Existen campos vacíos que son obligatorios: {}. Revisar la línea del archivo y capturar los campos con la información correcta.".format(
                ", ".join(obligatorios)
            ),
        )
    return None


def _validaciones_catalogos_cmex(
    row: Dict[str, Any],
    line_num: int,
    valid_provider_ids: Set[int],
    valid_sold_to_ids: Set[int],
    valid_shipped_to_ids: Set[int],
    valid_provider_short_names: Set[str],
    valid_sold_to_short_names: Set[str],
    valid_shipped_to_short_names: Set[str],
    valid_transporter_keys: Set[str],
    valid_incoterms: Set[str],
) -> Optional[Dict[str, Any]]:
    """Catálogos para Compras Mexicanas: Proveedor, Vendido A, Enviado A, Transportista, Incoterm (sin Agente Aduanal ni Aduana)."""

    def check_id_or_rfc(
        val: Any,
        col: str,
        catalog_name: str,
        valid_ids: Set[int],
        valid_short_names: Set[str],
    ) -> Optional[Dict[str, Any]]:
        if val is None or str(val).strip() == "":
            return None
        v = _parse_int(val)
        if v is not None:
            if valid_ids and v not in valid_ids:
                return _err(line_num, col, "Error: La clave en {} no existe en el Catálogo de {}.".format(col, catalog_name))
            return None
        sn_norm = str(val).strip().upper()
        if valid_short_names and sn_norm not in valid_short_names:
            return _err(line_num, col, "Error: La clave/corta en {} no existe en el Catálogo de {}.".format(col, catalog_name))
        if not valid_short_names:
            return _err(line_num, col, "Error: (Celda) {} debe ser un número entero o clave corta (short name) válida.".format(col))
        return None

    err = check_id_or_rfc(
        row.get("CLAVE PROVEEDOR"), "CLAVE PROVEEDOR", "Clientes/Proveedores",
        valid_provider_ids, valid_provider_short_names,
    )
    if err:
        return err
    err = check_id_or_rfc(
        row.get("CLAVE VENDIDO A"), "CLAVE VENDIDO A", "Clientes/Proveedores",
        valid_sold_to_ids, valid_sold_to_short_names,
    )
    if err:
        return err
    err = check_id_or_rfc(
        row.get("CLAVE ENVIADO A"), "CLAVE ENVIADO A", "Clientes/Proveedores",
        valid_shipped_to_ids, valid_shipped_to_short_names,
    )
    if err:
        return err

    k = _get(row, "CLAVE TRANSPORTISTA")
    if k and valid_transporter_keys and k.upper() not in valid_transporter_keys:
        return _err(
            line_num,
            "CLAVE TRANSPORTISTA",
            "Error: (Celda K{}) La Clave del Transportista: {} no existe en el Catálogo de Transportistas.".format(line_num, k),
        )

    v = _get(row, "CLAVE INCOTERM")
    if v and valid_incoterms and v.upper() not in valid_incoterms:
        return _err(
            line_num,
            "CLAVE INCOTERM",
            "Error: (Celda V{}) La Clave de INCOTERM: {} no existe en el Catálogo de INCOTERMS.".format(line_num, v),
        )

    return None


def validate_row_encabezados_cmex(
    row: Dict[str, Any],
    line_num: int,
    actualizar: bool,
    invoice_exists_by_number: Dict[str, bool],
    invoice_processed_by_number: Dict[str, bool],
    valid_provider_ids: Set[int],
    valid_sold_to_ids: Set[int],
    valid_shipped_to_ids: Set[int],
    valid_transporter_keys: Set[str],
    valid_incoterms: Set[str],
    valid_currency_codes: Set[str],
    exchange_rate_by_date: Optional[Dict[str, Any]] = None,
    invoice_has_partidas_by_number: Optional[Dict[str, bool]] = None,
    existing_tipo_moneda_by_number: Optional[Dict[str, str]] = None,
    valid_provider_short_names: Optional[Set[str]] = None,
    valid_sold_to_short_names: Optional[Set[str]] = None,
    valid_shipped_to_short_names: Optional[Set[str]] = None,
    date_format: Optional[str] = None,
    parse_date_fn=None,
    warnings: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
    """
    Valida una fila de CSV de Encabezados de Compras Mexicanas.
    Clarion: VALIDA_TODA_FAC_COM_MEX (factura nueva o no actualizar) vs VALIDA_PARCIAL_FAC_COM_MEX (actualizar existente).
    Siempre ejecuta VALIDACIONES_FAC_COM_MEX (longitud C, tipo cambio, catálogos G/H/I/K, transporte M/N, moneda O/P, incoterm V, tipo peso Y).
    """
    errors: List[Dict[str, Any]] = []

    factura = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if not factura:
        errors.append(_err(
            line_num,
            "NUMERO FACTURA",
            "Error: (Col.C) La columna de Número de Factura está vacía y no se pueden hacer las validaciones.",
        ))
        return errors

    if invoice_processed_by_number.get(factura.strip(), False):
        errors.append(_err(
            line_num,
            "NUMERO FACTURA",
            "Error: (Celda C{}) El Número de Factura: {} ya existe y está Actualizada, no se puede hacer cambios.".format(
                line_num, factura
            ),
        ))
        return errors

    if actualizar and factura.strip() not in invoice_exists_by_number:
        errors.append(_err(
            line_num,
            "NUMERO FACTURA",
            "Error: (Col.C) Factura de importación {} no existe (modo Actualizar).".format(factura),
        ))
        return errors

    use_partial = actualizar and invoice_exists_by_number.get(factura.strip(), False)

    if not use_partial:
        err = _validaciones_obligatorios_toda_cmex(row, line_num, actualizar)
        if err:
            errors.append(err)

    has_partidas = invoice_has_partidas_by_number.get(factura.strip(), False) if invoice_has_partidas_by_number else False
    existing_moneda = existing_tipo_moneda_by_number.get(factura.strip()) if existing_tipo_moneda_by_number else None

    invoice_date_parsed = None
    if parse_date_fn:
        date_str = _get(row, "FECHA FACTURA", "FECHA")
        if date_str:
            invoice_date_parsed = parse_date_fn(date_str, date_format)

    for err in [
        _validaciones_factura_longitud(row, line_num),
        _validaciones_transporte_cmex(row, line_num),
        _validaciones_moneda(
            row,
            line_num,
            valid_currency_codes or set(),
            has_partidas if use_partial else None,
            existing_moneda if use_partial else None,
        ),
        _validaciones_tipo_peso(row, line_num),
        _validaciones_catalogos_cmex(
            row,
            line_num,
            valid_provider_ids or set(),
            valid_sold_to_ids or set(),
            valid_shipped_to_ids or set(),
            valid_provider_short_names or set(),
            valid_sold_to_short_names or set(),
            valid_shipped_to_short_names or set(),
            valid_transporter_keys or set(),
            valid_incoterms or set(),
        ),
        _validaciones_tipo_cambio(
            row, line_num, invoice_date_parsed, exchange_rate_by_date or {}, warnings
        ),
    ]:
        if err:
            errors.append(err)

    return errors
