"""
Validadores reutilizables para import CSV de clientes y proveedores.
Paridad Clarion: procedencia E/N, tipo C/P/A, clave máx 8, SECON, Prosec, Vinculación,
Es Empresa Certificada, Transformador/SubMaq, desfase.
"""
import re
from typing import Dict, Any, Optional

from api.v1.modules.a76.clients_and_providers.models import ClientOrProviderEnum

RFC_MAX = 30

# Formato RFC México: 3-4 letras, 6 dígitos, 3 homoclave. TAX-ID: 2 dígitos, guión, resto. Ej: 12-3456789.
RFC_PATTERN = re.compile(r"^[A-Z&Ñ]{3,4}\d{6}[A-Z0-9]{3}$", re.IGNORECASE)
TAX_ID_PATTERN = re.compile(r"^\d{2}-[A-Z0-9]{1,27}$", re.IGNORECASE)
NAME_MAX = 256
SHORT_NAME_MAX = 10
# Clarion: Col C máx 8 caracteres
SHORT_NAME_MAX_CLARION = 8
CURP_MAX = 19

# Valores permitidos Col Q (Tipo programa SECON)
TIPO_PROGRAMA_SECON_VALIDOS = frozenset(
    {"IMMEX", "Maquila", "Pitex", "Ecex", "RECIME", "Pronex", "Ninguno"}
)


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


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


def check_tax_id_format(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col E (TAX-ID): 2 dígitos, guión y resto alfanumérico. Ej: 12-3456789. Máx 30 caracteres."""
    val = (row.get("RFC") or "").strip()
    if not val:
        return None
    if len(val) > 30 or not TAX_ID_PATTERN.match(val):
        return {
            "line": line_num,
            "col": "RFC",
            "msg": "El formato del TAX-ID es inválido.",
            "solution": "Capturar en la columna E un TAX-ID con formato: 2 dígitos, guión y resto (ej. 12-3456789). Máx 30 caracteres.",
        }
    return None


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


def parse_client_or_provider(val: Optional[str]) -> Optional[ClientOrProviderEnum]:
    if not val or not str(val).strip():
        return None
    s = str(val).strip()
    v = s.lower()
    # Una sola letra: C, P, A o B (Clarion: A=Ambos; B también usado como Ambos)
    if len(v) == 1:
        if v == "c":
            return ClientOrProviderEnum.CLIENT
        if v == "p":
            return ClientOrProviderEnum.PROVIDER
        if v in ("a", "b"):
            return ClientOrProviderEnum.BOTH
    if v in ("client", "cliente"):
        return ClientOrProviderEnum.CLIENT
    if v in ("provider", "proveedor"):
        return ClientOrProviderEnum.PROVIDER
    if v in ("both", "ambos"):
        return ClientOrProviderEnum.BOTH
    return None


def check_tipo_client_provider(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    tipo_raw = (row.get("TIPO") or "").strip()
    if not tipo_raw:
        return None
    if parse_client_or_provider(tipo_raw) is None:
        return {
            "line": line_num,
            "col": "TIPO",
            "msg": "Valor no válido. Use Cliente (C), Proveedor (P), Ambos (A) o dejar vacío.",
            "solution": "Capturar una opción valida: C para Cliente, P para Proveedor, A para Ambos o dejar el campo vacio.",
        }
    return None


def check_procedencia(
    row: Dict[str, Any], line_num: int
) -> Optional[Dict[str, Any]]:
    """Col A: E o N. Si PAIS (Col L) tiene valor: N → MEX, E → no MEX."""
    val = (row.get("PROCEDENCIA") or "").strip().upper()
    if not val:
        return None
    if val not in ("E", "N"):
        return {
            "line": line_num,
            "col": "PROCEDENCIA",
            "msg": f"Error: (Col. A) El tipo de cliente: {val} es incorrecto.",
            "solution": "Capturar en la columna A el Tipo de cliente correcto: E para Extranjero y N para Nacional.",
        }
    pais = (row.get("PAIS") or "").strip().upper()
    if not pais:
        return None
    if val == "N" and pais != "MEX":
        return {
            "line": line_num,
            "col": "PAIS",
            "msg": f"Error: (Col. L) El tipo de cliente es Nacional y tiene el país {pais}, es incorrecto.",
            "solution": "Capturar en la columna L el país con clave MEX, ya que es un cliente Nacional.",
        }
    if val == "E" and pais == "MEX":
        return {
            "line": line_num,
            "col": "PAIS",
            "msg": f"Error: (Col. L) El tipo de cliente es Extranjero y tiene el país MEX, es incorrecto.",
            "solution": "Capturar en la columna L un país con clave diferente de MEX, ya que es un cliente Extranjero.",
        }
    return None


def check_short_name_max_clarion(
    row: Dict[str, Any], line_num: int, max_len: int = SHORT_NAME_MAX_CLARION
) -> Optional[Dict[str, Any]]:
    """Col C: Clave cliente/proveedor máx 8 caracteres (Clarion)."""
    val = (row.get("SHORT_NAME") or "").strip()
    if not val:
        return None
    if len(val) > max_len:
        return {
            "line": line_num,
            "col": "SHORT_NAME",
            "msg": f"Error: (Col. C) La Clave de Cliente/Proveedor supera la longitud de caracteres (máx {max_len}).",
            "solution": f"Capturar en la columna C una Clave de Cliente/Proveedor de {max_len} caracteres como máximo.",
        }
    return None


def check_tipo_programa_secon(
    row: Dict[str, Any], line_num: int
) -> Optional[Dict[str, Any]]:
    """Col Q: IMMEX, Maquila, Pitex, Ecex, RECIME, Pronex, Ninguno.

    Reglas Clarion para el import CSV:
    - Si NO es Ninguno: obligatorios `NUM_PROGRAMA_SECON` (Col. R) y `FECHA_AUT_SECON` (Col. S).
    - Si es Ninguno o viene vacío: `NUM_PROGRAMA_SECON` (Col. R) debe venir vacío;
      `FECHA_AUT_SECON` (Col. S) se permite (no debe bloquear la carga).
    """
    q_raw = (row.get("TIPO_PROGRAMA_SECON") or "").strip()
    # Normalización robusta: CSV puede traer "Ninguno" o "ninguno", etc.
    valid_map = {v.lower(): v for v in TIPO_PROGRAMA_SECON_VALIDOS}
    q_lower = q_raw.lower()
    q = valid_map.get(q_lower, q_raw)
    r = (row.get("NUM_PROGRAMA_SECON") or "").strip()
    s = (row.get("FECHA_AUT_SECON") or "").strip()
    if q_raw and q not in TIPO_PROGRAMA_SECON_VALIDOS:
        return {
            "line": line_num,
            "col": "TIPO_PROGRAMA_SECON",
            "msg": f"Error: (Col. Q) El Tipo de Programa SECON: {q_raw} es incorrecto.",
            "solution": "Capturar los Tipos de Programa correctos: IMMEX, Maquila, Pitex, Ecex, RECIME, Pronex o Ninguno.",
        }
    if not q or q == "Ninguno":
        if r:
            return {
                "line": line_num,
                "col": "NUM_PROGRAMA_SECON",
                "msg": "Error: (Col. R) El Tipo de Programa es Ninguno y está capturado el número de programa.",
                "solution": "Borrar la información en la columna R del archivo o asignar un Programa en la columna Q.",
            }
        return None
    # No es Ninguno: R y S obligatorios
    if not r:
        return {
            "line": line_num,
            "col": "NUM_PROGRAMA_SECON",
            "msg": f"Error: (Col. R) El Tipo de Programa es: {q} y no está capturado el número de programa.",
            "solution": "Capturarlo en la columna R del archivo.",
        }
    if not s:
        return {
            "line": line_num,
            "col": "FECHA_AUT_SECON",
            "msg": f"Error: (Col. S) El Tipo de Programa es: {q} y no está capturada la Fecha de autorización.",
            "solution": "Capturarla en la columna S del archivo.",
        }
    return None


def check_es_prosec_num_aut(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col T (SI/NO): valida solo el formato/validez de `ES_PROSEC`.

    Nota de negocio (Clarion): `NUM_AUT_PROSEC` ya no debe ser forzado por la opción
    de `ES_PROSEC` (la carga no debe depender entre ambas columnas).
    """
    t = (row.get("ES_PROSEC") or "").strip().upper()
    u = (row.get("NUM_AUT_PROSEC") or "").strip()
    if t and t not in ("SI", "NO"):
        return {
            "line": line_num,
            "col": "ES_PROSEC",
            "msg": f"Error: (Col. T) La opción de si Es Prosec? {t} no es valida.",
            "solution": "Capturar una opción valida: SI, NO, o dejar el campo vacio (se asigna NO).",
        }
    return None


def check_vinculacion(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col V: solo 0, 1 o 2 (o vacío → 0)."""
    val = (row.get("VINCULACION") or "").strip()
    if not val:
        return None
    if val not in ("0", "1", "2"):
        return {
            "line": line_num,
            "col": "VINCULACION",
            "msg": f"Error: (Col. V) La opción de Vinculación: {val} no es valida.",
            "solution": "Capturar una opción valida: 0, 1, 2 o dejar el campo vacio (se asigna 0).",
        }
    return None


def check_es_empresa_certificada_registro(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col W (SI/NO): si SI → Col X obligatoria; si no SI → Col X vacía."""
    w = (row.get("ES_EMPRESA_CERTIFICADA") or "").strip().upper()
    x = (row.get("REGISTRO_EMPRESA_CERT") or "").strip()
    if w and w not in ("SI", "NO"):
        return {
            "line": line_num,
            "col": "ES_EMPRESA_CERTIFICADA",
            "msg": f"Error: (Col. W) La opción de si Es Empresa Certificada? {w} no es valida.",
            "solution": "Capturar una opción valida: SI, NO, o dejar el campo vacio (se asigna NO).",
        }
    if w == "SI" and not x:
        return {
            "line": line_num,
            "col": "REGISTRO_EMPRESA_CERT",
            "msg": "Error: (Col. X) Es Empresa Certificada? es SI y no está capturado el número de empresa certificada.",
            "solution": "Capturar en la columna X el número de Empresa Certificada o cambiar la opción a NO en la columna W.",
        }
    if w != "SI" and x:
        return {
            "line": line_num,
            "col": "REGISTRO_EMPRESA_CERT",
            "msg": "Error: (Col. X) Es Empresa Certificada? no es SI y está capturado el número de empresa certificada.",
            "solution": "Borrar la información de la columna X o cambiar la opción a SI en la columna W.",
        }
    return None


def check_transformador_submaq(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Col AF: primera letra T, S o N (Transformador, SubMaquila, Ninguno) o vacío → Ninguno."""
    val = (row.get("TRANSFORMA_SUBMAQ") or "").strip().upper()
    if not val:
        return None
    first = val[:1] if val else ""
    if first not in ("T", "S", "N"):
        return {
            "line": line_num,
            "col": "TRANSFORMA_SUBMAQ",
            "msg": f"Error: (Col. AF) La opción (SUBMAQUILA/TRANSFORMADOR/NINGUNO): {val} no es valida.",
            "solution": "Capturar una opción valida: Transformador, SubMaquila, Ninguno o dejar el campo vacio (se asigna Ninguno).",
        }
    return None


def check_desfase(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, Any]]:
    """Si COL_EXTRA (Col AH) 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_active(val: Optional[str]) -> bool:
    if not val or not str(val).strip():
        return True
    v = str(val).strip().lower()
    if v in ("1", "true", "si", "sí", "yes", "s", "x"):
        return True
    if v in ("0", "false", "no", "n"):
        return False
    return True
