"""
Validaciones CSV para Series de Importación Temporal.
Paridad Clarion: VALIDA_TODA_SERIES_IMPO_TEM, VALIDA_PARCIAL_SERIES_IMPO_TEM, LLENA_SERIES_IMPO_TEM.
Estructura: NUMERO FACTURA, LINEA FACTURA, LINEA SERIE, SERIE, MODELO, NUM PARTE, SUB MODELO, NUMERO ID.
"""
from typing import Dict, Any, Optional, Set, Tuple, List

# Longitudes máximas según modelo Serie (item_line_series)
MAX_LEN = {
    "serial_numbers": 50,
    "model": 50,
    "sub_model": 50,
    "number_id": 25,
}

APOSTROFE = "'"


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


def normalize_sacarcomasenters(val: Any) -> str:
    """Clarion SACARCOMASENTERS: quitar comas y saltos de línea."""
    s = _clip(val)
    s = s.replace(",", " ").replace("\n", " ").replace("\r", " ")
    return " ".join(s.split())


def _check_desfase(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Si COL_EXTRA tiene valor → advertencia desfase (no bloqueante)."""
    val = _clip(row.get("COL_EXTRA"))
    if not val:
        return None
    return {
        "line": line_num,
        "col": "COL_EXTRA",
        "msg": "Advertencia: Podría existir un desfase en esta línea.",
        "solution": "Revisar esta línea del archivo CSV y verificar cada campo esté en la posición correcta.",
        "warning": True,
    }


def _check_factura_vacia(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """NUMERO FACTURA (A) vacío → error bloqueante."""
    val = _clip(row.get("NUMERO FACTURA") or row.get("NUM FACTURA") or row.get("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.",
            "solution": "Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar series.",
        }
    return None


def _check_factura_existe(
    invoice_number: str,
    line_num: int,
    invoice_id_by_number: Dict[str, int],
) -> Optional[Dict[str, Any]]:
    """Factura debe existir en BD (imp + TEM)."""
    if 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.",
            "solution": "Capturar en la Celda A un número de Factura existente al cual desee agregar o actualizar partidas.",
            "identifier": "FAC_IMPO_TEM",
            "fields": invoice_number,
        }
    return None


def _check_factura_no_actualizada(
    invoice_number: str,
    line_num: int,
    invoice_processed_by_number: Dict[str, bool],
) -> Optional[Dict[str, Any]]:
    """Si factura ya actualizada (Estatus AC) no se pueden hacer cambios."""
    if invoice_processed_by_number.get(invoice_number, 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.",
            "solution": "Capturar otro número de Factura de Importación Temporal o Desactualizar la factura.",
            "identifier": "FAC_IMPO_TEM",
        }
    return None


def _check_linea_factura_vacia(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """LINEA FACTURA (B) vacía → error."""
    val = _clip(row.get("LINEA FACTURA") or row.get("LINEA") or row.get("PARTIDA"))
    if not val:
        return {
            "line": line_num,
            "col": "LINEA FACTURA",
            "msg": "Error: (Celda B) El campo de la línea de la partida está vacío y no se pueden hacer las validaciones.",
            "solution": "Capturar en la Celda B la línea de la partida al cual desee agregar o actualizar información.",
        }
    return None


def _check_linea_serie_si_no_autonumerar(
    row: Dict[str, Any],
    line_num: int,
    autonumerar: bool,
) -> Optional[Dict[str, Any]]:
    """Si Autonumerar = NO, LINEA SERIE (C) es obligatoria."""
    if autonumerar:
        return None
    val = _clip(row.get("LINEA SERIE") or row.get("RENGLON"))
    if not val:
        return {
            "line": line_num,
            "col": "LINEA SERIE",
            "msg": "Error: (Celda C) El campo de Renglón está vacío y no se pueden hacer las validaciones, ya que se tiene la opción de Autonumerar como NO.",
            "solution": "Capturar en la Celda C el renglón de la serie la cual desee agregar o actualizar información.",
        }
    return None


def _warn_apostrofes(
    row: Dict[str, Any],
    line_num: int,
    warnings: Optional[List[Dict[str, Any]]],
) -> None:
    """Advertencias si SERIE, MODELO o NUM PARTE contienen apostrofe (no bloqueante)."""
    if warnings is None:
        return
    checks = [
        ("SERIE", "Número de Serie", row.get("SERIE")),
        ("MODELO", "Número de Modelo", row.get("MODELO")),
        ("NUM PARTE", "Número de Parte", row.get("NUM PARTE") or row.get("NUMPARTE") or row.get("NUMERO PARTE")),
    ]
    for col, label, val in checks:
        v = _clip(val) if val is not None else ""
        if v and APOSTROFE in v:
            warnings.append({
                "line": line_num,
                "col": col,
                "msg": f"Advertencia: El {label}: {v} Contiene Apostrofes.",
                "solution": "Se Omitirá el Apostrofe para Subir.",
                "warning": True,
            })


def _check_max_length(col: str, val: str, line_num: int, max_len: int) -> Optional[Dict[str, Any]]:
    if not val or len(val) <= max_len:
        return None
    return {
        "line": line_num,
        "col": col,
        "msg": f"Máximo {max_len} caracteres",
    }


def valida_toda_series_impo_tem(
    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_TEM: cuando no existe la partida/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 = _clip(row.get("SERIE"))
    e = _clip(row.get("MODELO"))
    f = _clip(row.get("NUM PARTE") or row.get("NUMPARTE") or row.get("NUMERO 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: {', '.join(obligatorios)}.",
                "solution": "Revisar la línea del archivo y capturar los campos con la información correcta.",
            }

    # Longitudes (solo si hay valor)
    def get_val(k: str) -> str:
        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))

    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(key)
        if val:
            err = _check_max_length(col, val, line_num, max_len)
            if err:
                return err

    return None


def valida_parcial_series_impo_tem(
    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_TEM: actualizar serie existente; campos vacíos se rellenan con existente.
    Solo validar longitudes en campos no vacíos.
    """
    def get_val(k: str) -> str:
        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))

    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(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_temp(
    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],
    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 Importación Temporal.
    Clarion: decisión VALIDA_TODA vs VALIDA_PARCIAL según autonumerar, actualizar y si la serie existe.
    """
    errors: List[Dict[str, Any]] = []

    # Desfase (solo advertencia, no bloqueante)
    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

    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),
        _check_linea_factura_vacia(row, line_num),
        _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_factura = _clip(row.get("LINEA FACTURA") or row.get("LINEA") or row.get("PARTIDA"))
    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_tem(row, line_num, existing_series_data[key], warnings)
    else:
        err = valida_toda_series_impo_tem(row, line_num, validar_series_exception, warnings)

    if err:
        errors.append(err)
    return errors


def row_to_series_normalized(row: Dict[str, Any]) -> Dict[str, Any]:
    """
    Normaliza fila para guardar: SACARCOMASENTERS en D, E, F, G, H.
    Clarion LLENA_SERIES_IMPO_TEM: asigna QueCSV a SerImp.
    """
    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"]),
    }
