"""
Validaciones comunes de fila para import CSV de partes.
Paridad Clarion: VALIDA_TODA_PARTES (obligatorios A, B, E salvo excepción RFC), VALIDACIONES_PARTES.
"""
from typing import Dict, Any, Optional, Set, List

from ..common.common_validators import check_max_length, check_decimal
from decimal import Decimal, InvalidOperation


MSG_NUMPARTE_VACIO = (
    "Error: (Col. A) La columna de Número de Parte esta vacio y no se pueden hacer las validaciones. "
    "Capturar en la Columna A un Número de Parte nuevo o una ya existente a la cual desee actualizar campos"
)
NUMPARTE_MAX_LEN_CLARION = 30
NUMPARTE_MAX_LEN_DB = 70


def validate_row_required(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col A (NUMPARTE) obligatorio; mensaje Clarion si está vacío."""
    val = (row.get("NUMPARTE") or "").strip()
    if not val:
        return {"line": line_num, "col": "NUMPARTE", "msg": MSG_NUMPARTE_VACIO}
    if len(val) > NUMPARTE_MAX_LEN_DB:
        return {
            "line": line_num,
            "col": "NUMPARTE",
            "msg": f"Error: (Col. A) El Número de Parte: {val} supera la longitud de caracteres. "
            f"Capturar en la columna A un Número de Parte de {NUMPARTE_MAX_LEN_DB} caracteres como máximo.",
        }
    return None


def validate_row_required_full(
    row: Dict[str, Any],
    line_num: int,
    is_rfc_exception: bool = False,
) -> Optional[Dict[str, Any]]:
    """Obligatorios en validación completa: A, B; y E (U.M. Comercial) salvo excepción RFC con D vacía."""
    err = validate_row_required(row, line_num)
    if err:
        return err
    if not (row.get("DESCRIPCIONE") or "").strip():
        return {
            "line": line_num,
            "col": "DESCRIPCIONE",
            "msg": "Existen campos vacios que son obligatorios, es la (Col.B) Descripción Español. "
            "Revisar la línea del archivo y capturar los campos con la información correcta.",
        }
    # E obligatorio salvo que empresa sea excepción RFC y Col D (Clase) esté vacía
    col_d = (row.get("CLASE") or "").strip()
    if not is_rfc_exception or col_d:
        if not (row.get("UNIMED") or "").strip():
            return {
                "line": line_num,
                "col": "UNIMED",
                "msg": "Existen campos vacios que son obligatorios, es la (Col.E) U.M. Comercial. "
                "Revisar la línea del archivo y capturar los campos con la información correcta.",
            }
    return None


def validate_row_required_act_part_exists(
    row: Dict[str, Any],
    line_num: int,
    existing_part_numbers: Set[str],
) -> Optional[Dict[str, Any]]:
    """Cuando actualizar=True y se hace validación full: el número de parte debe existir en catálogo."""
    num_parte = (row.get("NUMPARTE") or "").strip().upper()
    if not num_parte:
        return None
    if num_parte not in existing_part_numbers:
        return {
            "line": line_num,
            "col": "NUMPARTE",
            "msg": "(Col.A) El número de parte no existe.",
        }
    return None


def validate_row_lengths(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Longitudes máximas (Clarion: Col A 30; resto según modelo)."""
    err = check_max_length(row, "NUMPARTE", NUMPARTE_MAX_LEN_DB, line_num)
    if err:
        return err
    checks = [
        ("DESCRIPCIONE", 500),
        ("DESCRIPCIONI", 500),
        ("CLASE", 8),
        ("UNIMED", 5),
        ("TIPOMONEDA", 3),
        ("CLAVEMONEDA", 3),
        ("FRACCION", 10),
        ("PAIS", 3),
        ("PREFERENCIA", 7),
        ("SECTOR", 5),
        ("RUTAIMAGEN", 255),
        ("TIPOPESO", 6),
    ]
    for col, max_len in checks:
        err = check_max_length(row, col, max_len, line_num)
        if err:
            return err
    return None


def validate_row_types(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Tipos numéricos (decimales) de la fila."""
    err = check_decimal(row, "COSTOUNIT", line_num)
    if err:
        return err
    # No negativos
    raw = (row.get("COSTOUNIT") or "").strip()
    if raw:
        try:
            if Decimal(raw) < 0:
                return {
                    "line": line_num,
                    "col": "COSTOUNIT",
                    "msg": "Error: (Col. I) El Costo Unitario no puede ser negativo.",
                }
        except (InvalidOperation, ValueError):
            # check_decimal already handles format; ignore here
            pass
    err = check_decimal(row, "PESOUNIT", line_num)
    if err:
        return err
    raw = (row.get("PESOUNIT") or "").strip()
    if raw:
        try:
            if Decimal(raw) < 0:
                return {
                    "line": line_num,
                    "col": "PESOUNIT",
                    "msg": "Error: (Col. K) El Peso Unitario no puede ser negativo.",
                }
        except (InvalidOperation, ValueError):
            pass
    return None


def validate_row_class_fk(
    row: Dict[str, Any],
    line_num: int,
    valid_class_codes: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col D (Clase): si no vacía, debe existir en catálogo de clases."""
    val = (row.get("CLASE") or "").strip()
    if not val or valid_class_codes is None:
        return None
    if val.upper() in valid_class_codes:
        return None
    return {
        "line": line_num,
        "col": "CLASE",
        "msg": f"Error: (Col. D) La Clase: {val} no existe en el Catálogo de Clases de Activo Fijo. "
        "Dar de alta la Clase en el Catálogo de Clases de Activo Fijo.",
    }


def validate_row_uom_fk(
    row: Dict[str, Any],
    line_num: int,
    valid_uom_codes: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col E (U.M. Comercial): si no vacía, debe existir en catálogo UOM."""
    val = (row.get("UNIMED") or "").strip()
    if not val or valid_uom_codes is None:
        return None
    if val.upper() in valid_uom_codes:
        return None
    return {
        "line": line_num,
        "col": "UNIMED",
        "msg": f"Error: (Col. E) La Unidad de Medida Comercial: {val} no existe en el Catálogo de U.M. "
        "Revisar esta Unidad de Medida en el archivo, en caso de ser correcta dar la de alta en el Catálogo de U.M.",
    }


def validate_row_tipo_moneda(
    row: Dict[str, Any],
    line_num: int,
) -> Optional[Dict[str, Any]]:
    """Col G (Tipo Moneda): solo ME, MN o MC (o vacío)."""
    val = (row.get("TIPOMONEDA") or "").strip().upper()
    if not val:
        return None
    if val in ("ME", "MN", "MC"):
        return None
    return {
        "line": line_num,
        "col": "TIPOMONEDA",
        "msg": f"Error: (Col. G) La opción de Tipo Moneda: {val} no es valida. "
        "Capturar una opción valida: ME para Dolares, MN para Pesos, MC para Moneda de Captura o dejar el campo vacio.",
    }


def validate_row_clave_moneda(
    row: Dict[str, Any],
    line_num: int,
    valid_currency_codes: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col H (Clave Moneda): si G = MC, H obligatoria y debe existir en catálogo."""
    g = (row.get("TIPOMONEDA") or "").strip().upper()
    if g != "MC":
        return None
    val = (row.get("CLAVEMONEDA") or "").strip()
    if not val:
        return {
            "line": line_num,
            "col": "CLAVEMONEDA",
            "msg": "Error: (Col. H) La Clave de la Moneda es obligatoria cuando Tipo Moneda es MC.",
        }
    if valid_currency_codes is not None and val.upper() not in valid_currency_codes:
        return {
            "line": line_num,
            "col": "CLAVEMONEDA",
            "msg": f"Error: (Col. H) La Clave de la Moneda: {val} no existe en el Catálogo de Claves de Moneda. "
            "Revisar esta clave de la Moneda en el archivo, en caso de ser correcta Actualizar los Catálogos Fijos.",
        }
    return None


def validate_row_tipo_peso(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col L (Tipo Peso): solo KILOS o LIBRAS (o vacío)."""
    val = (row.get("TIPOPESO") or "").strip()
    if not val:
        return None
    if val.upper() in ("KILOS", "LIBRAS"):
        return None
    return {
        "line": line_num,
        "col": "TIPOPESO",
        "msg": f"Error: (Col. L) La opción del Tipo de Peso {val} no es valida. "
        "Capturar una opción valida: KILOS, LIBRAS o dejar el campo vacio y automaticamente se asigna KILOS.",
    }


def _normalize_fraction_mex_8(value: str) -> str:
    """Primeros 8 caracteres si len>=10, sino hasta 8 (Clarion SUB)."""
    if not value:
        return ""
    v = value.strip()
    if len(v) >= 10:
        return v[:8]
    return v[:8] if len(v) > 8 else v


def validate_row_fraction_mex_catalog(
    row: Dict[str, Any],
    line_num: int,
    valid_fraction_mex_8: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col M (Fracción Mex): si no vacía, debe existir en catálogo Mex o histórico."""
    val = (row.get("FRACCION") or "").strip()
    if not val or valid_fraction_mex_8 is None:
        return None
    code_8 = _normalize_fraction_mex_8(val)
    if not code_8:
        return None
    if code_8 in valid_fraction_mex_8:
        return None
    return {
        "line": line_num,
        "col": "FRACCION",
        "msg": f"Error: (Col. M) La Fraccion Mexicana: {val} no existe en el Catálogo de Fracciones Arancelarias Sifr@ ni en el Historico. "
        "Revisar esta Fracción Arancelaria en el archivo, en caso de ser correcta Actualizar las Fracciones Arancelarias.",
    }


def validate_row_country_fk(
    row: Dict[str, Any],
    line_num: int,
    valid_country_m3: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col N (País): si no vacía, debe existir en catálogo de países (m3_key)."""
    val = (row.get("PAIS") or "").strip()
    if not val or valid_country_m3 is None:
        return None
    if val.upper() in valid_country_m3:
        return None
    return {
        "line": line_num,
        "col": "PAIS",
        "msg": f"Error: (Col. N) La Clave del País: {val} no existe en el Catálogo de Paises. Dar lo de alta en el Catálogo de Paises.",
    }


def validate_row_preferencia(
    row: Dict[str, Any],
    line_num: int,
    is_rfc_exception: bool,
) -> Optional[Dict[str, Any]]:
    """Col O (Preferencia): solo GENERAL, TLCS, PROSEC o ALADI. Excepción RFC: solo aplica si D no vacía."""
    col_d = (row.get("CLASE") or "").strip()
    if is_rfc_exception and not col_d:
        return None
    val = (row.get("PREFERENCIA") or "").strip().upper()
    if not val:
        return None
    if val in ("GENERAL", "TLCS", "PROSEC", "ALADI"):
        return None
    return {
        "line": line_num,
        "col": "PREFERENCIA",
        "msg": "Error: (Col. O) La opción de Preferencia Arancelaria no es valida. "
        "Capturar una opción valida: GENERAL, TLCS, PROSEC, ALADI.",
    }


def validate_row_sector(
    row: Dict[str, Any],
    line_num: int,
    company_has_prosec: bool,
    authorized_sector_keys: Optional[Set[str]],
) -> Optional[Dict[str, Any]]:
    """Col P (Sector): si O=PROSEC entonces P obligatorio, empresa PROSEC, sector autorizado; si O≠PROSEC y P no vacío error."""
    pref = (row.get("PREFERENCIA") or "").strip().upper()
    sector = (row.get("SECTOR") or "").strip()
    # Formato: solo A-Z/0-9 (sin espacios ni especiales), hasta 8
    if sector:
        s = sector.strip().upper()
        if not (1 <= len(s) <= 8) or not s.isalnum():
            return {
                "line": line_num,
                "col": "SECTOR",
                "msg": "Error: (Col. P) El Sector contiene caracteres no permitidos. Use solo letras y números sin espacios (máx. 8 caracteres).",
            }
    if pref == "PROSEC":
        if not sector:
            return {
                "line": line_num,
                "col": "SECTOR",
                "msg": "Error: (Col. P) no hay un sector capturado y se tiene la opción de Preferencia Arancelaria: PROSEC. Capturar un Sector valido en la columna P",
            }
        if not company_has_prosec:
            return {
                "line": line_num,
                "col": "SECTOR",
                "msg": f"Error: (Col. P) Se quiere aplicar el sector {sector} pero la empresa no cuenta con Permiso PROSEC. "
                "Marcar la opción que cuenta con un Permiso PROSEC en los Datos de la Empresa.",
            }
        if authorized_sector_keys is not None and sector.upper() not in authorized_sector_keys:
            return {
                "line": line_num,
                "col": "SECTOR",
                "msg": f"Error: (Col. P) El sector {sector} no esta promovido para esta Empresa. "
                "Capturar un Sector en la columna P que este promovido en los Datos de la Empresa o active el Sector en el Catálogo de Sectores.",
            }
    else:
        if sector:
            return {
                "line": line_num,
                "col": "SECTOR",
                "msg": f"Error: (Col. P) Hay un sector capturado y se tiene la opción de Preferencia Arancelaria: {pref or '(vacío)'}. "
                "Borra el sector en la columna P o cambiar la preferencia en la columna O.",
            }
    return None


def validaciones_parte(
    row: Dict[str, Any],
    line_num: int,
    valid_class_codes: Optional[Set[str]] = None,
    valid_uom_codes: Optional[Set[str]] = None,
    valid_currency_codes: Optional[Set[str]] = None,
    valid_fraction_mex_8: Optional[Set[str]] = None,
    valid_country_m3: Optional[Set[str]] = None,
    authorized_sector_keys: Optional[Set[str]] = None,
    company_has_prosec: bool = False,
    is_rfc_exception: bool = False,
) -> List[Dict[str, Any]]:
    """Reglas compartidas Clarion (VALIDACIONES_PARTES): longitudes, tipos, FKs, preferencia, sector."""
    errors: List[Dict[str, Any]] = []

    for err in [
        validate_row_lengths(row, line_num),
        validate_row_types(row, line_num),
        validate_row_class_fk(row, line_num, valid_class_codes),
        validate_row_uom_fk(row, line_num, valid_uom_codes),
        validate_row_tipo_moneda(row, line_num),
        validate_row_clave_moneda(row, line_num, valid_currency_codes),
        validate_row_tipo_peso(row, line_num),
        validate_row_fraction_mex_catalog(row, line_num, valid_fraction_mex_8),
        validate_row_country_fk(row, line_num, valid_country_m3),
        validate_row_preferencia(row, line_num, is_rfc_exception),
        validate_row_sector(row, line_num, company_has_prosec, authorized_sector_keys),
    ]:
        if err:
            errors.append(err)

    return errors


def validate_row_warnings(
    row: Dict[str, Any],
    line_num: int,
) -> list:
    """
    Devuelve lista de advertencias (no bloquean; no se añaden a error_lines).
    Clarion: desfase cuando Col Q tiene valor; apóstrofes en Número de Parte.
    """
    warnings: list = []
    # Advertencia desfase: Clarion cuando CSVArc:ColumnaQ <> ''
    if (row.get("RUTAIMAGEN") or "").strip():
        warnings.append({
            "line": line_num,
            "col": "RUTAIMAGEN",
            "msg": "Advertencia: Podría existir un desfase en esta línea.",
            "severity": "warning",
            "warning": True,
        })
    # Advertencia apóstrofes en Número de Parte
    num_parte = (row.get("NUMPARTE") or "").strip()
    if "'" in num_parte:
        warnings.append({
            "line": line_num,
            "col": "NUMPARTE",
            "msg": f"Advertencia: El Número de Parte: {num_parte} Contiene Apostrofes. Se Omitirá el Apostrofe para Subir el Número de Parte.",
            "severity": "warning",
            "warning": True,
        })
    return warnings
