"""
Validadores reutilizables para import CSV de agentes aduanales (customs brokers).
Paridad Clarion: VALIDACIONES_AGENTE_ADUANAL, tipo MEX/AME, patente obligatoria si MEX,
país en catálogo, RFC/CURP máx, desfase.
"""
import re
from typing import Dict, Any, Optional, Set

BROKER_KEY_MAX = 5
LICENSE_MAX = 4
RFC_MAX = 30
CURP_MAX = 19
PHONE_MIN_DIGITS = 10
PHONE_MAX_DIGITS = 15

# Formato RFC/CURP (paridad con flujo manual dashboard/customs_brokers)
RFC_PATTERN = re.compile(r"^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$", re.IGNORECASE)
# Email (paridad con flujo manual via DTO)
EMAIL_PATTERN = re.compile(
    r"^$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
)
# CURP: 18 caracteres (inicial, vocal, 2 letras, fecha, sexo, estado, 3 consonantes, homoclave)
CURP_PATTERN = re.compile(
    r"^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM]"
    r"(AS|BC|BS|CC|CS|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)"
    r"[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]\d$",
    re.IGNORECASE,
)


def check_required_broker_key(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col B: Clave de Agente Aduanal obligatoria, máx 5 caracteres."""
    clave = (row.get("CLAVE") or "").strip()
    if not clave:
        return {
            "line": line_num,
            "col": "CLAVE",
            "msg": f"Error: (Celda B{line_num}) La Clave de Agente Aduanal está vacía y no se pueden hacer las validaciones.",
            "solution": f"Capturar en la Celda B{line_num} una Clave de Agente Aduanal nueva o existente a la cual desee agregar, remplazar o actualizar campos",
        }
    if len(clave) > BROKER_KEY_MAX:
        return {
            "line": line_num,
            "col": "CLAVE",
            "msg": f"Error: (Col. B) La Clave de Agente Aduanal: {clave} supera la longitud de caracteres.",
            "solution": f"Capturar en la columna B una Clave de Agente Aduanal de {BROKER_KEY_MAX} caracteres como máximo.",
        }
    if not re.match(r"^[a-zA-Z0-9]+$", clave):
        return {
            "line": line_num,
            "col": "CLAVE",
            "msg": "Error: (Col. B) La Clave de Agente Aduanal solo puede contener letras y números.",
            "solution": "Capturar en la columna B una Clave alfanumérica.",
        }
    return None


def check_optional_license(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col C: Patente/Licencia opcional; si tiene valor, máx 4 dígitos."""
    licencia = (row.get("LICENCIA") or "").strip()
    if not licencia:
        return None
    if len(licencia) > LICENSE_MAX or not licencia.isdigit():
        return {
            "line": line_num,
            "col": "LICENCIA",
            "msg": f"Error: (Col. C) La Patente debe ser de hasta {LICENSE_MAX} dígitos numéricos.",
            "solution": f"Capturar en la columna C una Patente de hasta {LICENSE_MAX} dígitos.",
        }
    return None


def check_desfase(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Si COL_EXTRA (Col O) tiene valor → advertencia de desfase."""
    val = (row.get("COL_EXTRA") or "").strip()
    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 parse_tipo_agente_aduanal(val: Optional[str]) -> Optional[str]:
    """
    Parsea TIPO (Col A): letra (M/A) o palabra (MEX/Mexicano, AME/Americano).
    Devuelve 'M' o 'A'; si no reconoce, None.
    """
    if not val or not str(val).strip():
        return None
    s = str(val).strip()
    v = s.upper()
    if len(v) == 1:
        if v == "M":
            return "M"
        if v == "A":
            return "A"
    v_lower = s.lower()
    if v_lower in ("mex", "mexicano"):
        return "M"
    if v_lower in ("ame", "americano"):
        return "A"
    return None


def check_tipo_mex_ame(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """
    Col A: TIPO debe ser MEX o AME (flexible: M/MEX/Mexicano, A/AME/Americano).
    Si TIPO=M entonces PAIS (Col J) debe ser MEX; si TIPO=A entonces PAIS no debe ser MEX.
    """
    tipo_raw = (row.get("TIPO") or "").strip()
    if not tipo_raw:
        return None
    tipo = parse_tipo_agente_aduanal(tipo_raw)
    if tipo is None:
        return {
            "line": line_num,
            "col": "TIPO",
            "msg": f"Error: (Col. A) El tipo de agente aduanal: {tipo_raw} es incorrecto.",
            "solution": "Capturar en columna A el Tipo de agente aduanal correcto, MEX para Mexicano y AME para Americano.",
        }
    pais = (row.get("PAIS") or "").strip().upper()
    if not pais:
        return None
    if tipo == "M" and pais != "MEX":
        return {
            "line": line_num,
            "col": "PAIS",
            "msg": f"Error: (Col. J) El tipo de cliente: {tipo_raw} tiene el país {pais}, es incorrecto.",
            "solution": "Capturar en la columna J el país con clave MEX, ya que es un Agente Aduanal Mexicano",
        }
    if tipo == "A" and pais == "MEX":
        return {
            "line": line_num,
            "col": "PAIS",
            "msg": f"Error: (Col. J) El tipo de cliente: {tipo_raw} tiene el país {pais}, es incorrecto.",
            "solution": "Capturar en la columna J un país con clave diferente de MEX, ya que es un Agente Aduanal Americano.",
        }
    return None


def check_patente_obligatoria_si_mex(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col C: Patente obligatoria si TIPO es MEX."""
    tipo = parse_tipo_agente_aduanal((row.get("TIPO") or "").strip())
    if tipo != "M":
        return None
    licencia = (row.get("LICENCIA") or "").strip()
    if not licencia:
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "LICENCIA",
            "msg": f"Error: (Col. C) La Patente para el Agente Aduanal con Clave: {clave} no está capturada.",
            "solution": "Capturar en la columna C una Patente de Agente Aduanal.",
        }
    return None


def check_rfc_max(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col E: RFC opcional; si tiene valor, máx 30 caracteres."""
    val = (row.get("RFC") or "").strip()
    if not val:
        return None
    if len(val) > RFC_MAX:
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "RFC",
            "msg": f"Error: (Col. E) El RFC de Agente Aduanal: {clave} supera la longitud de caracteres.",
            "solution": f"Capturar en la columna E un RFC de {RFC_MAX} caracteres como máximo.",
        }
    return None


def check_curp_max(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col N: CURP/PERSONAL_ID opcional; si tiene valor, máx 19 caracteres."""
    val = (row.get("PERSONAL_ID") or "").strip()
    if not val:
        return None
    if len(val) > CURP_MAX:
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "PERSONAL_ID",
            "msg": f"Error: (Col. N) El CURP de Agente Aduanal: {clave} supera la longitud de caracteres.",
            "solution": f"Capturar en la columna N un CURP de Agente Aduanal de {CURP_MAX} caracteres como máximo.",
        }
    return None


def check_rfc_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col E: RFC opcional; si tiene valor, debe cumplir formato RFC (ej. XAXX010101XXX)."""
    val = (row.get("RFC") or "").strip()
    if not val:
        return None
    if not RFC_PATTERN.match(val):
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "RFC",
            "msg": "Error: (Col. E) El formato del RFC es inválido.",
            "solution": "Capturar en la columna E un RFC con formato válido (ej. XAXX010101XXX).",
        }
    return None


def check_curp_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col N: CURP/PERSONAL_ID opcional; si tiene valor, debe cumplir formato CURP de 18 caracteres."""
    val = (row.get("PERSONAL_ID") or "").strip()
    if not val:
        return None
    if not CURP_PATTERN.match(val):
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "PERSONAL_ID",
            "msg": "Error: (Col. N) El formato de la CURP es inválido.",
            "solution": "Capturar en la columna N un CURP con formato válido de 18 caracteres.",
        }
    return None


def check_pais_catalogo(
    row: Dict[str, Any],
    line_num: int,
    valid_country_m3: Optional[Set[str]] = None,
) -> Optional[Dict[str, Any]]:
    """Col J: Si PAIS tiene valor, debe existir en catálogo (clave M3)."""
    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
    clave = (row.get("CLAVE") or "").strip()
    return {
        "line": line_num,
        "col": "PAIS",
        "msg": f"Error: (Col. J) El Pais del Agente Aduanal: {clave} no está en el Catálogo de Países.",
        "solution": "Capturar en la columna J un País válido.",
    }


def normalize_phone_digits(value: Optional[str]) -> Optional[str]:
    """
    Normaliza teléfono a solo dígitos.
    - Permite separadores: espacio, guion, paréntesis y punto.
    - No permite signos ni letras (por ejemplo '+', 'ext', '#').
    - Si queda vacío tras normalizar, devuelve None.
    """
    if value is None:
        return None
    raw = str(value).strip()
    if not raw:
        return None
    # Caracteres aceptados en entrada para normalizar.
    if re.search(r"[^0-9\s\-\(\)\.]", raw):
        return None
    normalized = re.sub(r"[\s\-\(\)\.]", "", raw)
    if not normalized:
        return None
    return normalized


def check_phone_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """
    Col K: TELEFONO opcional.
    Si existe, debe poder normalizarse a solo dígitos y quedar entre 10 y 15.
    """
    raw_phone = (row.get("TELEFONO") or "").strip()
    if not raw_phone:
        return None

    normalized_phone = normalize_phone_digits(raw_phone)
    if not normalized_phone:
        return {
            "line": line_num,
            "col": "TELEFONO",
            "msg": "Error: (Col. K) El teléfono contiene texto o símbolos no permitidos.",
            "solution": "Capturar en la columna K solo números o separadores válidos (espacio, guion, paréntesis o punto).",
        }

    digits = len(normalized_phone)
    if digits < PHONE_MIN_DIGITS or digits > PHONE_MAX_DIGITS:
        return {
            "line": line_num,
            "col": "TELEFONO",
            "msg": f"Error: (Col. K) El teléfono debe contener entre {PHONE_MIN_DIGITS} y {PHONE_MAX_DIGITS} dígitos.",
            "solution": f"Capturar en la columna K un teléfono válido de {PHONE_MIN_DIGITS} a {PHONE_MAX_DIGITS} dígitos.",
        }

    # Se deja la fila ya normalizada para mapear/persistir consistentemente.
    row["TELEFONO"] = normalized_phone
    return None


def check_optional_email_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """
    Col M: EMAIL opcional.
    Si viene informado (no vacío), debe cumplir formato email.

    Nota: Se rechaza explícitamente texto como 'NULL' (string literal),
    para evitar que el endpoint de listado falle por validación Pydantic.
    """
    raw_email = (row.get("EMAIL") or "").strip()
    if not raw_email:
        return None

    # EMAIL_PATTERN incluye el caso '' via ^$; aun así aquí raw_email no está vacío.
    if not EMAIL_PATTERN.match(raw_email):
        clave = (row.get("CLAVE") or "").strip()
        return {
            "line": line_num,
            "col": "EMAIL",
            "msg": f"Error: (Col. M) El correo electrónico del Agente Aduanal con Clave: {clave} es inválido.",
            "solution": "Capturar en la columna M un correo con formato válido (ej. usuario@dominio.com) o dejar la celda vacía.",
        }
    return None
