#!/usr/bin/env python3
"""harkaq-verdict — clasifica las denegaciones crudas y emite el Verdict final (SDD 16 §4.2).

    harkaq-verdict.py <verdict-crudo.json> <politica> [--human]

Por qué es un proceso aparte y NO parte de harkaq-audit: el lector corre con CAP_AUDIT_READ, y
clasificar es POLÍTICA, no privilegio. El componente privilegiado se mantiene lo más tonto
posible — mismo argumento que Q1c (el privilegio en un helper chico, no en hammerd). Efecto
lateral útil: se itera la clasificación sin recompilar el binario capabilitado (y sin perder el
setcap en cada cambio).

Las tres categorías salen de la medición de §4.2:

  base      concedida por la política ⇒ nunca aparece como denegación. No se clasifica acá.
  esperada  denegada A PROPÓSITO y previsible. `zig cc` sondea /usr/bin/gcc y el build sale rc=0
            igual: concederlo lo dejaría usar el gcc de Alpine por detrás. Se declara en la
            política con `# expect <path>` — la MISMA fuente de verdad que las reglas, y
            harkaq-exec la ignora como comentario. D3: no se calla, se clasifica.
  deuda     todo lo demás. ESTO es la de-Alpinización: lo que el build usó sin declarar.

Y la consecuencia que obliga: `Hermetico` = **cero deuda**, no cero denegaciones. Con la
definición estricta ningún build real lo alcanzaría jamás y el estado más importante del sistema
quedaría vacío de uso.
"""
import json
import sys


def cargar_esperadas(politica):
    """Lee los `# expect <path>` de la política. Un fichero, una fuente de verdad."""
    esperadas = set()
    with open(politica) as f:
        for linea in f:
            linea = linea.strip()
            if linea.startswith("# expect "):
                esperadas.add(linea[len("# expect "):].strip())
    return esperadas


def main():
    if len(sys.argv) < 3:
        print(__doc__.split("\n")[2].strip(), file=sys.stderr)
        return 2
    crudo = json.load(open(sys.argv[1]))
    esperadas = cargar_esperadas(sys.argv[2])
    humano = "--human" in sys.argv

    # SinEvidencia manda sobre todo: si el lector no es confiable, no clasificamos nada. Un
    # veredicto sin canario no es "limpio", es "no sabemos" (D9), y reinterpretarlo acá sería
    # justo el falso Hermetico que el canario existe para impedir.
    if crudo["estado"] == "SinEvidencia":
        salida = dict(crudo, esperadas=[], deuda=[])
        print(json.dumps(salida) if not humano else
              f"SinEvidencia — {crudo.get('motivo', 'sin motivo')}")
        return 3

    vistas_esperadas, deuda = [], []
    for d in crudo["denials"]:
        (vistas_esperadas if d["path"] in esperadas else deuda).append(d)

    estado = "Hermetico" if not deuda else "Impuro"
    salida = {
        "estado": estado,
        "canario_visto": crudo["canario_visto"],
        "domain": crudo["domain"],
        "contador_kernel": crudo["contador_kernel"],
        "esperadas": vistas_esperadas,
        "deuda": deuda,
    }

    if not humano:
        print(json.dumps(salida))
    else:
        print(f"  estado: {estado}  (canario visto, contador kernel={crudo['contador_kernel']})")
        if vistas_esperadas:
            uniq = sorted({d["path"] for d in vistas_esperadas})
            print(f"  esperadas ({len(vistas_esperadas)}): " + ", ".join(uniq))
        if deuda:
            print(f"  DEUDA ({len(deuda)}) — usado sin declarar:")
            for p in sorted({d["path"] for d in deuda}):
                print(f"      {p}")
        else:
            print("  deuda: ninguna ✓")
    return 0 if estado == "Hermetico" else 1


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