"""Registre des modules audités + PREUVE de couverture 100% vs le CI Gitea.

Le périmètre revendiqué par cet audit — « 100% des livrables » — n'est crédible
que s'il est PROUVÉ, pas déclaré. On lit donc `.gitea/workflows/ci.yml` (la seule
autorité sur ce qui est réellement gated) et on vérifie :

  1. l'ensemble des modules du registre == l'ensemble des `working-directory:`
     sous `05_deliverables_mvp/` du CI, PRIVÉ du module auditeur lui-même
     (séparation des pouvoirs · l'auditeur ne s'auto-note pas · ISA 315) ;
  2. chaque module gated alimente bien le job d'agrégat `gate` (via `needs`).

Ainsi, tout futur livrable ajouté au CI sans mise à jour du registre casse la
couverture → le gate rougit. Aucune omission silencieuse possible.

Parsing YAML volontairement minimal (stdlib pure · aucune dépendance pip · #2).
"""

from __future__ import annotations

import os
import re

from .deps import DELIVERABLES_ROOT

_REPO_ROOT = os.path.normpath(os.path.join(DELIVERABLES_ROOT, ".."))
_CI_PATH = os.path.join(_REPO_ROOT, ".gitea", "workflows", "ci.yml")

_PREFIX = "05_deliverables_mvp/"

_JOB_RE = re.compile(r"^  ([A-Za-z0-9_-]+):\s*$")
_WD_RE = re.compile(r"^\s*working-directory:\s*(\S+)\s*$")
_NEEDS_RE = re.compile(r"^\s*needs:\s*\[(.*)\]\s*$")


def _read_ci() -> str:
    with open(_CI_PATH, encoding="utf-8") as fh:
        return fh.read()


def parse_ci() -> dict:
    """Retourne {job_id -> chemin module} pour les jobs sous 05_deliverables_mvp
    et la liste `needs` du job `gate`."""
    lines = _read_ci().splitlines()
    current_job = None
    job_to_path: dict[str, str] = {}
    gate_needs: list[str] = []
    in_gate = False

    for line in lines:
        m = _JOB_RE.match(line)
        if m:
            current_job = m.group(1)
            in_gate = current_job == "gate"
            continue
        wd = _WD_RE.match(line)
        if wd and current_job:
            val = wd.group(1)
            if val.startswith(_PREFIX):
                job_to_path[current_job] = val[len(_PREFIX):]
            continue
        if in_gate:
            nm = _NEEDS_RE.match(line)
            if nm:
                gate_needs = [j.strip() for j in nm.group(1).split(",") if j.strip()]

    return {"job_to_path": job_to_path, "gate_needs": gate_needs}


def ci_module_paths(exclude: str | None = None) -> set[str]:
    """Chemins de modules gated par le CI (hors `exclude`, ex. l'auditeur)."""
    paths = set(parse_ci()["job_to_path"].values())
    if exclude is not None:
        paths.discard(exclude)
    return paths


def coverage_report(spec: dict) -> dict:
    """Compare le registre du spec à la réalité du CI. Retourne un diagnostic
    sérialisable ; `ok=True` ssi couverture bijective + tous gated."""
    self_module = spec["self_module"]
    registry_paths = {m["path"] for m in spec["modules"]}
    ci = parse_ci()
    job_to_path = ci["job_to_path"]
    ci_paths = {p for p in job_to_path.values() if p != self_module}

    missing_in_registry = sorted(ci_paths - registry_paths)   # gated mais non audité
    missing_in_ci = sorted(registry_paths - ci_paths)          # audité mais non gated

    # Chaque module audité doit alimenter le gate d'agrégat.
    path_to_jobs: dict[str, list[str]] = {}
    for job, path in job_to_path.items():
        path_to_jobs.setdefault(path, []).append(job)
    not_in_gate = sorted(
        p for p in registry_paths
        if not any(j in ci["gate_needs"] for j in path_to_jobs.get(p, []))
    )

    ok = not missing_in_registry and not missing_in_ci and not not_in_gate
    return {
        "ok": ok,
        "ci_modules_count": len(ci_paths),
        "registry_modules_count": len(registry_paths),
        "missing_in_registry": missing_in_registry,
        "missing_in_ci": missing_in_ci,
        "not_in_gate": not_in_gate,
        "self_module_excluded": self_module,
    }
