#!/usr/bin/env python3
"""affected — frontera MÍNIMA de rebuild ante un CVE (SDD 17 §4).

    affected.py zlib                 # ¿qué hay que reconstruir si zlib tiene un CVE?
    affected.py zlib --agenda        # + el orden topológico (qué primero)
    affected.py zlib --sellados       # sólo lo que YA está en el store (el trabajo real)

Un CVE en `zlib` no obliga a reconstruir el catálogo: obliga a reconstruir **el cono de lo que
depende de zlib**, y nada más. El grafo de dependencias declarado ya lo sabe — es el `dirty_cone`
de `dataflow` (crate no_std de tawasuyu), y el `topo_order` es la agenda.

Que esto sea exacto (y no "reconstruí todo por las dudas") **cae solo del invariante**: las deps
están declaradas por hash, así que el cono es computable. Un distro sin clausura declarada tiene
que adivinar.

Nota honesta: la frontera es exacta respecto de **lo declarado**. Si una receta usa algo sin
declararlo, queda fuera del cono — y ese es justo el agujero que harkaq cierra (SDD 16): la
campaña de esta semana declaró ~84 deps que el kernel midió y nadie había escrito. **El cono es
tan bueno como la clausura**, y por eso los dos frentes se necesitan.
"""
import collections
import glob
import os
import re
import sys


def grafo():
    """(deps, rev): receta → sus deps declaradas, y su inverso."""
    deps = collections.defaultdict(set)
    for f in glob.glob("recipes/*.toml"):
        n = os.path.basename(f)[:-5]
        s = open(f, errors="ignore").read()
        m = re.search(r"\[deps\](.*?)(\n\[|\Z)", s, re.S)
        if m:
            for d in re.findall(r'"([^"]+)"', m.group(1)):
                deps[n].add(d)
    rev = collections.defaultdict(set)
    for n, ds in deps.items():
        for d in ds:
            rev[d].add(n)
    return deps, rev


def cono(rev, semilla):
    """dirty_cone: todo lo que depende TRANSITIVAMENTE de `semilla`. La frontera de rebuild."""
    vistos, frontera = set(), [semilla]
    while frontera:
        x = frontera.pop()
        for c in rev.get(x, ()):
            if c not in vistos:
                vistos.add(c)
                frontera.append(c)
    return vistos


def agenda(deps, afectadas):
    """topo_order dentro del cono: qué reconstruir primero (una dep antes que su consumidor)."""
    pend = {n: {d for d in deps.get(n, ()) if d in afectadas} for n in afectadas}
    orden, listos = [], [n for n, p in pend.items() if not p]
    while listos:
        n = listos.pop()
        orden.append(n)
        for m in afectadas:
            if n in pend[m]:
                pend[m].discard(n)
                if not pend[m]:
                    listos.append(m)
    ciclo = [n for n in afectadas if n not in orden]
    return orden, ciclo


def main():
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not args:
        print(__doc__.split("\n")[2].strip(), file=sys.stderr)
        return 2
    pkg = args[0]
    if not os.path.exists(f"recipes/{pkg}.toml"):
        print(f"no existe recipes/{pkg}.toml", file=sys.stderr)
        return 2

    deps, rev = grafo()
    afectadas = cono(rev, pkg)
    total = len(glob.glob("recipes/*.toml"))

    print(f"══ CVE en «{pkg}» — frontera mínima de rebuild")
    print(f"   {len(afectadas)} de {total} recetas afectadas ({len(afectadas)*100//max(total,1)}%)"
          f" — el resto NO se toca")

    if "--sellados" in sys.argv or "--agenda" in sys.argv:
        sell = {n for n in afectadas if glob.glob(f"store/*-{n}")}
        print(f"   de ésas, {len(sell)} ya están selladas en el store ⇒ ése es el trabajo real")

    if "--agenda" in sys.argv:
        orden, ciclo = agenda(deps, afectadas)
        print("\n   ── agenda (topo_order: una dep antes que su consumidor):")
        for i, n in enumerate(orden, 1):
            marca = "●" if glob.glob(f"store/*-{n}") else "○"
            print(f"      {i:3}. {marca} {n}")
        if ciclo:
            print(f"   ⚠ {len(ciclo)} en ciclo (no ordenables): {', '.join(sorted(ciclo))}")
        print("      ● = sellado (hay que reconstruir)   ○ = sin artefacto")
    else:
        for n in sorted(afectadas):
            print(f"      {n}")

    print("\n   La frontera es exacta respecto de lo DECLARADO. Lo no declarado queda fuera del"
          "\n   cono — es el agujero que harkaq cierra (SDD 16).")
    return 0


if __name__ == "__main__":
    sys.exit(main())
