#!/usr/bin/env python3
"""why-differs-barrido — pasa `hammer why-differs` por TODO el store y agrega las causas.

Para qué: el store guarda varios sellados por paquete (recetas que cambiaron a lo largo del tiempo).
Comparar dos sellados del mismo nombre responde una pregunta que hasta ahora nadie hacía a escala:
**¿por qué difieren?** Y sobre todo separa los dos casos que se confunden a ojo:

  · `.hammer/recipe.toml` DIFIERE  ⇒ son recetas distintas. Que el artefacto cambie es lo esperado.
  · `.hammer/recipe.toml` IDÉNTICO ⇒ misma receta, distinto artefacto: **sospechoso**.

CUIDADO CON EL SEGUNDO CASO (y por qué este script no grita "no-reproducción"): el artefacto sólo
guarda su `recipe.toml`, NO los hashes de sus dependencias — pero el ArtifactHash SÍ los incluye.
Dos sellados con el mismo `recipe.toml` pueden diferir **legítimamente** porque una dep cambió
(glib nuevo ⇒ gtk4 se re-sella con contenido distinto). "Mismo recipe.toml, artefacto distinto" es
un SUPERCONJUNTO de las no-reproducciones, no una prueba.

Lo que sí se puede afirmar mirando la CAUSA, y por eso el informe separa dos grupos:

  · **no-determinismo probable** — todas las divergencias son metadatos que no dependen de las
    deps: MTIME de gzip, cabecera `ar`, ruta de build embebida, o secciones ELF puramente
    informativas (`.debug_*`, `.comment`, `.note.gnu.build-id`, tablas de símbolos). Una dep
    distinta no explica que cambie SÓLO eso.
  · **puede ser una dep** — difiere `.text`/`.data`/`.rodata` o hay ficheros presentes/ausentes.
    Indistinguible de "la dependencia cambió" sin reconstruir. Reconstruir es la única prueba.

Es el "multiplicador de granja" del SDD 17 §1.2: debuggear una no-reproducción deja de ser artesanal.

Uso:   scripts/why-differs-barrido.sh              # barre todo el store
       LIMITE=50 scripts/why-differs-barrido.sh    # sólo los primeros 50 paquetes
       JSON=informe.json scripts/why-differs-barrido.sh
"""
import collections
import json
import os
import pathlib
import subprocess
import sys

RAIZ = pathlib.Path(__file__).resolve().parent.parent
STORE = RAIZ / "store"
HAMMER = RAIZ / "target/release/hammer"
LIMITE = int(os.environ.get("LIMITE", "0"))
SALIDA_JSON = os.environ.get("JSON")
# Un artefacto puede pesar GB (qt6, kernels): hashearlo entero para el barrido no aporta.
TIMEOUT = int(os.environ.get("TIMEOUT", "120"))


def sellados_por_nombre():
    """{nombre: [rutas…]} — el nombre es lo que sigue al hash en `<hash>-<nombre>`."""
    m = collections.defaultdict(list)
    for d in STORE.iterdir():
        if not d.is_dir():
            continue
        _, sep, nombre = d.name.partition("-")
        if sep:
            m[nombre].append(d)
    return {n: sorted(v, key=lambda p: p.stat().st_mtime) for n, v in m.items() if len(v) >= 2}


# Secciones ELF que son metadatos: su contenido no cambia porque una dependencia cambie.
SECCIONES_META = (".debug", ".zdebug", ".comment", ".note", ".symtab", ".strtab", ".shstrtab")
# Causas que, por sí solas, no las explica un cambio de dependencia.
CAUSAS_META = {"gzip_mtime", "ar_cabecera", "ruta_de_build"}


def solo_metadatos(d):
    """¿Esta divergencia es explicable SÓLO por no-determinismo del build?"""
    if d["causa"] in CAUSAS_META:
        return True
    if d["causa"] != "elf_secciones":
        return False
    return all(s.split(" ")[0].startswith(SECCIONES_META) for s in d["secciones"])


def main():
    if not HAMMER.exists():
        sys.exit(f"falta {HAMMER} (cargo build --release --bin hammer)")

    grupos = sellados_por_nombre()
    nombres = sorted(grupos)
    if LIMITE:
        nombres = nombres[:LIMITE]
    print(f"== barrido why-differs: {len(nombres)} paquetes con ≥2 sellados\n")

    causas = collections.Counter()
    no_determinismo = []    # misma receta y sólo metadatos ⇒ el build no es determinista
    quiza_dep = []          # misma receta pero difiere código ⇒ pudo cambiar una dependencia
    recetas_distintas = 0
    errores = []

    for i, nombre in enumerate(nombres, 1):
        a, b = grupos[nombre][-2], grupos[nombre][-1]   # los dos más recientes
        try:
            r = subprocess.run(
                [str(HAMMER), "why-differs", str(a), str(b), "--json"],
                capture_output=True, text=True, timeout=TIMEOUT,
            )
            informe = json.loads(r.stdout)
        except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
            errores.append((nombre, type(e).__name__))
            continue

        divs = informe["divergencias"]
        if not divs:
            continue
        receta_cambio = any(d["ruta"] == ".hammer/recipe.toml" for d in divs)
        for d in divs:
            causas[d["causa"]] += 1
        if receta_cambio:
            recetas_distintas += 1
        elif all(solo_metadatos(d) for d in divs):
            no_determinismo.append((nombre, a.name[:12], b.name[:12], divs))
        else:
            quiza_dep.append((nombre, a.name[:12], b.name[:12], divs))
        if i % 50 == 0:
            print(f"   … {i}/{len(nombres)}")

    print(f"\n── recetas distintas (divergencia esperada): {recetas_distintas}")
    print(f"── misma receta, SÓLO metadatos difieren (no-determinismo probable): {len(no_determinismo)}")
    print(f"── misma receta, difiere código/ficheros (puede ser una dep — no concluyente): {len(quiza_dep)}")
    if errores:
        print(f"── no analizados: {len(errores)} ({errores[:3]}…)")

    print("\n── causas agregadas:")
    for c, n in causas.most_common():
        print(f"   {c:22s} {n}")

    if no_determinismo:
        print("\n══ NO-DETERMINISMO PROBABLE (misma receta; sólo difieren metadatos que las deps")
        print("   no explican — reconstruir para confirmar):")
        for nombre, ha, hb, divs in no_determinismo:
            print(f"\n   {nombre}  [{ha}… vs {hb}…]  {len(divs)} divergencia(s)")
            for d in divs[:4]:
                print(f"     · {d['ruta']}: {d['explicacion'].splitlines()[0][:150]}")
                if d.get("remedio"):
                    print(f"       → {d['remedio']}")

    if SALIDA_JSON:
        pathlib.Path(SALIDA_JSON).write_text(json.dumps({
            "causas": dict(causas),
            "recetas_distintas": recetas_distintas,
            "no_determinismo": [
                {"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in no_determinismo
            ],
            "quiza_dep": [
                {"nombre": n, "a": a, "b": b, "divergencias": d} for n, a, b, d in quiza_dep
            ],
        }, indent=2, ensure_ascii=False))
        print(f"\n   informe → {SALIDA_JSON}")


if __name__ == "__main__":
    main()
