"""
Validadores reutilizables para import CSV de pedimentos.
Incluye parseo de fechas (paridad exchange_rate) para FECHA_INICIO, FECHA_FINAL, FECHA_PAGO.
"""
from datetime import datetime, time
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Optional, Set, List

DATE_FORMATS: List[str] = ["%Y-%m-%d", "%d/%m/%Y", "%m/%d/%Y", "%d-%m-%Y", "%Y/%m/%d"]
DATE_FORMAT_PREFERENCE_MAP: Dict[str, str] = {
    "dd/mm/yyyy": "%d/%m/%Y",
    "mm/dd/yyyy": "%m/%d/%Y",
    "yyyy-mm-dd": "%Y-%m-%d",
}


def parse_date(
    val: Optional[str], date_format_preference: Optional[str] = None
) -> Optional[datetime]:
    """Parsea fecha; si date_format_preference está definido, solo se acepta ese formato."""
    if not val or not str(val).strip():
        return None
    raw = str(val).strip()
    if date_format_preference and date_format_preference in DATE_FORMAT_PREFERENCE_MAP:
        fmt = DATE_FORMAT_PREFERENCE_MAP[date_format_preference]
        try:
            parsed = datetime.strptime(raw, fmt)
            return datetime.combine(parsed.date(), time.min)
        except ValueError:
            return None
    for fmt in DATE_FORMATS:
        try:
            parsed = datetime.strptime(raw, fmt)
            return datetime.combine(parsed.date(), time.min)
        except ValueError:
            continue
    return None


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_int_in_set(
    row: Dict[str, Any], col: str, valid_ids: Set[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"}
    try:
        client_id = int(val)
    except ValueError:
        return {"line": line_num, "col": col, "msg": "Debe ser número entero"}
    if client_id not in valid_ids:
        return {"line": line_num, "col": col, "msg": "Cliente no existe en catálogo"}
    return None


def check_optional_int_in_set(
    row: Dict[str, Any], col: str, valid_ids: Set[int], line_num: int
) -> Optional[Dict[str, Any]]:
    """Si la columna viene vacía no se valida; si trae valor debe ser entero y estar en valid_ids."""
    val = (row.get(col) or "").strip()
    if not val:
        return None
    try:
        client_id = int(val)
    except ValueError:
        return {"line": line_num, "col": col, "msg": "Debe ser número entero"}
    if valid_ids and client_id not in valid_ids:
        return {"line": line_num, "col": col, "msg": "Cliente no existe en catálogo"}
    return None


def check_optional_short_name(
    row: Dict[str, Any],
    col: str,
    short_name_to_id: Dict[str, int],
    line_num: int,
) -> Optional[Dict[str, Any]]:
    """Si la columna viene vacía no se valida; si trae valor debe ser short_name existente en catálogo. Solo string, no ID."""
    val = (row.get(col) or "").strip()
    if not val:
        return None
    key = val.upper()
    if key not in short_name_to_id:
        return {
            "line": line_num,
            "col": col,
            "msg": "Cliente no encontrado (short_name no existe en catálogo)",
        }
    return None


def check_in_set(
    row: Dict[str, Any],
    col: str,
    valid_set: Set[str],
    line_num: int,
    max_len: int = 10,
    catalog_name: str = "catálogo",
) -> 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"}
    if valid_set and val not in valid_set:
        return {"line": line_num, "col": col, "msg": f"No existe en {catalog_name}"}
    return None


def check_optional_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 check_optional_decimal(row: Dict[str, Any], col: str, line_num: int) -> Optional[Dict[str, Any]]:
    val = (row.get(col) or "").strip()
    if not val:
        return None
    try:
        Decimal(val.replace(",", "."))
    except (InvalidOperation, ValueError):
        return {"line": line_num, "col": col, "msg": "Valor numérico inválido"}
    return None


def check_optional_in_set(
    row: Dict[str, Any], col: str, allowed: Set[str], line_num: int, msg: str
) -> Optional[Dict[str, Any]]:
    val = (row.get(col) or "").strip().lower()
    if not val:
        return None
    if val not in allowed:
        return {"line": line_num, "col": col, "msg": msg}
    return None


PEDIMENTO_CODE_MAX = 2
REGIMEN_MAX = 3
