#!/usr/bin/env python3
"""Générateur de fixtures ERPNext · RBAC 50 rôles → Frappe fixtures (Sprint 2).

Cible de portage VPS : agent ERPNext Backend (SPEC §7). Ce worker n'écrit
JAMAIS sur le VPS — il produit en-repo les fichiers de fixtures que l'agent
appliquera côté serveur via `bench --site frontend migrate` / `import-fixtures`.

Transforme `rbac_50_roles.json` (contrat validé par `rbac.schema.json`) en :
  - role.json           → fixtures DocType `Role` (50 rôles préfixés « OTO »)
  - custom_docperm.json → fixtures `Custom DocPerm` (permissions par DocType)
  - MANIFEST.json       → traçabilité + DocTypes DTP `custom` à confirmer VPS

Sous-commandes :
  build    [-o OUT]   → écrit role.json / custom_docperm.json / MANIFEST.json
  validate [-o OUT]   → (re)génère en mémoire, valide vs fixtures.schema.json,
                        vérifie les invariants (50 rôles, unicité, séparation
                        des pouvoirs) — sort en erreur si un invariant casse.

Sortie déterministe (tri stable, aucun horodatage) → diffable + re-générable.
Anti-invention (#6) : 100 % des données proviennent du contrat ; les seuls
ajouts sont des flags DocPerm natifs à 0 (défaut sûr) et des méta de traçabilité.
"""

from __future__ import annotations

import argparse
import json
import os
import sys

_HERE = os.path.dirname(os.path.abspath(__file__))
_RBAC_DIR = os.path.normpath(os.path.join(_HERE, ".."))          # rbac/
_DELIVERABLES = os.path.normpath(os.path.join(_RBAC_DIR, ".."))  # 05_deliverables_mvp/

sys.path.insert(0, _HERE)
# Réutilisation (workflow #5 : zéro duplication) du validateur maison Publiciste.
sys.path.insert(0, os.path.join(_DELIVERABLES, "publiciste"))

from fixturelib import builder  # noqa: E402
from lib import validator as maison  # type: ignore  # noqa: E402

_CONTRACT_PATH = os.path.join(_RBAC_DIR, "rbac_50_roles.json")
_SCHEMA_PATH = os.path.join(_HERE, "fixtures.schema.json")
_DEFAULT_OUT = os.path.join(_HERE, "out")


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


def _load(path: str) -> dict:
    with open(path, encoding="utf-8") as fh:
        return json.load(fh)


def _write_json(path: str, data) -> None:
    with open(path, "w", encoding="utf-8") as fh:
        json.dump(data, fh, ensure_ascii=False, indent=2)
        fh.write("\n")


def _build_bundle() -> dict:
    return builder.build_bundle(_load(_CONTRACT_PATH))


def _validate_bundle(bundle: dict) -> list[str]:
    """Valide le bundle contre fixtures.schema.json + invariants métier RBAC."""
    schema = _load(_SCHEMA_PATH)
    errors = list(maison.validate(bundle, schema))

    manifest = bundle["manifest"]
    roles = bundle["role"]
    docperms = bundle["custom_docperm"]

    # Invariant 1 · cible non-négociable « RBAC 50 rôles ».
    if manifest["cible_rbac_roles"] != len(roles):
        errors.append(
            f"cible_rbac_roles={manifest['cible_rbac_roles']} ≠ role fixtures={len(roles)}"
        )

    # Invariant 2 · unicité des noms de rôle Frappe (anti-collision).
    names = [r["role_name"] for r in roles]
    if len(names) != len(set(names)):
        errors.append("Noms de rôle Frappe dupliqués dans les fixtures Role.")

    # Invariant 3 · unicité (role, DocType, permlevel) côté DocPerm.
    keys = [(d["role"], d["parent"], d["permlevel"]) for d in docperms]
    if len(keys) != len(set(keys)):
        errors.append("DocPerm dupliqué pour un même (role, DocType, permlevel).")

    # Invariant 4 · séparation des pouvoirs (#6) : `set_user_permissions`
    # réservé au seul RBAC Admin (aucune élévation de privilège fixée en dur).
    contract = _load(_CONTRACT_PATH)
    admin_roles = {
        r["erpnext_role_name"]
        for r in contract["roles"]
        if r.get("famille") == "plateforme" and r.get("scope_donnees") == "groupe"
    }
    for d in docperms:
        if d["set_user_permissions"] == 1 and d["role"] not in admin_roles:
            errors.append(
                f"Élévation de privilège : {d['role']} porte set_user_permissions "
                f"sur {d['parent']} (réservé au RBAC Admin)."
            )

    # Invariant 5 · cohérence de comptage du manifeste.
    if manifest["counts"]["role"] != len(roles):
        errors.append("counts.role incohérent avec le nombre de fixtures Role.")
    if manifest["counts"]["custom_docperm"] != len(docperms):
        errors.append("counts.custom_docperm incohérent avec les DocPerm.")

    return errors


def cmd_build(args: argparse.Namespace) -> int:
    bundle = _build_bundle()
    errors = _validate_bundle(bundle)
    if errors:
        _eprint("❌ Bundle invalide — génération refusée (anti-régression) :")
        for e in errors:
            _eprint(f"   - {e}")
        return 1

    out = os.path.abspath(args.out)
    os.makedirs(out, exist_ok=True)
    _write_json(os.path.join(out, "role.json"), bundle["role"])
    _write_json(os.path.join(out, "custom_docperm.json"), bundle["custom_docperm"])
    _write_json(os.path.join(out, "MANIFEST.json"), bundle["manifest"])

    m = bundle["manifest"]
    print(f"✅ Fixtures générées dans {out}")
    print(f"   role.json           : {m['counts']['role']} rôles")
    print(
        f"   custom_docperm.json : {m['counts']['custom_docperm']} DocPerm "
        f"sur {m['counts']['doctypes_uniques']} DocTypes"
    )
    if m["custom_doctypes_a_confirmer"]:
        print(
            "   ⚠ DocTypes DTP `custom` à créer + confirmer VPS avant import : "
            + ", ".join(m["custom_doctypes_a_confirmer"])
        )
    return 0


def cmd_validate(args: argparse.Namespace) -> int:
    bundle = _build_bundle()
    errors = _validate_bundle(bundle)
    if errors:
        _eprint("❌ Validation KO :")
        for e in errors:
            _eprint(f"   - {e}")
        return 1
    m = bundle["manifest"]
    print(
        f"✅ Validation OK — {m['counts']['role']} rôles, "
        f"{m['counts']['custom_docperm']} DocPerm, schéma + invariants verts."
    )
    return 0


def main(argv: list[str] | None = None) -> int:
    p = argparse.ArgumentParser(description="Générateur de fixtures ERPNext depuis le contrat RBAC.")
    sub = p.add_subparsers(dest="cmd", required=True)

    pb = sub.add_parser("build", help="génère role.json / custom_docperm.json / MANIFEST.json")
    pb.add_argument("-o", "--out", default=_DEFAULT_OUT, help="dossier de sortie (défaut: ./out)")
    pb.set_defaults(func=cmd_build)

    pv = sub.add_parser("validate", help="valide le bundle (schéma + invariants) sans écrire")
    pv.add_argument("-o", "--out", default=_DEFAULT_OUT, help="ignoré (compat)")
    pv.set_defaults(func=cmd_validate)

    args = p.parse_args(argv)
    return args.func(args)


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