"""
Validaciones CSV para Series de Importación Definitiva.
Paridad Clarion: VALIDA_TODA_SERIES_IMPO_DEF, VALIDA_PARCIAL_SERIES_IMPO_DEF, LLENA_SERIES_IMPO_DEF.
Estructura: NUMERO FACTURA, LINEA FACTURA, LINEA SERIE, SERIE, MODELO, NUM PARTE, SUB MODELO, NUMERO ID.
Reutiliza helpers de series_impo_temp; añade reglas DEF: factura en catálogo DEF, cantidad series vs partida.
"""
from typing import Dict, Any, Optional, Set, Tuple, List

from .series_impo_temp import (
    _clip,
    normalize_sacarcomasenters,
    _check_desfase,
    _check_factura_vacia,
    _check_linea_factura_vacia,
    _check_linea_serie_si_no_autonumerar,
    _warn_apostrofes,
    _check_max_length,
    MAX_LEN,
)


def _check_factura_existe_def(
    invoice_number: str,
    line_num: int,
    invoice_id_by_number: Dict[str, int],
    catalog_label: str = "Importación Definitiva",
) -> Optional[Dict[str, Any]]:
    """Factura debe existir en BD (Importación Definitiva o Compras Mexicanas según catalog_label)."""
    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 Importació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_IMPO_DEF",
            "fields": invoice_number,
        }
    return None


def _check_factura_no_actualizada_def(
    invoice_number: str,
    line_num: int,
    invoice_processed_by_number: Dict[str, bool],
    catalog_label: str = "Importación Definitiva",
) -> Optional[Dict[str, Any]]:
    """Si factura ya actualizada (Estatus AC) no se pueden hacer cambios. Mensaje según catalog_label."""
    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 {catalog_label}: {invoice_number} "
                "ya existe y está Actualizada, no se puede hacer cambios a las facturas actualizadas."
            ),
            "solution": (
                "Capturar otro número de Factura de Importación Temporal o Desactualizar la factura."
            ),
            "identifier": "FAC_IMPO_DEF",
        }
    return None


def _check_partida_existe_en_factura(
    invoice_number: str,
    linea_factura: str,
    line_num: int,
    partida_max_series: Dict[Tuple[str, str], int],
) -> Optional[Dict[str, Any]]:
    """
    Clarion: la partida (línea de factura) debe existir en la factura.
    partida_max_series tiene como claves (invoice_number, line_number) de las partidas existentes.
    """
    if not linea_factura:
        return None
    key = (invoice_number.strip(), _clip(linea_factura))
    if key in partida_max_series:
        return None
    return {
        "line": line_num,
        "col": "LINEA FACTURA",
        "msg": f"Partida línea {linea_factura} no existe en la factura.",
        "solution": "Usar un número de línea de partida que exista en la factura (capturar partidas antes de cargar series).",
        "identifier": "ARCHIVO CSV",
    }


def _check_cantidad_series_vs_partida(
    invoice_number: str,
    linea_factura: str,
    line_num: int,
    partida_max_series: Dict[Tuple[str, str], int],
    csv_series_count_so_far: Dict[Tuple[str, str], int],
) -> Optional[Dict[str, Any]]:
    """
    Clarion: si cantidad de series en CSV para (factura, partida) excede CantImpoDef → error.
    partida_max_series[(inv, line)] = máximo permitido (desde LineQuantity.quantity).
    csv_series_count_so_far = conteo actual de filas válidas ya procesadas por (inv, line).
    """
    key = (invoice_number.strip(), _clip(linea_factura))
    max_allowed = partida_max_series.get(key)
    if max_allowed is None or max_allowed <= 0:
        return None
    current = csv_series_count_so_far.get(key, 0)
    if current + 1 > max_allowed:
        return {
            "line": line_num,
            "col": "LINEA FACTURA",
            "msg": (
                f"Error: La cantidad de Series de la partida {_clip(linea_factura)} en la factura "
                f"{invoice_number} es menor al número de Series en el archivo. "
            ),
            "solution": "Nivelar la cantidad de la Partida o el número de Series.",
            "identifier": "ARCHIVO CSV",
        }
    return None


def _get_val_def(row: Dict[str, Any], k: str) -> str:
    """Obtener valor normalizado por clave (aliases DEF/TEM)."""
    if k == "NUM PARTE":
        return _clip(row.get("NUM PARTE") or row.get("NUMPARTE") or row.get("NUMERO PARTE"))
    if k == "SUB MODELO":
        return _clip(row.get("SUB MODELO") or row.get("SUBMODELO"))
    if k == "NUMERO ID":
        return _clip(row.get("NUMERO ID") or row.get("NUMEROID"))
    return _clip(row.get(k))


def valida_toda_series_impo_def(
    row: Dict[str, Any],
    line_num: int,
    validar_series_exception: bool,
    warnings: Optional[List[Dict[str, Any]]],
) -> Optional[Dict[str, Any]]:
    """
    VALIDA_TODA_SERIES_IMPO_DEF: 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_impo_def(
    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_IMPO_DEF: 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_impo_def(
    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],
    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,
    catalog_label: str = "Importación Definitiva",
) -> List[Dict[str, Any]]:
    """
    Punto de entrada: valida una fila de CSV de Series de Importación Definitiva.
    Clarion: decisión VALIDA_TODA vs VALIDA_PARCIAL según autonumerar, actualizar y si la serie existe.
    partida_max_series: máximo de series por (invoice_number, linea_factura); 0 o ausente = no validar.
    csv_series_count_so_far: conteo de filas ya aceptadas por (invoice_number, linea_factura); el caller debe incrementar al aceptar.
    """
    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(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"))
    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_def(invoice_number, line_num, invoice_id_by_number, catalog_label),
        _check_factura_no_actualizada_def(invoice_number, line_num, invoice_processed_by_number, catalog_label),
        _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_impo_def(row, line_num, existing_series_data[key], warnings)
    else:
        err = valida_toda_series_impo_def(row, line_num, validar_series_exception, warnings)

    if err:
        errors.append(err)
    return errors


def row_to_series_normalized_def(row: Dict[str, Any]) -> Dict[str, Any]:
    """
    Normaliza fila para guardar: SACARCOMASENTERS en D, E, F, G, H.
    Clarion LLENA_SERIES_IMPO_DEF: asigna QueCSV a SerDef.
    Incluye NUM PARTE para validación; el modelo Serie actual no tiene campo parte (documentar si se añade).
    """
    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"]),
        "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"]),
    }
