#!/usr/bin/env python3
"""Générateur Faisabilité 4 volets · orchestrateur CLI (Sprint 2).

Cible de portage VPS : `otoia/capabilities/faisabilite_agent.py`
(cf. 03_agents/faisabilite/AGENT.md §Livrable S2).

Produit une faisabilité canonique `data_room/PXX/` (template v1.0) à partir d'un
brief JSON, puis PROUVE que le résultat est consommable par le Publiciste
(re-parse + validation `version.json`) — sinon échoue et retire le score de l'axe
machine-lisible (jamais de score auto-décerné à l'aveugle).

Sous-commandes :
  score     BRIEF.json                 → bilan 4Big (§7) sur stdout
  scaffold  PXX -o OUT                  → arborescence vide (placeholders) d'un projet
  generate  BRIEF.json -o OUT           → data_room/PXX/ + preuve machine-lisible
  batch     BRIEFS_DIR -o DATA_ROOM     → génère tous les briefs d'un dossier

⚠ Ce worker n'écrit JAMAIS sur le VPS. Les artefacts sont produits en local ;
l'application réelle (copie data_room/ VPS + archives + notif) est un livrable
ultérieur côté serveur (AGENT.md §Workflow, Semaines 3-4).
"""

from __future__ import annotations

import argparse
import json
import os
import sys
from datetime import datetime, timezone

_HERE = os.path.dirname(os.path.abspath(__file__))
# Ordre voulu sur sys.path : d'abord le générateur (paquet `genlib`), puis le
# Publiciste (paquet `lib`). Les deux noms sont distincts → aucune collision.
sys.path.insert(0, os.path.normpath(os.path.join(_HERE, "..", "..", "publiciste")))
sys.path.insert(0, _HERE)

from genlib import renderer, scorer  # noqa: E402

# Réutilisation (workflow #5 : zéro duplication) du parser + validateur Publiciste.
from lib import parser as pub_parser  # type: ignore  # noqa: E402
from lib import validator as pub_validator  # type: ignore  # noqa: E402

_SCHEMA_DIR = os.path.normpath(os.path.join(_HERE, ".."))


def _eprint(*args) -> None:
    print(*args, file=sys.stderr)


def _now_iso() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _load_version_schema() -> dict:
    with open(os.path.join(_SCHEMA_DIR, "version.schema.json"), encoding="utf-8") as fh:
        return json.load(fh)


def _validate_brief(brief: dict) -> None:
    """Garde-fou minimal : le brief DOIT porter un code projet canonique."""
    code = brief.get("projet")
    if not (isinstance(code, str) and len(code) == 3 and code[:2] == "P0"
            and code[2] in "123456789"):
        raise ValueError(f"brief.projet invalide (attendu P01..P09) : {code!r}")


# --------------------------------------------------------------------------- #
def _generate_tree(brief: dict, out_dir: str, generated_at: str) -> tuple[str, dict]:
    """Écrit l'arborescence PXX/ et retourne (chemin_projet, bilan) avec l'axe
    machine-lisible PROUVÉ (re-parse Publiciste + validation version.json)."""
    _validate_brief(brief)
    code = brief["projet"]

    # 1er passage : bilan optimiste (axe 5 = 20 par construction).
    bilan = scorer.score(brief, machine_lisible_pts=20)

    projet_dir = os.path.join(out_dir, code)
    files = renderer.render_files(brief, bilan, generated_at)
    for rel, content in files.items():
        dest = os.path.join(projet_dir, rel)
        os.makedirs(os.path.dirname(dest), exist_ok=True)
        with open(dest, "w", encoding="utf-8") as fh:
            fh.write(content)

    # PREUVE axe 5 — machine-lisible :
    #   (a) version.json conforme à version.schema.json
    #   (b) commodité canonique présente dans le volet ingénierie (#9/#10)
    #   (c) parser Publiciste consomme le projet sans erreur et retrouve le code
    machine_ok, motifs = _prove_machine_lisible(projet_dir, code, files)

    if not machine_ok:
        # Honnêteté (#6) : on retire les 20 pts et on réécrit version.json.
        bilan = scorer.score(brief, machine_lisible_pts=0)
        version = renderer.render_version_json(brief, bilan, generated_at)
        with open(os.path.join(projet_dir, "_META", "version.json"),
                  "w", encoding="utf-8") as fh:
            fh.write(json.dumps(version, ensure_ascii=False, indent=2) + "\n")
        bilan["_machine_lisible_motifs"] = motifs

    return projet_dir, bilan


def _prove_machine_lisible(projet_dir: str, code: str,
                           files: dict) -> tuple[bool, list[str]]:
    motifs: list[str] = []

    # (a) version.json valide.
    with open(os.path.join(projet_dir, "_META", "version.json"), encoding="utf-8") as fh:
        version = json.load(fh)
    errs = pub_validator.validate(version, _load_version_schema())
    if errs:
        motifs += [f"version.json: {e}" for e in errs]

    # (b) paramètres canoniques présents dans le volet ingénierie.
    ing_md = files[os.path.join("40_ingenierie_faisabilite",
                                "ingenierie_faisabilite.md")]
    manquants = renderer.check_canonical(ing_md)
    if manquants:
        motifs.append("paramètres canoniques absents : " + ", ".join(manquants))

    # (c) le parser Publiciste consomme le projet sans erreur.
    try:
        projet = pub_parser.parse_projet(projet_dir)
        if projet.get("code") != code:
            motifs.append(f"parser: code {projet.get('code')!r} != {code!r}")
    except Exception as exc:  # noqa: BLE001 — on veut la raison exacte
        motifs.append(f"parser a levé : {exc!r}")

    return (not motifs), motifs


# --------------------------------------------------------------------------- #
def cmd_score(ns: argparse.Namespace) -> int:
    with open(ns.brief, encoding="utf-8") as fh:
        brief = json.load(fh)
    _validate_brief(brief)
    bilan = scorer.score(brief)
    print(json.dumps(bilan, ensure_ascii=False, indent=2))
    return 0


def cmd_scaffold(ns: argparse.Namespace) -> int:
    """Arborescence vide (placeholders) — utile pour amorcer un nouveau projet."""
    brief = {"projet": ns.projet, "synthetique": False}
    generated_at = ns.generated_at or _now_iso()
    projet_dir, bilan = _generate_tree(brief, ns.out, generated_at)
    _eprint(f"✅ scaffold {ns.projet} → {projet_dir} "
            f"(statut={bilan['statut_faisabilite']}, score={bilan['score_4big']})")
    _eprint(f"   {len(bilan['champs_manquants'])} champ(s) 🔴 à renseigner.")
    return 0


def cmd_generate(ns: argparse.Namespace) -> int:
    with open(ns.brief, encoding="utf-8") as fh:
        brief = json.load(fh)
    generated_at = ns.generated_at or _now_iso()
    projet_dir, bilan = _generate_tree(brief, ns.out, generated_at)

    _eprint(f"✅ {brief['projet']} → {projet_dir}")
    _eprint(f"   statut={bilan['statut_faisabilite']} · score 4Big={bilan['score_4big']}/100")
    _eprint(f"   axes={bilan['axes']}")
    if bilan["champs_manquants"]:
        _eprint(f"   champs 🔴 manquants ({len(bilan['champs_manquants'])}) :")
        for c in bilan["champs_manquants"]:
            _eprint("     ·", c)
    motifs = bilan.get("_machine_lisible_motifs")
    if motifs:
        _eprint("❌ preuve machine-lisible ÉCHOUÉE (axe 5 = 0) :")
        for m in motifs:
            _eprint("     ·", m)
        return 1

    # Un statut « complete » DOIT être publiable (#5) — sinon on alerte.
    if bilan["statut_faisabilite"] == "complete" and bilan["score_4big"] < 95:
        _eprint("❌ incohérence : complete avec score < 95.")
        return 1
    return 0


def cmd_batch(ns: argparse.Namespace) -> int:
    generated_at = ns.generated_at or _now_iso()
    briefs = sorted(
        f for f in os.listdir(ns.briefs_dir)
        if f.endswith(".json") and not f.startswith(".")
    )
    if not briefs:
        _eprint(f"⚠ aucun brief *.json dans {ns.briefs_dir}")
        return 1
    rc = 0
    for fn in briefs:
        with open(os.path.join(ns.briefs_dir, fn), encoding="utf-8") as fh:
            brief = json.load(fh)
        try:
            projet_dir, bilan = _generate_tree(brief, ns.out, generated_at)
        except Exception as exc:  # noqa: BLE001
            _eprint(f"❌ {fn} : {exc}")
            rc = 1
            continue
        flag = "⚠" if bilan.get("_machine_lisible_motifs") else "✅"
        _eprint(f"{flag} {brief.get('projet')} · {bilan['statut_faisabilite']} · "
                f"{bilan['score_4big']}/100 · {len(bilan['champs_manquants'])} 🔴")
        if bilan.get("_machine_lisible_motifs"):
            rc = 1
    return rc


# --------------------------------------------------------------------------- #
def build_argparser() -> argparse.ArgumentParser:
    ap = argparse.ArgumentParser(prog="faisabilite_gen",
                                 description=__doc__.splitlines()[0])
    sub = ap.add_subparsers(dest="cmd", required=True)

    p = sub.add_parser("score", help="brief.json → bilan 4Big")
    p.add_argument("brief")
    p.set_defaults(func=cmd_score)

    p = sub.add_parser("scaffold", help="PXX → arborescence vide (placeholders)")
    p.add_argument("projet")
    p.add_argument("-o", "--out", required=True, help="dossier data_room de sortie")
    p.add_argument("--generated-at", dest="generated_at", default=None)
    p.set_defaults(func=cmd_scaffold)

    p = sub.add_parser("generate", help="brief.json → data_room/PXX/ (+ preuve)")
    p.add_argument("brief")
    p.add_argument("-o", "--out", required=True, help="dossier data_room de sortie")
    p.add_argument("--generated-at", dest="generated_at", default=None)
    p.set_defaults(func=cmd_generate)

    p = sub.add_parser("batch", help="dossier de briefs → data_room/")
    p.add_argument("briefs_dir")
    p.add_argument("-o", "--out", required=True)
    p.add_argument("--generated-at", dest="generated_at", default=None)
    p.set_defaults(func=cmd_batch)
    return ap


def main(argv: list[str] | None = None) -> int:
    ns = build_argparser().parse_args(argv)
    return ns.func(ns)


if __name__ == "__main__":
    raise SystemExit(main())
