"""
Validaciones CSV para Encabezados de Facturas de Importación Temporal.
Paridad Clarion: VALIDA_TODA_FACIMPO_TEM, VALIDA_PARCIAL_FACIMPO_TEM, VALIDACIONES_FACIMPO_TEM.
Mapeo a BD en commit: LLENA_FACIMPO_TEM (tasks.insert_valid_rows).
Estructura CSV: PEDIMENTO (A), REMESA (B), NUMERO FACTURA (C), ... ADUANA DE CRUCE (AB), OBSERVACIONES E/I, FACTURA ALTERNA.
"""
import logging
from datetime import datetime
from decimal import Decimal, InvalidOperation
from typing import Any, Dict, List, Optional, Set, Tuple
from .pedimento_resolution import (
    aduana_cruce_matches_catalog,
    canonical_customs_office_for_pedimento_key,
    normalize_pedimento_number_str,
    resolve_pedimento_candidates,
)
from .consolidated_invoice_dates import validate_consolidated_invoice_date_csv

logger = logging.getLogger(__name__)

# Longitudes máximas Clarion
MAX_LEN_PEDIMENTO = 18  # CC-LLLL-NNNNNNN o CCC-LLLL-NNNNNNN (sin año; ej. 01-1234-2312412 o 640-1234-2312412)
MAX_LEN_FACTURA = 15

# Formato PEDIMENTO: CC-LLLL-NNNNNNN (sin año: aduana 2-3, patente 4, número 7)

REGIMENES_VALIDOS = frozenset({"ITE", "ITR"})
TIPOS_MONEDA_VALIDOS = frozenset({"ME", "MN", "MC"})
TIPO_PESO_VALIDOS = frozenset({"KILOS", "LIBRAS"})


def csv_tipo_moneda_es_me_mn_mc(value: Any) -> bool:
    """
    ME / MN / MC con la misma regla que _validaciones_moneda (strip + upper).
    Compartido entre scan (encabezados) y commit / validate_row_strict en tasks.
    """
    if value is None or not str(value).strip():
        return False
    return str(value).strip().upper() in TIPOS_MONEDA_VALIDOS

# Clarion Col M → valor normalizado (minúscula para TransportType enum)
TIPO_TRANSPORTE_CLARION_TO_NORM = {
    "NINGUNO": "none",
    "TRANSPORTE": "transport",
    "CAJA": "box",
    "PLACAS": "licence plates",
    "CAMION": "truck",
    "BUQUE": "vessel",
    "FERROBARCAZA": "rail_barge",
    "CONTENEDOR": "container",
    "PLATAFORMA": "flatbed",
    "GONDOLA": "gondola",
    "AVION": "airplane",
}
TIPO_TRANSPORTE_VALIDOS = frozenset(TIPO_TRANSPORTE_CLARION_TO_NORM.keys())


def _clip(val: Any) -> str:
    if val is None:
        return ""
    return str(val).strip()


def _get(row: Dict[str, Any], *keys: str) -> str:
    for k in keys:
        v = row.get(k)
        if v is not None and str(v).strip():
            return _clip(v)
    return ""


def _parse_decimal(val: Any) -> Optional[Decimal]:
    if val is None:
        return None
    s = _clip(val)
    if not s:
        return None
    s = s.replace(",", "")
    try:
        return Decimal(s)
    except (InvalidOperation, ValueError):
        return None


def _parse_int(val: Any) -> Optional[int]:
    if val is None:
        return None
    s = _clip(val)
    if not s:
        return None
    try:
        return int(s)
    except ValueError:
        return None


def _sanitize_pedimento_csv_delimiters(raw: Any) -> str:
    """Guiones Unicode / BOM → ASCII (Excel a veces exporta caracteres distintos de '-')."""
    s = str(raw if raw is not None else "").strip().strip("\ufeff")
    for ch in ("\u2212", "\u2013", "\u2014"):
        s = s.replace(ch, "-")
    return s


def _digits_segment_from_csv_cell(seg: Any, *, max_digits: int, min_digits: int = 1) -> Optional[str]:
    """
    Segmento numérico desde CSV: tolera celdas Excel como 3428.0, 7011747.0 o enteros.
    """
    raw = str(seg if seg is not None else "").strip().replace(",", "").replace(" ", "")
    if not raw:
        return None
    if raw.endswith(".0") and raw[:-2].isdigit():
        raw = raw[:-2]
    elif "." in raw or "e" in raw.lower():
        try:
            fv = float(raw)
            if fv < 0 or fv != int(fv):
                return None
            raw = str(int(fv))
        except (ValueError, OverflowError):
            return None
    if not raw.isdigit():
        return None
    if len(raw) > max_digits or len(raw) < min_digits:
        return None
    return raw


def parse_pedimento_col_a(pedimento_str: Any) -> Optional[Tuple[str, str, str]]:
    """
    Parsea Col A (PEDIMENTO) formato CC-LLLL-NNNNNNN o CCC-LLLL-NNNNNNN (sin año).
    El número puede llevar 1–7 dígitos; se normaliza a 7 con ceros a la izquierda.
    Tolera exportación Excel (ej. aduana 7 → 07, 3428.0, 7011747.0).
    Retorna (customs_office_2o3, license_4, pedimento_number_7) o None si formato inválido.
    """
    if pedimento_str is None or (isinstance(pedimento_str, str) and not pedimento_str.strip()):
        return None
    s = _sanitize_pedimento_csv_delimiters(pedimento_str)
    parts = s.split("-")
    if len(parts) != 3:
        return None

    customs_raw = _digits_segment_from_csv_cell(parts[0], max_digits=3, min_digits=1)
    if not customs_raw:
        return None
    if len(customs_raw) == 1:
        customs_office = customs_raw.zfill(2)
    else:
        customs_office = customs_raw
    if len(customs_office) not in (2, 3):
        return None

    lic_raw = _digits_segment_from_csv_cell(parts[1], max_digits=4, min_digits=1)
    if not lic_raw:
        return None
    license_val = lic_raw.zfill(4)

    num_raw = _digits_segment_from_csv_cell(parts[2], max_digits=7, min_digits=1)
    if not num_raw:
        return None
    pedimento_number = normalize_pedimento_number_str(num_raw)
    return (customs_office, license_val, pedimento_number)


def _pedimento_key_from_parsed(customs_office: str, license_val: str, pedimento_number: str) -> str:
    """Clave para lookup: CC-LLLL-NNNNNNN (aduana canónica 2 dígitos, número 7 dígitos)."""
    co = canonical_customs_office_for_pedimento_key(customs_office)
    num = normalize_pedimento_number_str(pedimento_number)
    lic = (license_val or "").strip()
    return f"{co}-{lic}-{num}"


def _patente_from_agente_aduanal(row: Dict[str, Any], default_license: str) -> str:
    """
    Obtiene patente para lookup desde AGENTE ADUANAL (columna específica).
    Si no es numérica o viene vacía, usa la patente parseada del PEDIMENTO.
    """
    agente = _get(row, "AGENTE ADUANAL")
    if agente and agente.isdigit():
        if len(agente) < 4:
            return agente.zfill(4)
        if len(agente) > 4:
            return agente[-4:]
        return agente
    return (default_license or "").strip()


def _err(line_num: int, col: str, msg: str, not_found_reason: Optional[str] = None) -> Dict[str, Any]:
    out: Dict[str, Any] = {"line": line_num, "col": col, "msg": msg}
    if not_found_reason:
        out["not_found_reason"] = not_found_reason
    return out


# --- Obligatorios VALIDA_TODA (cuando no es actualizar) ---
def _validaciones_obligatorios_toda(
    row: Dict[str, Any],
    line_num: int,
    tiene_pedimento: bool,
) -> Optional[Dict[str, Any]]:
    """Obligatorios: C, D, F, G, H, I, J; AB obligatoria si A tiene valor."""
    obligatorios: List[str] = []
    if not _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA"):
        obligatorios.append("(Col.C) Número de Factura")
    if not _get(row, "FECHA FACTURA", "FECHA"):
        obligatorios.append("(Col.D) Fecha de la Factura")
    if not _get(row, "REGIMEN", "CLAVEDOCUMENTO"):
        obligatorios.append("(Col.F) Clave Régimen")
    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 not _get(row, "AGENTE ADUANAL"):
        obligatorios.append("(Col.J) Clave del Agente Aduanal")
    if tiene_pedimento and not _get(row, "ADUANA DE CRUCE"):
        obligatorios.append("(Col.AB) Aduana de Cruce")
    if obligatorios:
        msg = f"Existen campos vacíos que son obligatorios: {', '.join(obligatorios)}."
        logger.warning(f"Line {line_num} rejected: {msg}")
        return _err(
            line_num,
            "ARCHIVO CSV",
            msg,
        )
    return None


# --- Pedimento y Remesa (Col A, B) ---
def _validaciones_pedimento_remesa(
    row: Dict[str, Any],
    line_num: int,
    pedimento_rows: List[Dict[str, Any]],
    remesa_por_pedimento_bd: Dict[str, Set[int]],
    remesa_por_pedimento_csv: Dict[str, Dict[int, str]],
    autonumerar_remesas: bool,
    control_remesa: bool,
    remesa_inicio: Optional[int],
    remesa_fin: Optional[int],
    date_format: Optional[str],
    invoice_date_parsed: Optional[datetime],
) -> Optional[Dict[str, Any]]:
    """Col A: formato, longitud, catálogo, tipo I, régimen ITE/ITR, match Col F, fechas. Col B: oblig si A, no 0, rango, unicidad."""
    col_a = _get(row, "PEDIMENTO")
    col_b_raw = row.get("REMESA")
    col_b = _clip(col_b_raw)
    col_f = _get(row, "REGIMEN", "CLAVEDOCUMENTO").upper()

    if not col_a:
        if col_b:
            return _err(
                line_num,
                "REMESA",
                f"Error: (Celda B{line_num}) Está asignado el número de Remesa y no se tiene un pedimento en (Celda A{line_num}).",
            )
        return None

    if len(col_a) > MAX_LEN_PEDIMENTO:
        return _err(
            line_num,
            "PEDIMENTO",
            f"Error: (Celda A{line_num}) El Pedimento: {col_a} supera la longitud de caracteres.",
        )
    parsed = parse_pedimento_col_a(col_a)
    if not parsed:
        return _err(
            line_num,
            "PEDIMENTO",
            f"Error: (Celda A{line_num}) El Formato del Pedimento: {col_a} es incorrecto. Use CC-LLLL-NNNNNNN (aduana 2–3 dígitos, patente 4, número 1–7 dígitos, sin año).",
        )

    customs_office, license_val, pedimento_number = parsed
    key = _pedimento_key_from_parsed(customs_office, license_val, pedimento_number)
    patente_lookup = _patente_from_agente_aduanal(row, license_val)
    ped_resolved = resolve_pedimento_candidates(
        customs_office=customs_office,
        license_val=license_val,
        pedimento_number=pedimento_number,
        patente_lookup=patente_lookup,
        pedimento_rows=pedimento_rows,
    )
    if ped_resolved["status"] == "not_found":
        if ped_resolved.get("not_found_reason") == "customs_mismatch":
            return _err(
                line_num,
                "PEDIMENTO",
                f"Error: (Celda A{line_num}) El pedimento {col_a} está en el catálogo pero la aduana no coincide con la registrada. "
                "Revise columna A (aduana en el pedimento) y Col.AB Aduana de cruce frente al alta del pedimento.",
                not_found_reason="customs_mismatch",
            )
        if ped_resolved.get("not_found_reason") == "license_or_patente_mismatch":
            return _err(
                line_num,
                "PEDIMENTO",
                f"Error: (Celda A{line_num}) El número de pedimento aparece en el catálogo pero la patente/agente no coincide "
                f"con el alta (clave {col_a}). Revise columna A y columna J (agente aduanal).",
                not_found_reason="license_or_patente_mismatch",
            )
        # no_catalog_match: paridad captura manual — pedimento aún no dado de alta; se permite continuar
        # si el régimen en columna F es ITE/ITR (validación local sin catálogo).
        if not col_f or col_f.upper() not in REGIMENES_VALIDOS:
            return _err(
                line_num,
                "REGIMEN",
                f"Error: (Celda F{line_num}) Cuando el pedimento no está en el catálogo, el Régimen (ITE o ITR) es obligatorio "
                "para poder importar la factura como pendiente de asignar pedimento.",
                not_found_reason="no_catalog_match",
            )
        ped_info = None
    elif ped_resolved["status"] == "ambiguous":
        return _err(
            line_num,
            "PEDIMENTO",
            f"Error: (Celda A{line_num}) El Número de Pedimento: {col_a} coincide con múltiples registros. "
            "Valide aduana/patente para identificar un único pedimento.",
        )
    else:
        ped_info = ped_resolved["pedimento"]

    if ped_info is not None:
        if (ped_info.get("operation_type") or "").upper() != "IMP":
            return _err(
                line_num,
                "PEDIMENTO",
                f"Error: (Celda A{line_num}) Este Número de Pedimento: {col_a} no está marcado como Importación Temporal.",
            )
        regimen_ped = (ped_info.get("regime") or "").strip().upper()
        if regimen_ped not in REGIMENES_VALIDOS:
            msg = f"Error: (Celda A{line_num}) El Pedimento: {col_a} tiene el Régimen {regimen_ped}, no válido (ITE o ITR)."
            logger.warning(f"Line {line_num} rejected: {msg}")
            return _err(
                line_num,
                "PEDIMENTO",
                msg,
            )
        if col_f and col_f not in REGIMENES_VALIDOS:
            pass
        elif col_f and col_f != regimen_ped:
            # Relax: allow ITE/ITR interchange for temporal imports
            if col_f in REGIMENES_VALIDOS and regimen_ped in REGIMENES_VALIDOS:
                pass
            else:
                return _err(
                    line_num,
                    "REGIMEN",
                    f"Error: (Celda F{line_num}) El Régimen Aduanero: {col_f} no coincide con el del Pedimento: {regimen_ped}.",
                )

        pedimento_type = (ped_info.get("pedimento_type") or "").strip().lower()
        err_consolidated = validate_consolidated_invoice_date_csv(
            line_num=line_num,
            pedimento_key_display=col_a,
            pedimento_type=pedimento_type,
            invoice_date_parsed=invoice_date_parsed,
            entry_raw=ped_info.get("entry_date"),
            end_raw=ped_info.get("end_date"),
            start_raw=ped_info.get("start_date"),
        )
        if err_consolidated:
            return err_consolidated

    if not autonumerar_remesas and not col_b:
        return _err(
            line_num,
            "REMESA",
            f"Error: (Celda B{line_num}) El Número de Remesa está vacío y se tiene un Pedimento en la Celda A{line_num}.",
        )
    remesa_int = _parse_int(col_b_raw)
    if col_b and remesa_int is not None and remesa_int == 0:
        return _err(
            line_num,
            "REMESA",
            f"Error: (Celda B{line_num}) El Número de Remesa no puede ser 0.",
        )
    if control_remesa and remesa_int is not None and remesa_inicio is not None and remesa_fin is not None:
        if remesa_int < remesa_inicio or remesa_int > remesa_fin:
            return _err(
                line_num,
                "REMESA",
                f"Error: (Celda B{line_num}) El Número de Remesa: {col_b} está fuera del rango configurado ({remesa_inicio}-{remesa_fin}).",
            )

    factura_actual = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if remesa_int is not None and key in remesa_por_pedimento_csv:
        other = remesa_por_pedimento_csv[key].get(remesa_int)
        if other and other != factura_actual:
            return _err(
                line_num,
                "REMESA",
                f"Error: (Celda B{line_num}) El Número de Remesa ya está asignado a la factura {other} en este archivo CSV.",
            )
    return None


# --- Factura longitud (Col C) ---
def _validaciones_factura_longitud(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    c = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if c and len(c) > MAX_LEN_FACTURA:
        return _err(line_num, "NUMERO FACTURA", f"Error: (Celda C{line_num}) El Número de Factura supera la longitud de caracteres.")
    return None


# --- Régimen (Col F) ---
def _validaciones_regimen(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    f = _get(row, "REGIMEN", "CLAVEDOCUMENTO")
    if f and f.upper() not in REGIMENES_VALIDOS:
        return _err(
            line_num,
            "REGIMEN",
            f"Error: (Celda F{line_num}) El Régimen Aduanero: {f} no es válido. Use ITE o ITR.",
        )
    return None


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


# --- Tipo moneda y clave moneda (Col O, P) ---
def _validaciones_moneda(
    row: Dict[str, Any],
    line_num: int,
    valid_currency_codes: Set[str],
    invoice_has_partidas: Optional[bool],
    existing_tipo_moneda: Optional[str],
) -> Optional[Dict[str, Any]]:
    o = _get(row, "TIPO MONEDA").upper()
    p = _get(row, "CLAVE MONEDA").upper()
    if o and o not in TIPOS_MONEDA_VALIDOS:
        return _err(
            line_num,
            "TIPO MONEDA",
            f"Error: (Celda O{line_num}) La opción de Tipo Moneda: {row.get('TIPO MONEDA')} no es válida. Use ME, MN o MC.",
        )
    if o == "MC":
        if not p:
            return _err(
                line_num,
                "CLAVE MONEDA",
                f"Error: (Celda P{line_num}) La Clave de la Moneda es obligatoria cuando Tipo de Moneda es MC.",
            )
        if p and valid_currency_codes and p not in valid_currency_codes:
            return _err(
                line_num,
                "CLAVE MONEDA",
                f"Error: (Celda P{line_num}) La Clave de la Moneda: {p} no existe en el Catálogo.",
            )
    if invoice_has_partidas and existing_tipo_moneda and o and o != existing_tipo_moneda.upper():
        return _err(
            line_num,
            "TIPO MONEDA",
            f"Error: (Celda O{line_num}) No se puede cambiar el Tipo de Moneda porque la factura ya tiene partidas. Use {existing_tipo_moneda}.",
        )
    return None


# --- Tipo peso (Col Y) ---
def _validaciones_tipo_peso(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    y = _get(row, "TIPO PESO").upper()
    if y and y not in TIPO_PESO_VALIDOS:
        return _err(
            line_num,
            "TIPO PESO",
            f"Error: (Celda Y{line_num}) La opción de Tipo de Peso: {row.get('TIPO PESO')} no es válida. Use KILOS o LIBRAS.",
        )
    return None


# --- Catálogos: Proveedor, Vendido A, Enviado A, Agente, Transportista, Incoterm, Aduana ---
def _validaciones_catalogos(
    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_broker_ids: Set[int],
    valid_broker_claves: Set[str],
    valid_transporter_keys: Set[str],
    valid_incoterms: Set[str],
    valid_aduana_codes: Set[str],
) -> Optional[Dict[str, Any]]:
    def check_id(val: Any, col: str, catalog_name: str, valid_set: Set[int]) -> Optional[Dict[str, Any]]:
        if val is None or str(val).strip() == "":
            return None
        v = _parse_int(val)
        if v is None:
            return _err(line_num, col, f"Error: (Celda) {col} debe ser un número entero.")
        if valid_set and v not in valid_set:
            return _err(line_num, col, f"Error: La clave en {col} no existe en el Catálogo de {catalog_name}.")
        return None

    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, f"Error: La clave en {col} no existe en el Catálogo de {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, f"Error: La clave/corta en {col} no existe en el Catálogo de {catalog_name}.")
        if not valid_short_names:
            return _err(line_num, col, f"Error: (Celda) {col} debe ser un número entero o clave corta (short name) válida.")
        return None

    def check_id_or_clave(
        val: Any,
        col: str,
        catalog_name: str,
        valid_ids: Set[int],
        valid_claves: Set[str],
    ) -> Optional[Dict[str, Any]]:
        """Para AGENTE ADUANAL: acepta id (entero) o broker_key (clave)."""
        if val is None or str(val).strip() == "":
            return None
        v = _parse_int(val)
        if v is not None:
            # Si no existe como id, intentar también como clave (p.ej. "5567" como broker_key).
            if not valid_ids or v in valid_ids:
                return None
        clave = str(val).strip().upper()
        if valid_claves and clave not in valid_claves:
            return _err(line_num, col, f"Error: La clave en {col} no existe en el Catálogo de {catalog_name}.")
        if not valid_claves:
            return _err(line_num, col, f"Error: (Celda) {col} debe ser un número entero o clave de agente aduanal válida.")
        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
    err = check_id_or_clave(
        row.get("AGENTE ADUANAL"), "AGENTE ADUANAL", "Agentes Aduanales",
        valid_broker_ids, valid_broker_claves,
    )
    if err:
        return err

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

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

    ab = _get(row, "ADUANA DE CRUCE")
    if ab and valid_aduana_codes and not aduana_cruce_matches_catalog(ab, valid_aduana_codes):
        return _err(line_num, "ADUANA DE CRUCE", f"Error: (Celda AB{line_num}) La Aduana de Cruce: {ab} no existe en el Catálogo.")

    return None


def _validaciones_tipo_cambio(
    row: Dict[str, Any],
    line_num: int,
    date_parsed: Optional[datetime],
    exchange_rate_by_date: Dict[str, Any],
    warnings: Optional[List[Dict[str, Any]]],
) -> Optional[Dict[str, Any]]:
    """Si hay fecha y no hay tipo cambio, debe existir en catálogo. Si hay tipo cambio, puede advertir si difiere del catálogo."""
    d = _get(row, "FECHA FACTURA", "FECHA")
    e = row.get("TIPO DE CAMBIO")
    if not d or not date_parsed:
        return None
    date_key = date_parsed.isoformat()[:10] if hasattr(date_parsed, "isoformat") else str(date_parsed)[:10]
    catalog_tc = exchange_rate_by_date.get(date_key) if exchange_rate_by_date else None
    if not _clip(e):
        if catalog_tc is None:
            return _err(
                line_num,
                "TIPO DE CAMBIO",
                f"Error: (Celda E{line_num}) El Tipo de Cambio para la Fecha {d} no se encontró en el Catálogo.",
            )
        return None
    val_e = _parse_decimal(e)
    if val_e is not None and catalog_tc is not None:
        cat_val = catalog_tc if isinstance(catalog_tc, (Decimal, int, float)) else getattr(catalog_tc, "valor", None) or getattr(catalog_tc, "value", None)
        if cat_val is not None and abs(float(val_e) - float(cat_val)) > 0.0001 and warnings is not None:
            warnings.append({
                "line": line_num,
                "col": "TIPO DE CAMBIO",
                "msg": f"Advertencia: (Celda E{line_num}) El Tipo de Cambio capturado ({e}) difiere del Catálogo para la fecha {d}.",
                "warning": True,
            })
    return None


def validate_row_encabezados_impo_temp(
    row: Dict[str, Any],
    line_num: int,
    actualizar: bool,
    invoice_exists_by_number: Dict[str, bool],
    invoice_processed_by_number: Dict[str, bool],
    pedimento_rows: List[Dict[str, Any]],
    remesa_por_pedimento_bd: Dict[str, Set[int]],
    remesa_por_pedimento_csv: Dict[str, Dict[int, str]],
    valid_provider_ids: Set[int],
    valid_sold_to_ids: Set[int],
    valid_shipped_to_ids: Set[int],
    valid_broker_ids: Set[int],
    valid_transporter_keys: Set[str],
    valid_incoterms: Set[str],
    valid_aduana_codes: Set[str],
    valid_currency_codes: Set[str],
    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,
    valid_broker_claves: Optional[Set[str]] = None,
    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,
    autonumerar_remesas: bool = False,
    control_remesa: bool = False,
    remesa_inicio: Optional[int] = None,
    remesa_fin: Optional[int] = None,
    date_format: Optional[str] = None,
    parse_date_fn=None,
    warnings: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
    """
    Punto de entrada: valida una fila de CSV de Encabezados de Importación Temporal.
    Clarion: VALIDA_TODA_FACIMPO_TEM vs VALIDA_PARCIAL_FACIMPO_TEM según actualizar y si la factura existe.
    """
    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",
            f"Error: (Celda C{line_num}) El Número de Factura: {factura} ya existe y está Actualizada, no se puede hacer cambios.",
        ))
        return errors

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

    use_partial = actualizar and invoice_exists_by_number.get(factura.strip(), False)
    tiene_pedimento = bool(_get(row, "PEDIMENTO"))

    if not use_partial:
        err = _validaciones_obligatorios_toda(row, line_num, tiene_pedimento)
        if err:
            errors.append(err)

    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_pedimento_remesa(
            row,
            line_num,
            pedimento_rows,
            remesa_por_pedimento_bd,
            remesa_por_pedimento_csv,
            autonumerar_remesas,
            control_remesa,
            remesa_inicio,
            remesa_fin,
            date_format,
            invoice_date_parsed,
        ),
        _validaciones_factura_longitud(row, line_num),
        _validaciones_regimen(row, line_num),
        _validaciones_transporte(row, line_num),
    ]:
        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
    for err in [
        _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(
            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_broker_ids or set(),
            valid_broker_claves or set(),
            valid_transporter_keys or set(),
            valid_incoterms or set(),
            valid_aduana_codes or set(),
        ),
        _validaciones_tipo_cambio(
            row, line_num, invoice_date_parsed, exchange_rate_by_date or {}, warnings
        ),
    ]:
        if err:
            errors.append(err)

    return errors


def row_to_transport_type_clarion(val: Optional[str]) -> Optional[str]:
    """Mapea valor Clarion Col M a valor enum TransportType (minúscula)."""
    if not val:
        return "none"
    u = _clip(val).upper()
    return TIPO_TRANSPORTE_CLARION_TO_NORM.get(u, "none")
