"""
Validaciones CSV para Partidas de Exportación Definitiva (y Cambio de Régimen).
Paridad Clarion: VALIDA_TODA_PAR_EXPO, VALIDA_PARCIAL_PAR_EXPO, VALIDACIONES_PAR_EXPO.
Estructura: NUMERO FACTURA EXPO, LINEA EXPO, TIPO DE IMPO, FACTURA IMPO, LINEA IMPO, GENERA DESCARGA, ...
Variante RFC EGM0303257J1: columnas ES PARTIDA/SUBPARTIDA y LINEA PRINCIPAL en T y U.
"""
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Optional, Set, Tuple, List

from .partidas_impo_temp import _clip, _get

# Longitudes Clarion partidas expo (Factura EXPO alineado con invoice_number String(100) en InvoiceHeader)
MAX_LEN_FACTURA_EXPO = 100
MAX_LEN_LINEA_EXPO = 5
MAX_LEN_TIPO_IMPO = 3
MAX_LEN_ORDEN_COMPRA = 20
MAX_LEN_NUM_PARTE = 30

TIPO_IMPO_VALIDOS = frozenset({"TEM", "DEF"})
GENERA_DESCARGA_VALIDOS = frozenset({"SI", "NO"})
SE_PAGO_IMPUESTO_VALIDOS = frozenset({"SI", "NO"})


def _err(
    line_num: int,
    col: str,
    msg: str,
    identifier: str = "ARCHIVO CSV",
) -> Dict[str, Any]:
    return {"line": line_num, "col": col, "msg": msg}


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


def _check_factura_expo_vacia(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    val = _get(row, "NUMERO FACTURA EXPO", "NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO")
    if not val:
        return _err(
            line_num,
            "NUMERO FACTURA EXPO",
            f"Error: (Celda A{line_num}) La Factura de Exportación está vacía y no se pueden hacer las validaciones. "
            "Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar partidas.",
        )
    return None


def _check_factura_expo_existe(
    invoice_number: str,
    line_num: int,
    invoice_id_by_number: Dict[str, int],
) -> Optional[Dict[str, Any]]:
    if invoice_number not in invoice_id_by_number:
        return _err(
            line_num,
            "NUMERO FACTURA EXPO",
            f"Error: (Celda A{line_num}) La Factura de Exportación {invoice_number} no existe en SCAII y no se pueden hacer las validaciones. "
            "Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar partidas.",
        )
    return None


def _check_factura_expo_no_actualizada(
    invoice_number: str,
    line_num: int,
    invoice_processed_by_number: Dict[str, bool],
    rfc_exception_updated: Set[str],
) -> Optional[Dict[str, Any]]:
    if invoice_number in rfc_exception_updated:
        return None
    if invoice_processed_by_number.get(invoice_number, False):
        return _err(
            line_num,
            "NUMERO FACTURA EXPO",
            f"Error: (Celda A{line_num}) La Factura de Exportación: {invoice_number} ya existe y está Actualizada, no se puede hacer cambios a las facturas actualizadas. "
            "Capturar otro número de Factura de Exportación o Desactualizar la factura.",
        )
    return None


def _check_linea_expo_si_no_autonumerar(
    row: Dict[str, Any], line_num: int, autonumerar: bool
) -> Optional[Dict[str, Any]]:
    if autonumerar:
        return None
    val = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
    if not val:
        return _err(
            line_num,
            "LINEA EXPO",
            f"Error: (Celda B{line_num}) 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. "
            "Capturar en la Celda B la línea de la partida al cual desee agregar o actualizar información.",
        )
    return None


def _check_levantar_subpartidas_expo(
    row: Dict[str, Any],
    line_num: int,
    levantar_subpartidas: bool,
    rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
    """Clarion: si LevantarSubpartidas=S, obligatorios ES PARTIDA/SUBPARTIDA y LINEA PRINCIPAL (col S/T o T/U para EGM0303257J1)."""
    if not levantar_subpartidas:
        return None
    es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA")
    if not es_sub:
        col = "T" if rfc_exception_egm else "S"
        return _err(
            line_num,
            "ES PARTIDA/SUBPARTIDA",
            f"Error: (Celda {col}{line_num}) El campo del tipo de la partida (partida o subpartida) está vacío. "
            "Capturar el tipo de la partida. [P] = Partida o [S] = Subpartida.",
        )
    linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if not linea_principal:
        col = "U" if rfc_exception_egm else "T"
        return _err(
            line_num,
            "LINEA PRINCIPAL",
            f"Error: (Celda {col}{line_num}) El campo de la partida principal está vacío. "
            f"Capturar en la Celda {col} la partida principal.",
        )
    return None


def _valida_toda_obligatorios_expo(
    row: Dict[str, Any],
    line_num: int,
    levantar_subpartidas: bool,
    rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
    """VALIDA_TODA_PAR_EXPO obligatorios: C (tipo impo), F (descarga), G (cantidad); si descarga=SI: D, E; si subpartidas: S/T o T/U."""
    obligatorios: List[str] = []
    tipo_impo = _get(row, "TIPO DE IMPO", "TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA")
    if not tipo_impo:
        obligatorios.append("(Col.C) Procedencia de la Importación.")
    descarga = _get(row, "GENERA DESCARGA", "GENERA DESCARGA?", "DESCARGA")
    if descarga == "SI":
        if not _get(row, "FACTURA IMPO", "FACTURA IMPO.", "FACTURA IMPORTACION"):
            obligatorios.append("(Col.D) Factura de Impo.")
        if not _get(row, "LINEA IMPO", "LINEA IMPO.", "LINEA IMPORTACION"):
            obligatorios.append("(Col.E) Línea de Impo.")
    if not descarga:
        obligatorios.append("(Col.F) Descarga? SI o NO")
    if not _get(row, "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD EXPORTADA", "CANTIDAD"):
        obligatorios.append("(Col.G) Cantidad Expo.")
    if levantar_subpartidas:
        if rfc_exception_egm:
            if not _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"):
                obligatorios.append("(Col.T) EsSubpartida?.")
            if not _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL"):
                obligatorios.append("(Col.U) Partida Principal.")
        else:
            if not _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA"):
                obligatorios.append("(Col.S) EsSubpartida?.")
            if not _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL"):
                obligatorios.append("(Col.T) Partida Principal.")
    if obligatorios:
        return _err(
            line_num,
            "TIPO DE IMPO",
            f"Existen campos vacíos que son obligatorios, es la {', '.join(obligatorios)}. "
            "Revisar la línea del archivo y capturar los campos con la información correcta.",
        )
    return None


def _valida_subpartidas_duplicados_expo(
    factura_expo: str,
    linea_expo: str,
    line_num: int,
    line_counts_csv: Dict[Tuple[str, str], int],
) -> Optional[Dict[str, Any]]:
    key = (factura_expo.strip(), _clip(linea_expo))
    if line_counts_csv.get(key, 0) > 1:
        return _err(
            line_num,
            "LINEA EXPO",
            f"Error: (Celda B{line_num}) El campo de la partida está duplicado entre las partidas. "
            f"Capturar en la Celda B{line_num} otro número de partida.",
        )
    return None


def _valida_subpartida_tiene_principal_expo(
    row: Dict[str, Any],
    line_num: int,
    factura_expo: str,
    partidas_principales_csv: Set[Tuple[str, str]],
    partidas_principales_bd: Set[Tuple[str, str]],
    rfc_exception_egm: bool,
) -> Optional[Dict[str, Any]]:
    es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA").upper()
    linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if es_sub != "S" or not linea_principal or linea_principal == "0":
        return None
    key_principal = (factura_expo.strip(), _clip(linea_principal))
    if key_principal in partidas_principales_csv or key_principal in partidas_principales_bd:
        return None
    col = "U" if rfc_exception_egm else "T"
    return _err(
        line_num,
        "LINEA PRINCIPAL",
        f"Error: (Celda {col}{line_num}) La partida principal {linea_principal} no existe. "
        f"Capturar en la Celda {col} la partida principal y/o verificar que si permita contener subpartidas.",
    )


def _valida_subpartida_linea_principal_no_cero_expo(
    row: Dict[str, Any], line_num: int, rfc_exception_egm: bool
) -> Optional[Dict[str, Any]]:
    es_sub = _get(row, "ES PARTIDA/SUBPARTIDA", "ESSUBPARTIDA", "ES PARTIDA O SUBPARTIDA").upper()
    linea_principal = _get(row, "LINEA PRINCIPAL", "LINEAPRINCIPAL", "PARTIDA PRINCIPAL")
    if es_sub == "S" and linea_principal == "0":
        col = "U" if rfc_exception_egm else "T"
        return _err(
            line_num,
            "LINEA PRINCIPAL",
            f"Error: (Celda {col}{line_num}) La SubPartida no tiene asignada una partida principal. "
            f"Capturar en la Celda {col} una partida principal.",
        )
    return None


def _validaciones_par_expo(
    row: Dict[str, Any],
    line_num: int,
    factura_expo: str,
    valid_uom_codes: Set[str],
    valid_bulks_codes: Set[str],
    valid_payment_methods: Set[str],
    valid_fraction_ame: Set[str],
    valid_part_numbers: Optional[Set[str]],
    factura_impo_tem_by_number: Dict[str, int],
    factura_impo_def_by_number: Dict[str, int],
    line_exists_tem: Set[Tuple[int, str]],
    line_exists_def: Set[Tuple[int, str]],
    validar_decimales_pza: bool = False,
) -> Optional[Dict[str, Any]]:
    """VALIDACIONES_PAR_EXPO: longitudes, TEM/DEF, FK factura impo + línea, descarga SI/NO, cantidad, U.M., bultos, forma pago, impuesto, fracción ame, num parte, decimales PZA."""
    tipo_impo = _get(row, "TIPO DE IMPO", "TIPO DE IMPO.", "TIPO IMPO", "PROCEDENCIA").strip().upper()
    if not tipo_impo:
        tipo_impo = "TEM"
    factura_impo = _get(row, "FACTURA IMPO", "FACTURA IMPO.", "FACTURA IMPORTACION")
    linea_impo = _get(row, "LINEA IMPO", "LINEA IMPO.", "LINEA IMPORTACION")
    descarga = _get(row, "GENERA DESCARGA", "GENERA DESCARGA?", "DESCARGA").upper()

    # Longitud A
    if factura_expo and len(factura_expo) > MAX_LEN_FACTURA_EXPO:
        return _err(
            line_num,
            "NUMERO FACTURA EXPO",
            f"Error: (Celda A{line_num}) La Factura de Exportación: {factura_expo} supera la longitud de caracteres. "
            f"Capturar en la Celda A el campo Factura de Exportación con un máximo de {MAX_LEN_FACTURA_EXPO} caracteres.",
        )
    # Longitud B
    linea_expo = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
    if linea_expo and len(linea_expo) > MAX_LEN_LINEA_EXPO:
        return _err(
            line_num,
            "LINEA EXPO",
            f"Error: (Celda B{line_num}) La Línea de Exportación: {linea_expo} supera la longitud de caracteres. "
            "Capturar en la Celda B una Línea de Exportación con formato #####.",
        )
    # Longitud C
    if tipo_impo and len(tipo_impo) > MAX_LEN_TIPO_IMPO:
        return _err(
            line_num,
            "TIPO DE IMPO",
            f"Error: (Celda C{line_num}) La Procedencia debe ser especificada como TEM o DEF. "
            "Capturar en la Celda C una procedencia no mayor de 3 caracteres.",
        )
    # C: TEM o DEF
    if tipo_impo and tipo_impo not in TIPO_IMPO_VALIDOS:
        return _err(
            line_num,
            "TIPO DE IMPO",
            f"Error: (Celda C{line_num}) El Tipo de Descargo: {tipo_impo} no es valido. Capturar uno valido como TEM o DEF.",
        )
    # D, E: factura impo + línea existen en catálogo TEM o DEF
    if factura_impo and tipo_impo:
        consec_tem = factura_impo_tem_by_number.get(factura_impo.strip())
        consec_def = factura_impo_def_by_number.get(factura_impo.strip())
        if tipo_impo == "TEM":
            if consec_tem is None:
                return _err(
                    line_num,
                    "FACTURA IMPO",
                    f"Error: (Celda D{line_num}) La Factura: {factura_impo} de Importación Temporal no existe. "
                    "Capturar un Número de Factura que exista en el Catálogo de Importaciones Temporales.",
                )
            key_line = (consec_tem, _clip(linea_impo))
            if linea_impo and key_line not in line_exists_tem:
                return _err(
                    line_num,
                    "LINEA IMPO",
                    f"Error: (Celda D{line_num}, E{line_num}) La Factura: {factura_impo} con línea: {linea_impo} de Importación Temporal no existe. "
                    "Capturar un Número de Factura con diferente línea que este en el Catálogo de Importaciones Temporales.",
                )
        elif tipo_impo == "DEF":
            if consec_def is None:
                return _err(
                    line_num,
                    "FACTURA IMPO",
                    f"Error: (Celda D{line_num}) La Factura: {factura_impo} de Importación Definitiva no existe. "
                    "Capturar un Número de Factura que exista en el Catálogo de Importaciones Definitivas.",
                )
            key_line = (consec_def, _clip(linea_impo))
            if linea_impo and key_line not in line_exists_def:
                return _err(
                    line_num,
                    "LINEA IMPO",
                    f"Error: (Celda D{line_num}, E{line_num}) La Factura: {factura_impo} con línea: {linea_impo} de Importación Definitiva no existe. "
                    "Capturar un Número de Factura con diferente línea que este en el Catálogo de Importaciones Definitivas.",
                )
    # F: SI o NO
    if descarga and descarga not in GENERA_DESCARGA_VALIDOS:
        return _err(
            line_num,
            "GENERA DESCARGA",
            f"Error: (Celda F{line_num}) La captura: {descarga} no es valido para la opción de que la partida genere descarga. "
            "Capturar un valor valido como SI o NO o dejar vacio y lo tomará como un SI.",
        )
    # G: cantidad no cero
    cant_str = _get(row, "CANTIDAD EXPORTADA/DESCARGAR", "CANTIDAD EXPORTADA", "CANTIDAD")
    if cant_str:
        cant = _parse_decimal(cant_str)
        if cant is not None and cant == 0:
            return _err(
                line_num,
                "CANTIDAD EXPORTADA/DESCARGAR",
                f"Error: (Celda G{line_num}) La Cantidad a Exportar: {cant_str} no puede ser cero. Capturar una cantidad a exportar valida.",
            )
    # H: U.M. en catálogo (si no se toma de impo)
    um = _get(row, "UNIDAD DE MEDIDA", "U.M.", "UNIDAD MEDIDA")
    if um and valid_uom_codes and um.upper() not in valid_uom_codes:
        return _err(
            line_num,
            "UNIDAD DE MEDIDA",
            f"Error: (Celda H{line_num}) La U.M.: {um} no existe en catálogo de Unidades de Medida. "
            "Capturar una Clave de Unidad de Medida que exista en el Catálogo.",
        )
    # Bultos: cantidad + clave (Clarion Col L, M)
    clave_bultos = _get(row, "CLAVE BULTOS", "CLAVEBULTOS")
    cant_bultos = row.get("CANTIDAD BULTOS") or row.get("CANTIDADBULTOS")
    if clave_bultos:
        if valid_bulks_codes and clave_bultos not in valid_bulks_codes:
            return _err(
                line_num,
                "CLAVE BULTOS",
                f"Error: (Celda M{line_num}) La Clave de Bulto: {clave_bultos} no existe en el Catálogo de Claves de Bultos. "
                "Darlo de alta en el Catálogo de Claves de Bultos o capturar uno ya existente.",
            )
        cant_bultos_val = _parse_decimal(cant_bultos)
        if cant_bultos_val is None:
            return _err(
                line_num,
                "CANTIDAD BULTOS",
                f"Error: (Celda L{line_num}) La Cantidad de Bultos está vacía y en la Celda M{line_num} se tienen la Clave de Bulto: {clave_bultos}.",
            )
        if cant_bultos_val == 0:
            return _err(
                line_num,
                "CANTIDAD BULTOS",
                f"Error: (Celda L{line_num}) La Cantidad de Bultos es cero y en la Celda M{line_num} se tienen la Clave de Bulto: {clave_bultos}.",
            )
    else:
        cant_bultos_val = _parse_decimal(cant_bultos)
        if cant_bultos_val is not None and cant_bultos_val > 0:
            return _err(
                line_num,
                "CANTIDAD BULTOS",
                f"Error: (Celda L{line_num}) La Cantidad de Bultos es {cant_bultos} y en la Celda M{line_num} no se tienen la Clave de Bulto.",
            )
    # L: Se pagó impuesto SI/NO
    se_pago = _get(row, "SE PAGO IMPUESTO", "SE PAGO IMPUESTO? (SI o NO)", "SEPAGOIMPUESTO")
    if se_pago and se_pago.upper() not in SE_PAGO_IMPUESTO_VALIDOS:
        return _err(
            line_num,
            "SE PAGO IMPUESTO",
            f"Error: (Celda L{line_num}) El Valor Capturado para Se Pago Impuesto no es Válido. Capturar en la Celda L{line_num} SI o NO.",
        )
    # M: Forma de pago en catálogo
    forma_pago = _get(row, "FORMA DE PAGO", "FORMADEPAGO", "FORMA PAGO")
    if forma_pago and valid_payment_methods and forma_pago not in valid_payment_methods:
        return _err(
            line_num,
            "FORMA DE PAGO",
            f"Error: (Celda M{line_num}) La Forma de Pago Capturado no es Válido. "
            "Capturar en la Celda M una Forma de Pago dentro del Catálogo General de Formas de Pago.",
        )
    # Fracción americana (Clarion Col R) — SITAR fracciones-usa
    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(
            line_num,
            "FRACCION AMERICANA",
            f"Error: (Celda R{line_num}) La Fracción Americana: {frac_ame} no está en el catálogo SITAR (fracciones USA). "
            "Capturar en la Celda R una fracción válida según SITAR.",
        )
    # Orden de compra / orden venta (Clarion Col S) máx 20
    orden = _get(row, "ORDEN DE COMPRA", "ORDENCOMPRA", "ORDEN DE VENTA")
    if orden and len(orden) > MAX_LEN_ORDEN_COMPRA:
        return _err(
            line_num,
            "ORDEN DE COMPRA",
            f"Error: (Celda S{line_num}) La Orden de Venta: {orden} supera la cantidad de caracteres permitidos. "
            "Capturar en la Celda S una orden de compra no mayor de 20 caracteres.",
        )
    # Número de parte (Clarion Col T): longitud y catálogo
    num_parte = _get(row, "NUM. PARTE", "NUMPARTE", "NUMERO PARTE", "NUM PARTE")
    if num_parte:
        if len(num_parte) > MAX_LEN_NUM_PARTE:
            return _err(
                line_num,
                "NUM. PARTE",
                f"Error: (Celda T{line_num}) El Número de Parte: {num_parte} supera la longitud de caracteres. "
                "Capturar en la Celda T el Número de Parte no mayor de 30 caracteres.",
            )
        if valid_part_numbers is not None and num_parte.upper() not in valid_part_numbers:
            return _err(
                line_num,
                "NUM. PARTE",
                f"Error: (Celda T{line_num}) El Número de Parte: {num_parte} no existe en el Catálogo de Partes. Darlo de alta en el Catálogo de Partes.",
            )
    # Decimales PZA
    if validar_decimales_pza and um and um.upper() 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(
                line_num,
                "CANTIDAD EXPORTADA/DESCARGAR",
                f"Error: (Celda G{line_num}) La Unidad de Medida es PZA, Por lo Tanto no es Válida la Captura de Decimales. Asignar una Cantidad sin decimales.",
            )
    return None


def validate_row_partidas_expo(
    row: Dict[str, Any],
    line_num: int,
    autonumerar: bool,
    actualizar: bool,
    levantar_subpartidas: 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_uom_codes: Set[str],
    valid_bulks_codes: Set[str],
    valid_payment_methods: Set[str],
    valid_fraction_ame: Set[str],
    valid_part_numbers: Optional[Set[str]],
    factura_impo_tem_by_number: Dict[str, int],
    factura_impo_def_by_number: Dict[str, int],
    line_exists_tem: Set[Tuple[int, str]],
    line_exists_def: Set[Tuple[int, str]],
    rfc_exception_egm: bool = False,
    validar_decimales_pza: bool = False,
) -> List[Dict[str, Any]]:
    """
    Valida una fila de CSV de Partidas de Exportación Definitiva (o Cambio de Régimen).
    Clarion: VALIDA_TODA_PAR_EXPO vs VALIDA_PARCIAL_PAR_EXPO según autonumerar, actualizar y si la partida existe.
    """
    errors: List[Dict[str, Any]] = []

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

    factura_expo = _get(row, "NUMERO FACTURA EXPO", "NUMERO FACTURA EXPO.", "NUM FACTURA EXPO", "FACTURA EXPO")
    if not factura_expo:
        errors.append(_err(line_num, "NUMERO FACTURA EXPO", "Requerido"))
        return errors

    for err in [
        _check_factura_expo_existe(factura_expo, line_num, invoice_id_by_number),
        _check_factura_expo_no_actualizada(factura_expo, line_num, invoice_processed_by_number, rfc_exception_updated),
        _check_linea_expo_si_no_autonumerar(row, line_num, autonumerar),
        _check_levantar_subpartidas_expo(row, line_num, levantar_subpartidas, rfc_exception_egm),
    ]:
        if err:
            errors.append(err)

    if errors:
        return errors

    linea_expo = _get(row, "LINEA EXPO", "LINEA EXPO.", "RENGLON EXPO")
    existing_lines = existing_line_keys_by_invoice.get(factura_expo.strip(), set())
    partida_existe = bool(linea_expo and linea_expo in existing_lines)
    use_partial = actualizar and not autonumerar and partida_existe

    _expo_kwargs = dict(
        valid_uom_codes=valid_uom_codes,
        valid_bulks_codes=valid_bulks_codes,
        valid_payment_methods=valid_payment_methods,
        valid_fraction_ame=valid_fraction_ame,
        valid_part_numbers=valid_part_numbers,
        factura_impo_tem_by_number=factura_impo_tem_by_number,
        factura_impo_def_by_number=factura_impo_def_by_number,
        line_exists_tem=line_exists_tem,
        line_exists_def=line_exists_def,
        validar_decimales_pza=validar_decimales_pza,
    )

    if use_partial:
        err = _validaciones_par_expo(row, line_num, factura_expo, **_expo_kwargs)
        if err:
            errors.append(err)
    else:
        err = _valida_toda_obligatorios_expo(row, line_num, levantar_subpartidas, rfc_exception_egm)
        if err:
            errors.append(err)
        if levantar_subpartidas:
            for err in [
                _valida_subpartidas_duplicados_expo(factura_expo, linea_expo or "", line_num, line_counts_csv),
                _valida_subpartida_tiene_principal_expo(row, line_num, factura_expo, partidas_principales_csv, partidas_principales_bd, rfc_exception_egm),
                _valida_subpartida_linea_principal_no_cero_expo(row, line_num, rfc_exception_egm),
            ]:
                if err:
                    errors.append(err)
        err = _validaciones_par_expo(row, line_num, factura_expo, **_expo_kwargs)
        if err:
            errors.append(err)
    return errors
