from typing import Any, Dict, List, Optional, Set, Tuple
import logging

logger = logging.getLogger(__name__)


def scalar_trim(val: Any) -> str:
    """ORM/csv pueden devolver tipos no-str; el matching debe ser estable."""
    if val is None:
        return ""
    return str(val).strip()


def normalize_license_for_match(s: Any) -> str:
    """
    Patente aduanal: 4 dígitos. Alinea CSV vs BD (ej. 428 → 0428; espacios).
    Si no es numérico o está vacío, retorna el valor strip (compatibilidad).
    """
    t = scalar_trim(s)
    if not t:
        return ""
    if t.isdigit() and len(t) <= 4:
        return t.zfill(4)
    return t


def normalize_pedimento_number_str(s: Any) -> str:
    """
    Número de pedimento a 7 dígitos para comparar CSV vs BD.

    - CSV (parse_pedimento_col_a) ya entrega segmentos numéricos ≤7 dígitos.
    - BD puede traer espacios, basura no numérica, o datos legacy con más de 7 dígitos
      (p. ej. clave larga pegada en la columna); se extraen dígitos y, si hay más de 7,
      se usan los últimos 7 (el número oficial que coincide con lo capturado en facturas).
    """
    t = scalar_trim(s)
    if not t:
        return ""
    digits = "".join(ch for ch in t if ch.isdigit())
    if not digits:
        return ""
    if len(digits) <= 7:
        return digits.zfill(7)
    return digits[-7:]


def _row_number_looks_like_csv_number(row_number: Any, csv_num_norm: str) -> bool:
    """
    Heurística defensiva para catálogos legacy:
    además del match canónico por últimos 7 dígitos, considera match
    si los dígitos crudos contienen el número CSV normalizado.
    """
    if not csv_num_norm:
        return False
    row_norm = normalize_pedimento_number_str(row_number)
    if row_norm == csv_num_norm:
        return True
    raw_digits = "".join(ch for ch in scalar_trim(row_number) if ch.isdigit())
    return bool(raw_digits and csv_num_norm in raw_digits)


def normalize_catalog_pedimento_identity(
    customs_office: Any,
    license_val: Any,
    pedimento_number: Any,
) -> Optional[Tuple[str, str, str]]:
    """
    Normaliza aduana, patente y número tal como los usa resolve_pedimento_candidates
    al cargar filas del catálogo en memoria.

    La patente puede venir vacía en BD (alta incompleta); se usa "" y el resolver
    no exige coincidencia de patente CSV vs catálogo en ese caso.
    El número de pedimento es obligatorio (sin número no hay fila válida).
    """
    co = scalar_trim(customs_office)
    lic_raw = scalar_trim(license_val)
    num_raw = scalar_trim(pedimento_number)
    if not num_raw:
        return None
    if lic_raw:
        lic = normalize_license_for_match(lic_raw) or lic_raw
    else:
        lic = ""
    num = normalize_pedimento_number_str(num_raw) or num_raw
    return co, lic, num


def canonical_customs_office_for_pedimento_key(customs_office: Any) -> str:
    """
    Aduana canónica de 2 dígitos para claves y matching (007 vs 07, 640 vs 64).
    Tres dígitos con ceros a la izquierda tipo 007 → últimos dos (07); otros tres dígitos → primeros dos (64).
    """
    co = scalar_trim(customs_office)
    if not co:
        return ""
    if len(co) == 3 and co.isdigit():
        if co[0] == "0":
            return co[1:3]
        return co[:2]
    if len(co) == 1 and co.isdigit():
        return co.zfill(2)
    return co[:2] if len(co) >= 2 else co


def customs_offices_equivalent(a: str, b: str) -> bool:
    """True si dos códigos de aduana representan la misma sección para efectos de pedimento."""
    ca = canonical_customs_office_for_pedimento_key(a)
    cb = canonical_customs_office_for_pedimento_key(b)
    if ca and cb:
        return ca == cb
    return _same_customs_office(a or "", b or "")


def _co_variants(customs_office: str) -> Set[str]:
    co = scalar_trim(customs_office)
    if not co:
        return set()
    out: Set[str] = {co}
    if len(co) == 1 and co.isdigit():
        out.add(co.zfill(2))
    if len(co) >= 2:
        out.add(co[:2])
        out.add(co[-2:])
    if len(co) >= 3:
        out.add(co[:3])
    return {x for x in out if x}


def _same_customs_office(csv_customs: str, candidate_customs: str) -> bool:
    csv_variants = _co_variants(csv_customs)
    cand_variants = _co_variants(candidate_customs)
    if not csv_variants or not cand_variants:
        return False
    return bool(csv_variants.intersection(cand_variants))


def aduana_cruce_matches_catalog(ab: str, valid_codes: Set[str]) -> bool:
    """
    Valida Col.AB contra public.customs_sections: mismo criterio que pedimento/aduana
    (p. ej. CSV `070` vs catálogo `07`).
    Si no hay catálogo cargado o la celda viene vacía, no rechaza.
    """
    if not scalar_trim(ab) or not valid_codes:
        return True
    a = scalar_trim(ab)
    if a in valid_codes:
        return True
    for vc in valid_codes:
        if _same_customs_office(a, scalar_trim(vc)):
            return True
    return False


def resolve_pedimento_candidates(
    customs_office: str,
    license_val: str,
    pedimento_number: str,
    patente_lookup: Optional[str],
    pedimento_rows: List[Dict[str, Any]],
) -> Dict[str, Any]:
    """
    Resuelve candidatos de pedimento por componentes reales.
    Prioriza match exacto por aduana/patente/numero y usa fallback por patente/numero.
    """
    candidates: List[Dict[str, Any]] = []
    seen_ids: Set[Any] = set()

    lookup_licenses: List[str] = []
    lic = scalar_trim(license_val)
    if lic:
        lookup_licenses.append(lic)
    patente = scalar_trim(patente_lookup)
    if patente and patente not in lookup_licenses:
        lookup_licenses.append(patente)

    lookup_license_raw: Set[str] = set()
    lookup_license_norm: Set[str] = set()
    for x in lookup_licenses:
        if not x:
            continue
        rx = x.strip()
        lookup_license_raw.add(rx)
        nx = normalize_license_for_match(rx)
        if nx:
            lookup_license_norm.add(nx)

    csv_num_norm = normalize_pedimento_number_str(pedimento_number)
    
    logger.debug(f"Resolving pedimento: customs={customs_office}, license={license_val}, num={pedimento_number} (norm={csv_num_norm}), lookup_licenses={lookup_license_norm}")

    total_catalog_rows = len(pedimento_rows or [])
    num_matches = 0

    for info in (pedimento_rows or []):
        row_num_raw = scalar_trim(info.get("pedimento_number"))
        if not _row_number_looks_like_csv_number(row_num_raw, csv_num_norm):
            continue
        
        num_matches += 1
        row_lic = scalar_trim(info.get("license"))
        if lookup_license_raw or lookup_license_norm:
            # Alta sin patente en catálogo: no excluir por patente del CSV (dato incompleto).
            if row_lic:
                row_ln = normalize_license_for_match(row_lic)
                if row_lic not in lookup_license_raw and row_ln not in lookup_license_norm:
                    continue
        pid = info.get("id")
        if pid not in seen_ids:
            candidates.append(info)
            seen_ids.add(pid)

    if not candidates:
        # Diagnostic: find why it failed
        num_only_matches = []
        for info in (pedimento_rows or []):
            rn_raw = scalar_trim(info.get("pedimento_number"))
            if _row_number_looks_like_csv_number(rn_raw, csv_num_norm):
                num_only_matches.append(info)

        # DEBUG: one line per CSV row on large jobs would flood logs; enable when diagnosing catalog mismatches.
        logger.debug(
            "Pedimento catalog search results: found 0 candidates for %s-%s-%s "
            "(csv_num_norm: %s) among %s rows. Matches by number only: %s",
            customs_office,
            license_val,
            pedimento_number,
            csv_num_norm,
            total_catalog_rows,
            [{"co": c.get("customs_office"), "lic": c.get("license"), "id": c.get("id")} for c in num_only_matches],
        )
        if num_only_matches:
            return {
                "status": "not_found",
                "pedimento": None,
                "candidates": [],
                "not_found_reason": "license_or_patente_mismatch",
            }
        return {
            "status": "not_found",
            "pedimento": None,
            "candidates": [],
            "not_found_reason": "no_catalog_match",
        }

    customs_filtered = [
        c for c in candidates
        if _same_customs_office(customs_office, scalar_trim(c.get("customs_office")))
    ]
    if len(customs_filtered) == 1:
        res = customs_filtered[0]
        logger.debug(
            "Pedimento found: ID %s for %s-%s-%s",
            res.get("id"),
            customs_office,
            license_val,
            pedimento_number,
        )
        return {"status": "ok", "pedimento": res, "candidates": customs_filtered}
    if len(customs_filtered) > 1:
        logger.warning(f"Ambiguous pedimento (multiple customs match): {len(customs_filtered)} candidates for {customs_office}-{license_val}-{pedimento_number}")
        return {"status": "ambiguous", "pedimento": None, "candidates": customs_filtered}

    if len(candidates) == 1:
        solo = candidates[0]
        cand_co = scalar_trim(solo.get("customs_office"))
        if not cand_co:
            return {"status": "ok", "pedimento": solo, "candidates": candidates}
        
        logger.warning(f"Pedimento customs mismatch: expected {customs_office}, found {cand_co}")
        return {
            "status": "not_found",
            "pedimento": None,
            "candidates": [],
            "not_found_reason": "customs_mismatch",
        }

    logger.warning(f"Ambiguous pedimento (no customs match but multiple candidates): {len(candidates)} candidates")
    return {"status": "ambiguous", "pedimento": None, "candidates": candidates}
