"""
Validaciones CSV para Series de Exportación Definitiva.
Paridad Clarion: VALIDA_TODA_SERIES_EXPO, VALIDA_PARCIAL_SERIES_EXPO, LLENA_SERIES_EXPO.
Estructura: NUMERO FACTURA, LINEA FACTURA, LINEA SERIE, SERIE, MODELO, NUM PARTE, SUB MODELO, NUMERO ID.
Reutiliza helpers de series_impo_temp y series_impo_def; factura = exportación (FAC_EXPO), rfc_exception_updated.
"""
from typing import Dict, Any, Optional, Set, Tuple, List

from .series_impo_temp import (
    _clip,
    normalize_sacarcomasenters,
    _check_desfase,
    _check_linea_factura_vacia,
    _check_linea_serie_si_no_autonumerar,
    _warn_apostrofes,
    _check_max_length,
    MAX_LEN,
)
from .series_impo_def import (
    _check_partida_existe_en_factura,
    _check_cantidad_series_vs_partida,
    _get_val_def,
)


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


def _check_factura_existe_expo(
    invoice_number: str,
    line_num: int,
    invoice_id_by_number: Dict[str, int],
) -> Optional[Dict[str, Any]]:
    """Factura debe existir en BD (catálogo exportación)."""
    if invoice_number not in invoice_id_by_number:
        return {
            "line": line_num,
            "col": "NUMERO FACTURA",
            "msg": (
                f"Error: (Celda A{line_num}) La Factura de Exportación {invoice_number} "
                f"no existe en SCAII y no se pueden hacer las validaciones. "
            ),
            "solution": (
                f"Capturar en la Celda A{line_num} un número de Factura existente "
                f"al cual desee agregar o actualizar series"
            ),
            "identifier": "FAC_EXPO",
            "fields": invoice_number,
        }
    return None


def _check_factura_no_actualizada_expo(
    invoice_number: str,
    line_num: int,
    invoice_processed_by_number: Dict[str, bool],
    rfc_exception_updated: Set[str],
) -> Optional[Dict[str, Any]]:
    """Si factura ya actualizada (Estatus AC) no se pueden hacer cambios; excepción por RFC (ej. EGM0303257J1)."""
    if invoice_number in rfc_exception_updated:
        return None
    if invoice_processed_by_number.get(invoice_number, False):
        return {
            "line": line_num,
            "col": "NUMERO FACTURA",
            "msg": (
                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."
            ),
            "solution": "Capturar otro número de Factura de Exportación o Desactualizar la factura.",
            "identifier": "FAC_EXPO",
        }
    return None


def valida_toda_series_expo(
    row: Dict[str, Any],
    line_num: int,
    validar_series_exception: bool,
    warnings: Optional[List[Dict[str, Any]]],
) -> Optional[Dict[str, Any]]:
    """
    VALIDA_TODA_SERIES_EXPO: cuando no existe la serie o autonumerar=SI.
    Si D+E+F están todos vacíos y no aplica excepción ValidarSeries → error obligatorios.
    Valida longitudes máximas.
    """
    d = _get_val_def(row, "SERIE")
    e = _get_val_def(row, "MODELO")
    f = _get_val_def(row, "NUM PARTE")
    campos = d + e + f

    if not campos and not validar_series_exception:
        obligatorios = []
        if not d:
            obligatorios.append("(Col.D) Serie")
        if not e:
            obligatorios.append("(Col.E) Modelo")
        if not f:
            obligatorios.append("(Col.F) Num. Parte")
        if obligatorios:
            return {
                "line": line_num,
                "col": "SERIE",
                "msg": (
                    f"Existen campos vacíos que son obligatorios al no tener ningun campo, "
                    f"es la {', '.join(obligatorios)}."
                ),
                "solution": "Revisar la línea del archivo y capturar los campos con la información correcta.",
                "identifier": "ARCHIVO CSV",
            }

    for col, key, max_len in [
        ("SERIE", "SERIE", MAX_LEN["serial_numbers"]),
        ("MODELO", "MODELO", MAX_LEN["model"]),
        ("NUM PARTE", "NUM PARTE", 50),
        ("SUB MODELO", "SUB MODELO", MAX_LEN["sub_model"]),
        ("NUMERO ID", "NUMERO ID", MAX_LEN["number_id"]),
    ]:
        val = _get_val_def(row, key)
        if val:
            err = _check_max_length(col, val, line_num, max_len)
            if err:
                return err
    return None


def valida_parcial_series_expo(
    row: Dict[str, Any],
    line_num: int,
    existing_series_data: Dict[str, Any],
    warnings: Optional[List[Dict[str, Any]]],
) -> Optional[Dict[str, Any]]:
    """
    VALIDA_PARCIAL_SERIES_EXPO: actualizar serie existente; campos vacíos se rellenan con existente.
    Solo validar longitudes en campos no vacíos.
    """
    for col, key, max_len in [
        ("SERIE", "SERIE", MAX_LEN["serial_numbers"]),
        ("MODELO", "MODELO", MAX_LEN["model"]),
        ("NUM PARTE", "NUM PARTE", 50),
        ("SUB MODELO", "SUB MODELO", MAX_LEN["sub_model"]),
        ("NUMERO ID", "NUMERO ID", MAX_LEN["number_id"]),
    ]:
        val = _get_val_def(row, key)
        if val:
            err = _check_max_length(col, val, line_num, max_len)
            if err:
                return err
    return None


def _series_key(invoice_number: str, linea_factura: str, linea_serie: str) -> Tuple[str, str, str]:
    return (invoice_number.strip(), _clip(linea_factura), _clip(linea_serie))


def validate_row_series_expo(
    row: Dict[str, Any],
    line_num: int,
    actualizar: bool,
    autonumerar: bool,
    validar_series_exception: bool,
    invoice_id_by_number: Dict[str, int],
    invoice_processed_by_number: Dict[str, bool],
    rfc_exception_updated: Set[str],
    partida_max_series: Dict[Tuple[str, str], int],
    csv_series_count_so_far: Dict[Tuple[str, str], int],
    existing_series_keys: Set[Tuple[str, str, str]],
    existing_series_data: Optional[Dict[Tuple[str, str, str], Dict[str, Any]]],
    warnings: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
    """
    Punto de entrada: valida una fila de CSV de Series de Exportación Definitiva.
    Clarion: decisión VALIDA_TODA vs VALIDA_PARCIAL según autonumerar, actualizar y si la serie existe.
    rfc_exception_updated: set de números de factura que se consideran no actualizadas (ej. EGM0303257J1).
    """
    errors: List[Dict[str, Any]] = []

    desfase = _check_desfase(row, line_num)
    if desfase and warnings is not None:
        warnings.append(desfase)

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

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

    linea_factura = _clip(row.get("LINEA FACTURA") or row.get("LINEA") or row.get("PARTIDA"))

    for err in [
        _check_factura_existe_expo(invoice_number, line_num, invoice_id_by_number),
        _check_factura_no_actualizada_expo(invoice_number, line_num, invoice_processed_by_number, rfc_exception_updated),
        _check_linea_factura_vacia(row, line_num),
        _check_partida_existe_en_factura(invoice_number, linea_factura, line_num, partida_max_series),
        _check_cantidad_series_vs_partida(invoice_number, linea_factura, line_num, partida_max_series, csv_series_count_so_far),
        _check_linea_serie_si_no_autonumerar(row, line_num, autonumerar),
    ]:
        if err:
            errors.append(err)

    if errors:
        return errors

    _warn_apostrofes(row, line_num, warnings)

    linea_serie = _clip(row.get("LINEA SERIE") or row.get("RENGLON"))
    key = _series_key(invoice_number, linea_factura, linea_serie)

    use_partial = (
        actualizar
        and not autonumerar
        and bool(linea_serie)
        and key in (existing_series_keys or set())
    )

    if use_partial and existing_series_data and key in existing_series_data:
        err = valida_parcial_series_expo(row, line_num, existing_series_data[key], warnings)
    else:
        err = valida_toda_series_expo(row, line_num, validar_series_exception, warnings)

    if err:
        errors.append(err)
    return errors


def row_to_series_normalized_expo(row: Dict[str, Any]) -> Dict[str, Any]:
    """
    Normaliza fila para guardar: SACARCOMASENTERS en D, E, F, G, H.
    Clarion LLENA_SERIES_EXPO: asigna QueCSV a SerExpo.
    NUM PARTE se valida pero el modelo Serie no tiene campo parte.
    """
    def clip(col: str, alt: Optional[List[str]] = None) -> str:
        v = row.get(col)
        if alt:
            for k in alt:
                if v is None or (isinstance(v, str) and not v.strip()):
                    v = row.get(k)
        return _clip(v) if v is not None else ""

    def norm(col: str, alt: Optional[List[str]] = None, max_len: int = 50) -> str:
        v = row.get(col)
        if alt:
            for k in alt:
                if v is None or (isinstance(v, str) and not v.strip()):
                    v = row.get(k)
        s = normalize_sacarcomasenters(v) if v is not None else ""
        return s[:max_len] if s else ""

    return {
        "NUMERO FACTURA": clip("NUMERO FACTURA", ["NUM FACTURA", "FACTURA", "FACTURA EXPO"]),
        "LINEA FACTURA": clip("LINEA FACTURA", ["LINEA", "PARTIDA"]),
        "LINEA SERIE": clip("LINEA SERIE", ["RENGLON"]),
        "SERIE": norm("SERIE", max_len=MAX_LEN["serial_numbers"]),
        "MODELO": norm("MODELO", max_len=MAX_LEN["model"]),
        "NUM PARTE": norm("NUM PARTE", ["NUMPARTE", "NUMERO PARTE"]),
        "SUB MODELO": norm("SUB MODELO", ["SUBMODELO"], MAX_LEN["sub_model"]),
        "NUMERO ID": norm("NUMERO ID", ["NUMEROID"], MAX_LEN["number_id"]),
    }
