#!/usr/bin/env python3
"""
Validador de catálogo — Bicicletas Maino
Corre todas las verificaciones aprendidas durante la migración. Reutilizable:
pásale el catálogo cada vez que se carguen productos nuevos.

    python3 validar_catalogo.py catalog.json [image_manifest.csv]

Sale con código 1 si hay hallazgos bloqueantes.
"""
import json, csv, re, sys, os, unicodedata, collections, itertools

def n(s): return unicodedata.normalize("NFKD",(s or "").lower()).encode("ascii","ignore").decode()

# Códigos que se verificaron como INEXISTENTES contra el catálogo del fabricante.
CODIGOS_FANTASMA = {
 "pd-r9000": "Shimano nunca usó prefijo «R» en la serie 9000 de pedales. Es PD-9000 (2012) o PD-R9100 (2016)",
 "rd-r62sg": "No existe en ningún catálogo MicroSHIFT. El Bona 10v jaula corta es RD-R67SG",
}
# Familias cuyo código NO identifica una variante única: exigen rango o medida en el título.
FAMILIA_AMBIGUA = {
 "cs-hg50": "código usado por Shimano desde ~1990: el rango de piñones es el identificador real",
 "cs-hg400": "varias variantes de rango bajo el mismo código",
 "cs-hg500": "varias variantes de rango bajo el mismo código",
}
NUCLEO_RUTA_11 = re.compile(r"spline l|road 1[12]|hg-ev", re.I)

def cargar(path):
    d = json.load(open(path, encoding="utf-8"))
    return d["products"], {v["product_handle"]: v for v in d.get("variants", [])}

def validar(cat_path, man_path=None):
    prods, var = cargar(cat_path)
    P = {p["handle"]: p for p in prods}
    H = []
    def add(sev, regla, prod, det, acc):
        H.append({"sev": sev, "regla": regla, "producto": prod, "detalle": det, "accion": acc})

    # 1 · códigos inexistentes
    for p in prods:
        t = n(p["title"])
        for cod, why in CODIGOS_FANTASMA.items():
            if cod in t.replace(" ", "-") or cod.replace("-", "") in t.replace(" ", "").replace("-", ""):
                add("bloqueante", "codigo-inexistente", p["title"], why,
                    "Revisar la unidad física e identificar el código real antes de publicar")

    # 2 · familia ambigua sin rango en el título
    for p in prods:
        t = n(p["title"])
        for fam, why in FAMILIA_AMBIGUA.items():
            if fam in t.replace(" ", "-") and not re.search(r"\d+\s*-\s*\d+\s*t", t):
                add("alto", "variante-ambigua", p["title"], why,
                    "Añadir el rango de piñones al título; sin él no se distingue la variante")

    # 3 · duplicados por SKU
    porsku = collections.defaultdict(list)
    for h, v in var.items():
        s = (v.get("sku") or "").strip().strip("'")
        if s: porsku[s].append(h)
    for s, hs in porsku.items():
        if len(hs) > 1:
            add("alto", "sku-repetido", " · ".join(P[h]["title"][:40] for h in hs),
                f"{len(hs)} productos comparten el SKU «{s}»",
                "Si solo cambian de color o talla, modelarlos como variantes de un producto")

    # 4 · duplicados por título
    def firma(t):
        RUIDO = {"bicicleta","de","la","el","para","con","y","en","del","pinon","velocidades","v","mtb","ruta"}
        return frozenset(w for w in re.sub(r"[^a-z0-9 ]"," ",n(t)).split() if w not in RUIDO and len(w)>1)
    F = {p["handle"]: firma(p["title"]) for p in prods}
    for a, b in itertools.combinations(P, 2):
        fa, fb = F[a], F[b]
        if fa and fb and len(fa & fb) / len(fa | fb) >= 0.92:
            add("alto", "titulo-duplicado", f"{P[a]['title'][:44]} · {P[b]['title'][:44]}",
                "Dos productos con título casi idéntico",
                "Verificar si es el mismo producto cargado dos veces o falta la variante en el título")

    # 5 · imágenes: misma foto en productos distintos, y productos sin foto
    if man_path and os.path.exists(man_path):
        man = list(csv.DictReader(open(man_path)))
        sha = collections.defaultdict(set)
        for m in man:
            if m.get("sha1"): sha[m["sha1"]].add(m["handle"])
        for s, hs in sha.items():
            hs = {h for h in hs if h in P}
            if len(hs) > 1:
                marcas = {P[h].get("brand_slug") for h in hs}
                sev = "bloqueante" if len(marcas) > 1 else "medio"
                det = ("Productos de MARCAS DISTINTAS comparten la misma foto"
                       if len(marcas) > 1 else "Productos distintos comparten la misma foto")
                add(sev, "imagen-compartida", " · ".join(P[h]["title"][:40] for h in hs), det,
                    "Una ficha con la foto de otro producto es motivo de devolución"
                    if len(marcas) > 1 else "Fotografiar cada variante: el comprador no ve qué compra")
    for p in prods:
        if not p.get("images"):
            add("alto", "sin-imagen", p["title"], "No tiene ninguna imagen",
                "No publicar sin foto")

    # 6 · precio y datos operativos
    for h, v in var.items():
        if h not in P: continue
        pr = v.get("price")
        if pr in (None, 0, "0"):
            add("bloqueante", "sin-precio", P[h]["title"], "Precio en 0 o vacío", "No publicable")
        if not (v.get("sku") or "").strip():
            add("medio", "sin-sku", P[h]["title"], "Sin SKU", "Impide control de inventario")
        if not v.get("weight_g"):
            add("medio", "sin-peso", P[h]["title"], "Sin peso",
                "Bloquea la cotización de envío si el checkout cobra por peso")

    # 7 · marca no asignada o genérica
    for p in prods:
        b = p.get("brand_slug") or ""
        if not b or b in ("sin-marca", "maino") and "maino" not in n(p["title"]):
            add("medio", "marca-dudosa", p["title"], f"Marca «{b or 'vacía'}»",
                "«Maino» era el vendor por defecto de Shopify: verificar el fabricante real del título")

    # 8 · título sin variante cuando existe otro igual
    ct = collections.Counter(p["title"] for p in prods)
    for t, c in ct.items():
        if c > 1:
            add("alto", "titulo-sin-variante", t,
                f"{c} productos tienen exactamente el mismo título",
                "Si son variantes (color, talla, acabado), el sufijo debe ir en el título")
    return H

if __name__ == "__main__":
    cat_p = sys.argv[1] if len(sys.argv) > 1 else "out/catalog_enriquecido.json"
    man_p = sys.argv[2] if len(sys.argv) > 2 else "out/image_manifest.csv"
    H = validar(cat_p, man_p)
    ORD = {"bloqueante": 0, "alto": 1, "medio": 2}
    H.sort(key=lambda x: (ORD.get(x["sev"], 9), x["regla"]))
    c = collections.Counter(x["sev"] for x in H)
    r = collections.Counter(x["regla"] for x in H)
    print("="*72); print("VALIDACIÓN DE CATÁLOGO"); print("="*72)
    print(f"\nhallazgos: {len(H)}   bloqueantes: {c['bloqueante']}  altos: {c['alto']}  medios: {c['medio']}\n")
    print("--- por regla ---")
    for k, v in r.most_common(): print(f"  {v:4d}  {k}")
    print("\n--- bloqueantes y altos (muestra) ---")
    for x in [y for y in H if y["sev"] in ("bloqueante","alto")][:16]:
        print(f"\n  [{x['sev']}] {x['regla']}")
        print(f"    {x['producto'][:88]}")
        print(f"    → {x['detalle']}")
    os.makedirs("out", exist_ok=True)
    json.dump(H, open("out/validacion.json","w"), ensure_ascii=False, indent=1)
    print(f"\n\ndetalle completo en out/validacion.json")
    sys.exit(1 if c["bloqueante"] else 0)
