"""
Validaciones CSV para Partidas de Importación Temporal.
Paridad Clarion: VALIDA_TODA_PARIMPO_TEM, VALIDA_PARCIAL_PARIMPO_TEM, VALIDACIONES_PARIMPO_TEM.
Estructura: NUMERO FACTURA, LINEA, CLASE, CANTIDAD IMPORTADA, ... hasta ID TYPE (columnas A-AG).
"""
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Optional, Set, Tuple, List

# Longitudes máximas Clarion
MAX_LEN_FACTURA = 15
MAX_LEN_LINEA = 5
MAX_LEN_CLASE = 9
MAX_LEN_CANTIDAD_STR = 19
MAX_LEN_ORDEN_COMPRA = 20

PREFERENCIAS_VALIDAS = frozenset({"GENERAL", "TLCS", "PROSEC", "ALADI"})
PREFERENCIA_ALIASES = {
    "N": "GENERAL",
    "NORMAL": "GENERAL",
    "GRAL": "GENERAL",
    "TLC": "TLCS",
    "PRO": "PROSEC",
}
SE_PAGO_IMPUESTO_VALIDOS = frozenset({"SI", "NO"})

APOSTROFE = "'"


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


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 _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 _norm_invoice(value: Any) -> str:
    return _clip(value).upper()


def normalize_preferencia_arancelaria(value: Any) -> str:
    """Map CSV/manual textual variants to system canonical preference types."""
    raw = _clip(value)
    if not raw:
        return ""
    text = raw.upper()
    return PREFERENCIA_ALIASES.get(text, text)


def _check_factura_vacia(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    val = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if not val:
        return {
            "line": line_num,
            "col": "NUMERO FACTURA",
            "msg": "Error: (Celda A) La Factura de Importación está vacía y no se pueden hacer las validaciones.",
        }
    return None


def _check_factura_existe(
    invoice_number: str,
    line_num: int,
    invoice_id_by_number: Dict[str, int],
) -> Optional[Dict[str, Any]]:
    if _norm_invoice(invoice_number) not in invoice_id_by_number:
        return {
            "line": line_num,
            "col": "NUMERO FACTURA",
            "msg": f"Error: (Celda A) La Factura de Importación {invoice_number} no existe en SCAII y no se pueden hacer las validaciones.",
        }
    return None


def _check_factura_no_actualizada(
    invoice_number: str,
    line_num: int,
    invoice_processed_by_number: Dict[str, bool],
    rfc_exception_updated: Set[str],
) -> Optional[Dict[str, Any]]:
    inv_norm = _norm_invoice(invoice_number)
    if inv_norm in rfc_exception_updated:
        return None
    if invoice_processed_by_number.get(inv_norm, False):
        return {
            "line": line_num,
            "col": "NUMERO FACTURA",
            "msg": f"Error: (Celda A) La Factura de Importación: {invoice_number} ya existe y está Actualizada, no se puede hacer cambios a las facturas actualizadas.",
        }
    return None


def _check_linea_si_no_autonumerar(row: Dict[str, Any], line_num: int, autonumerar: bool) -> Optional[Dict[str, Any]]:
    if autonumerar:
        return None
    val = _get(row, "LINEA", "RENGLON", "PARTIDA")
    if not val:
        return {
            "line": line_num,
            "col": "LINEA",
            "msg": "Error: (Celda B) El campo de la línea de la partida está vacío y no se pueden hacer las validaciones, ya que se tiene la opción de Autonumerar como NO.",
        }
    return None


def _check_levantar_subpartidas_uv(
    row: Dict[str, Any], line_num: int, levantar_subpartidas: bool
) -> Optional[Dict[str, Any]]:
    if not levantar_subpartidas:
        return None
    u = _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA")
    if not u:
        return {
            "line": line_num,
            "col": "ES PARTIDA O SUBPARTIDA",
            "msg": "Error: (Celda U) El campo del tipo de la partida (partida o subpartida) está vacío y no se pueden hacer las validaciones, ya que se tiene la opción de Levantar Subpartidas como Si.",
        }
    v = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if not v:
        return {
            "line": line_num,
            "col": "LINEA PRINCIPAL",
            "msg": "Error: (Celda V) El campo de la partida principal está vacío y no se pueden hacer las validaciones, ya que se tiene la opción de Levantar Subpartidas como Si.",
        }
    return None


def _valida_toda_obligatorios(
    row: Dict[str, Any],
    line_num: int,
    levantar_subpartidas: bool,
    calcular_costo_en_base_a_total: bool,
) -> Optional[Dict[str, Any]]:
    """Obligatorios vacíos: C, D, F (condicional), G, K, M; U, V si LevantarSubpartidas."""
    obligatorios: List[str] = []
    if not _get(row, "CLASE"):
        obligatorios.append("(Col.C) Clases")
    if not _get(row, "CANTIDAD IMPORTADA", "CANTIDAD"):
        obligatorios.append("(Col.D) Cantidad Importada")
    es_subpartida = _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA").upper() == "S"
    if not calcular_costo_en_base_a_total and not es_subpartida:
        if not _get(row, "COSTO UNITARIO", "COSTOUNITARIO", "PRECIO UNITARIO", "PRECIOUNITARIO"):
            obligatorios.append("(Col.F) Costo Unitario")
    if not _get(row, "PESO NETO", "PESONETO"):
        obligatorios.append("(Col.G) Peso Neto")
    if not _get(row, "PAIS ORIGEN", "PAISORIGEN", "PAIS"):
        obligatorios.append("(Col.K) País")
    if not _get(row, "PREFERENCIA ARANCELARIA", "PREFERENCIA", "PREFERENCIAARANCELARIA"):
        obligatorios.append("(Col.M) Preferencia.")
    if levantar_subpartidas:
        if not _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA"):
            obligatorios.append("(Col.U) EsSubpartida?.")
        if not _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL"):
            obligatorios.append("(Col.V) Partida Principal.")
    if obligatorios:
        return {
            "line": line_num,
            "col": "CLASE",
            "msg": f"Existen campos vacíos que son obligatorios: {', '.join(obligatorios)}.",
        }
    return None


def _valida_toda_numericos(
    row: Dict[str, Any],
    line_num: int,
    calcular_costo_en_base_a_total: bool,
) -> Optional[Dict[str, Any]]:
    """Costo unitario y peso neto no pueden ser cero (salvo subpartida / costo por total)."""
    es_subpartida = _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA").upper() == "S"
    if not calcular_costo_en_base_a_total and not es_subpartida:
        costo = _parse_decimal(row.get("COSTO UNITARIO") or row.get("COSTOUNITARIO") or row.get("PRECIO UNITARIO") or row.get("PRECIOUNITARIO"))
        if costo is not None and costo == 0:
            return {
                "line": line_num,
                "col": "COSTO UNITARIO",
                "msg": f"Error: (Celda F{line_num}) El Costo Unitario no puede ser cero.",
            }
    peso_neto = _parse_decimal(row.get("PESO NETO") or row.get("PESONETO"))
    if peso_neto is not None and peso_neto == 0:
        return {
            "line": line_num,
            "col": "PESO NETO",
            "msg": f"Error: (Celda G{line_num}) El Peso Neto no puede ser cero.",
        }
    return None


def _valida_subpartidas_duplicados(
    invoice_number: str,
    linea: str,
    line_num: int,
    line_counts: Dict[Tuple[str, str], int],
) -> Optional[Dict[str, Any]]:
    key = (_norm_invoice(invoice_number), _clip(linea))
    if line_counts.get(key, 0) > 1:
        return {
            "line": line_num,
            "col": "LINEA",
            "msg": f"Error: (Celda B{line_num}) El campo de la partida está duplicado entre las partidas.",
        }
    return None


def _valida_subpartida_tiene_principal(
    row: Dict[str, Any],
    line_num: int,
    partidas_principales_en_csv: Set[Tuple[str, str]],
    partidas_principales_en_bd: Set[Tuple[str, str]],
) -> Optional[Dict[str, Any]]:
    u = _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA").upper()
    v = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if u != "S" or not v or v == "0":
        return None
    inv = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if not inv:
        return None
    key_principal = (_norm_invoice(inv), _clip(v))
    if key_principal in partidas_principales_en_csv or key_principal in partidas_principales_en_bd:
        return None
    return {
        "line": line_num,
        "col": "LINEA PRINCIPAL",
        "msg": f"Error: (Celda V{line_num}) La partida principal {v} no existe.",
    }


def _valida_subpartida_v_no_cero(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    u = _get(row, "ES PARTIDA O SUBPARTIDA", "ESSUBPARTIDA").upper()
    v = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if u == "S" and v == "0":
        return {
            "line": line_num,
            "col": "LINEA PRINCIPAL",
            "msg": f"Error: (Celda V{line_num}) La SubPartida no tiene asignada una partida principal.",
        }
    return None


def _validaciones_parimpo_tem(
    row: Dict[str, Any],
    line_num: int,
    valid_class_codes: Set[str],
    class_um_by_code: Dict[str, str],
    class_fraction_by_code: Dict[str, str],
    class_desc_es_by_code: Dict[str, str],
    class_desc_en_by_code: Dict[str, str],
    valid_uom_codes: Set[str],
    valid_bulks_codes: Set[str],
    valid_country_keys: Set[str],
    valid_fraction_ame: Set[str],
    valid_payment_methods: Set[str],
    valid_valuation_methods: Set[str],
    authorized_sectors: Set[str],
    company_has_prosec: bool,
    validar_decimales_pza: bool,
    rfc_exception_num_parte: Optional[Set[str]],
    invoice_number: str,
) -> Optional[Dict[str, Any]]:
    """VALIDACIONES_PARIMPO_TEM: longitudes, catálogos, reglas de negocio."""

    def err(col: str, msg: str) -> Dict[str, Any]:
        return {"line": line_num, "col": col, "msg": msg}

    clase = _get(row, "CLASE")
    um = _get(row, "UNIDAD DE MEDIDA", "UNIDAD MEDIDA")

    # Longitudes
    factura = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if factura and len(factura) > MAX_LEN_FACTURA:
        return err("NUMERO FACTURA", f"Error: (Celda A{line_num}) La Factura de Importación: {factura} supera la longitud de caracteres.")
    linea = _get(row, "LINEA", "RENGLON", "PARTIDA")
    if linea and len(linea) > MAX_LEN_LINEA:
        return err("LINEA", f"Error: (Celda B{line_num}) La Línea de Importación: {linea} supera la longitud de caracteres.")
    if clase:
        if len(clase) > MAX_LEN_CLASE:
            return err("CLASE", f"Error: (Celda C{line_num}) La Clase: {clase} supera la longitud de caracteres.")
        if clase.upper() not in valid_class_codes:
            return err("CLASE", f"Error: (Celda C{line_num}) La Clase: {clase} no existe en el Catálogo de Clases.")
        if not um and not class_um_by_code.get(clase.upper()):
            return err("UNIDAD DE MEDIDA", f"Error: (Celda E{line_num}) Debido a que esta celda es vacía, se asignará la unidad de medida de la Clase pero también está vacía.")
        frac = _get(row, "FRACCION ARANCELARIA", "FRACCION", "FRACCIONARANCELARIA")
        if not frac and not class_fraction_by_code.get(clase.upper()):
            return err("FRACCION ARANCELARIA", f"Error: (Celda L{line_num}) Debido a que esta celda es vacía, se asignará la fracción de la Clase pero también está vacía.")
        if not _get(row, "DESCRIPCION ESPAÑOL", "DESCRIPCIONE", "DESCRIPCION") and not class_desc_es_by_code.get(clase.upper()):
            return err("DESCRIPCION ESPAÑOL", f"Error: (Celda Q{line_num}) Debido a que esta celda es vacía, se asignará la Descripción en Español de la Clase pero también está vacía.")
        if not _get(row, "DESCRIPCION INGLES", "DESCRIPCIONI") and not class_desc_en_by_code.get(clase.upper()):
            return err("DESCRIPCION INGLES", f"Error: (Celda R{line_num}) Debido a que esta celda es vacía, se asignará la Descripción en Inglés de la Clase pero también está vacía.")

    # D: Cantidad importada
    cant_str = _get(row, "CANTIDAD IMPORTADA", "CANTIDAD")
    if cant_str:
        cant = _parse_decimal(cant_str)
        if cant is not None and cant <= 0:
            return err("CANTIDAD IMPORTADA", f"Error: (Celda D{line_num}) La Cantidad Importada: {cant_str} es cero.")
        if len(cant_str) > MAX_LEN_CANTIDAD_STR:
            return err("CANTIDAD IMPORTADA", f"Error: (Celda D{line_num}) La Cantidad Importada: {cant_str} supera la cantidad de caracteres permitidos.")

    # E: Unidad de medida en catálogo
    if um and um.upper() not in valid_uom_codes:
        return err("UNIDAD DE MEDIDA", f"Error: (Celda E{line_num}) La Unidad de Medida: {um} no existe en el Catálogo de Unidades de Medida.")

    # I, J: Bultos condicional
    clave_bultos = _get(row, "CLAVE BULTOS", "CLAVEBULTOS")
    cant_bultos = row.get("CANTIDAD BULTOS") or row.get("CANTIDADBULTOS")
    if clave_bultos:
        if clave_bultos not in valid_bulks_codes:
            return err("CLAVE BULTOS", f"Error: (Celda J{line_num}) La Clave de Bulto: {clave_bultos} no existe en el Catálogo de Claves de Bultos.")
        cant_bultos_val = _parse_decimal(cant_bultos)
        if cant_bultos_val is None:
            return err("CANTIDAD BULTOS", f"Error: (Celda I{line_num}) La Cantidad de Bultos está vacía y en la Celda J{line_num} se tiene la Clave de Bulto.")
        if cant_bultos_val == 0:
            return err("CANTIDAD BULTOS", f"Error: (Celda I{line_num}) La Cantidad de Bultos es cero y en la Celda J{line_num} se tiene la Clave de Bulto.")
    else:
        cant_bultos_val = _parse_decimal(cant_bultos)
        if cant_bultos_val is not None and cant_bultos_val > 0:
            return err("CANTIDAD BULTOS", f"Error: (Celda I{line_num}) La Cantidad de Bultos es {cant_bultos} y en la Celda J{line_num} no se tiene la Clave de Bulto.")

    # K: País SAAIM3 o americana
    pais = _get(row, "PAIS ORIGEN", "PAISORIGEN", "PAIS")
    if pais and pais.upper() not in valid_country_keys:
        return err("PAIS ORIGEN", f"Error: (Celda K{line_num}) El País: {pais} no se encontró como Clave SAAIM3 ni Clave Americana en el Catálogo de Paises.")

    # M: Preferencia
    pref_raw = _get(row, "PREFERENCIA ARANCELARIA", "PREFERENCIA", "PREFERENCIAARANCELARIA")
    pref = normalize_preferencia_arancelaria(pref_raw)
    pref_ctx = f"{pref} (capturada: {pref_raw})" if pref_raw and pref_raw.strip().upper() != pref else pref
    if pref and pref not in PREFERENCIAS_VALIDAS:
        return err(
            "PREFERENCIA ARANCELARIA",
            f"Error: (Celda M{line_num}) La Preferencia Arancelaria: {pref_ctx} no es correcta para el sistema SCAF.",
        )
    sector = _get(row, "SECTOR")
    if pref == "PROSEC":
        if not sector:
            return err(
                "PREFERENCIA ARANCELARIA",
                f"Error: (Celda M{line_num} y N{line_num}) La Preferencia Arancelaria es: {pref_ctx} y en la columna N no tiene sector.",
            )
        if sector not in authorized_sectors:
            return err("SECTOR", f"Error: (Celda N{line_num}) El Sector: {sector} no existe en el Catálogo de Sectores.")
        if not company_has_prosec:
            return err("SECTOR", f"Error: (Celda N{line_num}) La empresa no cuenta con autorización PROSEC.")
    elif pref and pref != "PROSEC" and sector:
        return err(
            "PREFERENCIA ARANCELARIA",
            f"Error: (Celda M{line_num} y N{line_num}) La Preferencia Arancelaria es: {pref_ctx} y en la columna N tiene sector.",
        )

    # O: Fracción americana (SITAR fracciones-usa; valid_fraction_ame ignorado)
    from api.v1.modules.sitar.fracciones_usa.catalog_resolve import (
        resolve_american_fraction_from_sitar,
    )

    frac_ame = _get(row, "FRACCION AMERICANA", "FRACCIONAMERICANA")
    if frac_ame and not resolve_american_fraction_from_sitar(frac_ame):
        return err(
            "FRACCION AMERICANA",
            f"Advertencia: (Celda O{line_num}) La Fracción Americana: {frac_ame} no existe en el catálogo SITAR (fracciones USA).",
        )

    # P: Orden de compra máx 20
    orden = _get(row, "ORDEN DE COMPRA", "ORDENCOMPRA")
    if orden and len(orden) > MAX_LEN_ORDEN_COMPRA:
        return err("ORDEN DE COMPRA", f"Error: (Celda P{line_num}) La Orden de Compra: {orden} supera la cantidad de caracteres permitidos.")

    # Decimales PZA
    if validar_decimales_pza:
        um_code = (um or class_um_by_code.get(clase.upper() or "") or "").upper()
        if um_code in ["PZA", "PIEZA", "PIEZAS", "PZAS", "PCE", "1"] and cant_str:
            d = _parse_decimal(cant_str)
            if d is not None and d != int(d):
                return err("CANTIDAD IMPORTADA", "Error: (Celda D) La Unidad de Medida es PZA, Por lo Tanto no es Válida la Captura de Decimales.")

    # Z: Método de valoración
    met_val = _get(row, "METODO DE VALORACION", "METODODEVALORACION", "METODO VALORACION")
    if met_val and met_val not in valid_valuation_methods:
        return err("METODO DE VALORACION", f"Error: (Celda Z{line_num}) El Método de Valoración Capturado: {met_val} No es Válido.")

    # RFC excepción: W obligatorio
    if rfc_exception_num_parte and invoice_number in rfc_exception_num_parte:
        num_parte = _get(row, "NUM. PARTE", "NUMPARTE", "NUMERO PARTE", "NUM PARTE")
        if not num_parte:
            return err("NUM. PARTE", f"Error: (Celda W{line_num}) No está capturado el número de parte.")

    # X: Se pagó impuesto SI/NO
    x = _get(row, "SE PAGO IMPUESTO", "SEPAGOIMPUESTO")
    if x and x.upper() not in SE_PAGO_IMPUESTO_VALIDOS:
        return err("SE PAGO IMPUESTO", f"Error: (Celda X{line_num}) El Valor Capturado para Se Pago Impuesto no es Válido. Capturar SI o NO.")

    # Y: Forma de pago en catálogo
    forma_pago = _get(row, "FORMA DE PAGO", "FORMADEPAGO", "FORMA PAGO")
    if forma_pago and forma_pago not in valid_payment_methods:
        return err("FORMA DE PAGO", f"Error: (Celda Y{line_num}) La Forma de Pago Capturado no es Válido.")

    return None


def _warn_apostrofes_num_parte(
    row: Dict[str, Any], line_num: int, warnings: Optional[List[Dict[str, Any]]]
) -> None:
    val = _get(row, "NUM. PARTE", "NUMPARTE", "NUMERO PARTE", "NUM PARTE")
    if val and APOSTROFE in val and warnings is not None:
        warnings.append({
            "line": line_num,
            "col": "NUM. PARTE",
            "msg": f"Advertencia: El Número de Parte: {val} Contiene Apostrofes.",
            "warning": True,
        })


def validate_row_partidas_impo_temp(
    row: Dict[str, Any],
    line_num: int,
    autonumerar: bool,
    actualizar: bool,
    levantar_subpartidas: bool,
    calcular_costo_en_base_a_total: bool,
    validar_decimales_pza: bool,
    invoice_id_by_number: Dict[str, int],
    invoice_processed_by_number: Dict[str, bool],
    rfc_exception_updated: Set[str],
    existing_line_keys_by_invoice: Dict[str, Set[str]],
    line_counts_csv: Dict[Tuple[str, str], int],
    partidas_principales_csv: Set[Tuple[str, str]],
    partidas_principales_bd: Set[Tuple[str, str]],
    valid_class_codes: Set[str],
    class_um_by_code: Dict[str, str],
    class_fraction_by_code: Dict[str, str],
    class_desc_es_by_code: Dict[str, str],
    class_desc_en_by_code: Dict[str, str],
    valid_uom_codes: Set[str],
    valid_bulks_codes: Set[str],
    valid_country_keys: Set[str],
    valid_fraction_ame: Set[str],
    valid_payment_methods: Set[str],
    valid_valuation_methods: Set[str],
    authorized_sectors: Set[str],
    company_has_prosec: bool,
    rfc_exception_num_parte: Optional[Set[str]],
    valid_part_numbers: Optional[Set[str]],
    warnings: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
    """
    Punto de entrada: valida una fila de CSV de Partidas de Importación Temporal.
    Clarion: VALIDA_TODA vs VALIDA_PARCIAL según autonumerar, actualizar y si la partida existe.
    """
    errors: List[Dict[str, Any]] = []

    err = _check_factura_vacia(row, line_num)
    if err:
        errors.append(err)
        return errors

    invoice_number = _get(row, "NUMERO FACTURA", "NUM FACTURA", "FACTURA")
    if not invoice_number:
        errors.append({"line": line_num, "col": "NUMERO FACTURA", "msg": "Requerido"})
        return errors

    for err in [
        _check_factura_existe(invoice_number, line_num, invoice_id_by_number),
        _check_factura_no_actualizada(
            invoice_number, line_num, invoice_processed_by_number, rfc_exception_updated
        ),
        _check_linea_si_no_autonumerar(row, line_num, autonumerar),
        _check_levantar_subpartidas_uv(row, line_num, levantar_subpartidas),
    ]:
        if err:
            errors.append(err)

    if errors:
        return errors

    _warn_apostrofes_num_parte(row, line_num, warnings)

    linea = _get(row, "LINEA", "RENGLON", "PARTIDA")
    invoice_number_norm = _norm_invoice(invoice_number)
    existing_lines = existing_line_keys_by_invoice.get(invoice_number_norm, set())
    partida_existe = bool(linea and linea in existing_lines)
    use_partial = actualizar and not autonumerar and partida_existe

    _validaciones_kwargs = dict(
        valid_class_codes=valid_class_codes,
        class_um_by_code=class_um_by_code,
        class_fraction_by_code=class_fraction_by_code,
        class_desc_es_by_code=class_desc_es_by_code,
        class_desc_en_by_code=class_desc_en_by_code,
        valid_uom_codes=valid_uom_codes,
        valid_bulks_codes=valid_bulks_codes,
        valid_country_keys=valid_country_keys,
        valid_fraction_ame=valid_fraction_ame,
        valid_payment_methods=valid_payment_methods,
        valid_valuation_methods=valid_valuation_methods,
        authorized_sectors=authorized_sectors,
        company_has_prosec=company_has_prosec,
        validar_decimales_pza=validar_decimales_pza,
        rfc_exception_num_parte=rfc_exception_num_parte,
        invoice_number=invoice_number,
    )

    if use_partial:
        err = _validaciones_parimpo_tem(row, line_num, **_validaciones_kwargs)
        if err:
            errors.append(err)
    else:
        for err in [
            _valida_toda_obligatorios(row, line_num, levantar_subpartidas, calcular_costo_en_base_a_total),
            _valida_toda_numericos(row, line_num, calcular_costo_en_base_a_total),
        ]:
            if err:
                errors.append(err)
        if levantar_subpartidas:
            for err in [
                _valida_subpartidas_duplicados(invoice_number_norm, linea, line_num, line_counts_csv),
                _valida_subpartida_tiene_principal(row, line_num, partidas_principales_csv, partidas_principales_bd),
                _valida_subpartida_v_no_cero(row, line_num),
            ]:
                if err:
                    errors.append(err)
        err = _validaciones_parimpo_tem(row, line_num, **_validaciones_kwargs)
        if err:
            errors.append(err)
        # Paridad con commit: si viene NUM. PARTE capturado, debe existir en catálogo.
        # La excepción RFC solo controla obligatoriedad; no desactiva la verificación de existencia.
        if valid_part_numbers is not None:
            num_parte = _get(row, "NUM. PARTE", "NUMPARTE", "NUMERO PARTE", "NUM PARTE")
            if num_parte and num_parte.upper() not in valid_part_numbers:
                errors.append({
                    "line": line_num,
                    "col": "NUM. PARTE",
                    "msg": f"Error: (Celda W{line_num}) El número de parte Capturado: {num_parte} no existe.",
                })
    return errors
