"""Les 5 critères 4Big — purs, déterministes, calculés depuis le dépôt.

Chaque critère lit UNIQUEMENT le système de fichiers (jamais une valeur saisie)
et renvoie {passed: bool, evidence: str}. C'est le cœur anti-invention (#6) de
l'audit : une note ne peut pas être « écrite pour faire 95 », elle est recomputée
à partir de faits vérifiables (présence d'un fichier, taille, comptage AST léger,
intégrité JSON).

Aucune dépendance pip ; stdlib pure (le runner Gitea tourne sans réseau · #2).
"""

from __future__ import annotations

import json
import os
import re

_TEST_DEF_RE = re.compile(r"^\s*def (test_[A-Za-z0-9_]+)\s*\(", re.MULTILINE)
_ADD_PARSER_RE = re.compile(r"add_parser\(|add_subparsers\(")


def _direct_files(mod_dir: str, suffix: str) -> list[str]:
    """Fichiers se terminant par `suffix` directement dans mod_dir (non récursif)."""
    if not os.path.isdir(mod_dir):
        return []
    return sorted(
        os.path.join(mod_dir, n)
        for n in os.listdir(mod_dir)
        if n.endswith(suffix) and os.path.isfile(os.path.join(mod_dir, n))
    )


def crit_doc(mod_dir: str, min_bytes: int) -> dict:
    """DOC — au moins un Markdown (README/SPEC) d'au moins `min_bytes` octets."""
    for md in _direct_files(mod_dir, ".md"):
        size = os.path.getsize(md)
        if size >= min_bytes:
            return {"passed": True,
                    "evidence": f"{os.path.basename(md)} ({size} octets)"}
    return {"passed": False,
            "evidence": f"aucun *.md ≥ {min_bytes} octets à la racine du module"}


def crit_contrat(mod_dir: str) -> dict:
    """CONTRAT — au moins un contrat de sortie *.schema.json."""
    schemas = _direct_files(mod_dir, ".schema.json")
    if schemas:
        return {"passed": True,
                "evidence": ", ".join(os.path.basename(s) for s in schemas)}
    return {"passed": False, "evidence": "aucun *.schema.json à la racine du module"}


def crit_tests(mod_dir: str, min_methods: int) -> dict:
    """TESTS — ≥ min_methods méthodes `def test_*` dans tests/."""
    tests_dir = os.path.join(mod_dir, "tests")
    count = 0
    files = 0
    if os.path.isdir(tests_dir):
        for root, _dirs, names in os.walk(tests_dir):
            if "__pycache__" in root:
                continue
            for n in names:
                if n.startswith("test_") and n.endswith(".py"):
                    files += 1
                    with open(os.path.join(root, n), encoding="utf-8") as fh:
                        count += len(_TEST_DEF_RE.findall(fh.read()))
    passed = count >= min_methods
    return {"passed": passed,
            "evidence": f"{count} méthodes test_* dans {files} fichier(s) "
                        f"(seuil {min_methods})"}


def crit_cli(mod_dir: str) -> dict:
    """CLI — un entrypoint __main__ avec sous-commandes argparse à la racine."""
    for py in _direct_files(mod_dir, ".py"):
        with open(py, encoding="utf-8") as fh:
            src = fh.read()
        if '__name__ == "__main__"' in src and _ADD_PARSER_RE.search(src):
            return {"passed": True,
                    "evidence": f"{os.path.basename(py)} (argparse + __main__)"}
    return {"passed": False,
            "evidence": "aucun entrypoint __main__ avec add_parser/add_subparsers"}


def crit_handoff(mod_dir: str) -> dict:
    """HANDOFF — out/MANIFEST.json + ≥1 autre artefact, tous JSON non vides."""
    out_dir = os.path.join(mod_dir, "out")
    manifest = os.path.join(out_dir, "MANIFEST.json")
    if not os.path.isfile(manifest):
        return {"passed": False, "evidence": "out/MANIFEST.json absent"}
    others = [os.path.join(out_dir, n) for n in sorted(os.listdir(out_dir))
              if n.endswith(".json") and n != "MANIFEST.json"]
    if not others:
        return {"passed": False,
                "evidence": "out/ ne contient que MANIFEST.json (aucun artefact)"}
    for art in [manifest] + others:
        try:
            with open(art, encoding="utf-8") as fh:
                data = json.load(fh)
        except (json.JSONDecodeError, OSError) as exc:
            return {"passed": False,
                    "evidence": f"{os.path.basename(art)} illisible : {exc}"}
        if data in (None, {}, [], ""):
            return {"passed": False,
                    "evidence": f"{os.path.basename(art)} JSON vide"}
    return {"passed": True,
            "evidence": f"MANIFEST + {len(others)} artefact(s) JSON valides"}


# id critère → (fonction, kwargs supplémentaires tirés des thresholds)
def evaluate(mod_dir: str, criterion_id: str, thresholds: dict) -> dict:
    if criterion_id == "DOC":
        return crit_doc(mod_dir, thresholds["min_doc_bytes"])
    if criterion_id == "CONTRAT":
        return crit_contrat(mod_dir)
    if criterion_id == "TESTS":
        return crit_tests(mod_dir, thresholds["min_test_methods"])
    if criterion_id == "CLI":
        return crit_cli(mod_dir)
    if criterion_id == "HANDOFF":
        return crit_handoff(mod_dir)
    raise KeyError(f"Critère inconnu : {criterion_id}")
