"""Agrégation des trois volets RBAC en un plan d'application VPS unique.

Pourquoi cet agrégateur
-----------------------
Trois générateurs RBAC sont livrés en-repo, chacun répondant à UNE question :
  - `fixtures_gen`   → « QUELS verbes sur QUEL DocType » (Role + Custom DocPerm).
  - `userperm_gen`   → « SUR QUELLES LIGNES » (scope_donnees → User Permission).
  - `roleprofile_gen`→ « QUEL bundle assignable » (Role Profile par portail).
Il manquait la **vue d'ensemble** : dans quel ORDRE l'agent ERPNext applique-t-il
ces trois sorties sur le VPS, avec quelles dépendances et quelles confirmations
préalables ? C'est le rôle de ce module : produire le **run-book** (SPEC §7)
consolidé, plus un manifeste agrégé qui recoupe la cohérence des trois volets.

Ce module NE ré-implémente RIEN : il APPELLE les trois builders et lit leurs
manifestes. Il n'invente aucun chiffre (#6) — tout compte vient d'un builder ;
tout item « à confirmer VPS » est repris tel quel du manifeste d'origine.

Déterminisme : ordre des étapes fixe (SPEC §7), listes de confirmations triées,
aucun horodatage → sortie diffable et re-générable bit-à-bit à contrat constant.
"""

from __future__ import annotations

from typing import Any

# Étapes du run-book (SPEC §7). L'ordre et les dépendances sont du SAVOIR
# procédural documenté (pas des chiffres inventés) : ils encodent les contraintes
# d'application ERPNext v15 (ex. les `Has Role` d'un Role Profile référencent des
# `Role` qui doivent exister → import Role AVANT Role Profile).
# `responsable` : worker (livré en-repo) | vps (agent ERPNext) | worker+vps.
_STEPS: list[dict[str, Any]] = [
    {
        "order": 1,
        "id": "fixtures-generate",
        "titre": "Générer les fixtures Role + Custom DocPerm",
        "responsable": "worker",
        "statut": "livré",
        "commande": "python3 fixtures_gen/rbac_fixtures_gen.py build",
        "produces": ["role.json", "custom_docperm.json"],
        "depends_on": [],
        "confirmations": [],
        "doc": "../fixtures_gen/README.md",
    },
    {
        "order": 2,
        "id": "custom-doctypes-create",
        "titre": "Créer les DocTypes DTP custom manquants (après confirmation)",
        "responsable": "vps",
        "statut": "à faire VPS",
        "commande": None,
        "produces": [],
        "depends_on": ["fixtures-generate"],
        "confirmations": ["custom_doctypes"],
        "doc": "../RBAC_50_ROLES_SPEC.md",
    },
    {
        "order": 3,
        "id": "fixtures-migrate",
        "titre": "Déposer role.json + custom_docperm.json dans fixtures/ puis bench migrate",
        "responsable": "vps",
        "statut": "à faire VPS",
        "commande": None,
        "produces": [],
        "depends_on": ["fixtures-generate", "custom-doctypes-create"],
        "confirmations": [],
        "doc": "../fixtures_gen/README.md",
    },
    {
        "order": 4,
        "id": "userperm-apply",
        "titre": "Matérialiser les User Permission row-level par utilisateur",
        "responsable": "worker+vps",
        "statut": "plan livré · matérialisation VPS",
        "commande": "python3 userperm_gen/userperm_gen.py build",
        "produces": ["user_permission_plan.json"],
        "depends_on": ["fixtures-migrate"],
        "confirmations": ["companies", "roles_scope_equipe"],
        "doc": "../userperm_gen/README.md",
    },
    {
        "order": 5,
        "id": "roleprofile-apply",
        "titre": "Importer les Role Profile (après les Role) puis affecter User.role_profile_name",
        "responsable": "worker+vps",
        "statut": "bundle livré · affectation VPS",
        "commande": "python3 roleprofile_gen/roleprofile_gen.py build",
        "produces": ["role_profile.json"],
        # Dépend de fixtures-migrate : les Has Role référencent des Role existants.
        "depends_on": ["fixtures-migrate"],
        "confirmations": [],
        "doc": "../roleprofile_gen/README.md",
    },
    {
        "order": 6,
        "id": "verify-http-qa",
        "titre": "Vérification HTTP post-déploiement + audit QA 4Big",
        "responsable": "vps",
        "statut": "à faire VPS",
        "commande": None,
        "produces": [],
        "depends_on": ["userperm-apply", "roleprofile-apply"],
        "confirmations": [],
        "doc": "../RBAC_50_ROLES_SPEC.md",
    },
]


def _require_same(label: str, values: list[Any]) -> Any:
    """Renvoie l'unique valeur partagée par les trois volets, ou lève."""
    uniques = sorted({repr(v) for v in values})
    if len(uniques) != 1:
        raise ValueError(
            f"Volets RBAC incohérents sur {label} : {uniques} — "
            "les trois générateurs doivent dériver du MÊME contrat."
        )
    return values[0]


def build_apply_plan(
    contract: dict,
    fixtures_bundle: dict,
    userperm_plan: dict,
    roleprofile_bundle: dict,
) -> dict[str, Any]:
    """Consolide les trois volets RBAC en run-book + manifeste agrégé.

    Aucun chiffre n'est recalculé : chaque compte provient du manifeste du
    builder correspondant (source unique). La cohérence inter-volets (même
    version de contrat, même cible 50 rôles) est vérifiée et refuse un mélange.
    """
    fm = fixtures_bundle["manifest"]
    um = userperm_plan["manifest"]
    pm = roleprofile_bundle["manifest"]

    # Cohérence : les trois volets doivent dériver du même contrat (version + cible).
    source_version = _require_same(
        "source_version", [fm["source_version"], um["source_version"], pm["source_version"]]
    )
    cible = _require_same(
        "cible_rbac_roles",
        [fm["cible_rbac_roles"], um["cible_rbac_roles"], pm["cible_rbac_roles"]],
    )
    generated_from = _require_same(
        "generated_from", [fm["generated_from"], um["generated_from"], pm["generated_from"]]
    )

    # Confirmations VPS consolidées (reprises telles quelles des manifestes source).
    confirmations_vps = {
        "custom_doctypes": sorted(fm.get("custom_doctypes_a_confirmer", [])),
        "companies": sorted(um.get("companies_a_confirmer", [])),
        "roles_scope_equipe": sorted(um.get("roles_scope_equipe_a_confirmer", [])),
    }

    # Comptes agrégés : chaque valeur vient d'un manifeste de builder (pas d'invention).
    counts = {
        "roles": fm["counts"]["role"],
        "custom_docperm": fm["counts"]["custom_docperm"],
        "doctypes_uniques": fm["counts"]["doctypes_uniques"],
        "user_permission_templates": um["counts"]["user_permission_templates"],
        "role_profiles": pm["counts"]["role_profiles"],
    }

    # Cohérence bijective : le nombre de rôles Role == cible == rôles couverts par
    # les Role Profile == entrées du plan User Permission (un plan par rôle).
    up_entries = um["counts"]["plan_entries"]
    rp_covered = pm["counts"]["roles_couverts"]
    consistency = {
        "sources_coherentes": True,  # garanti par _require_same ci-dessus
        "source_version": source_version,
        "cible_rbac_roles": cible,
        "roles_fixtures": counts["roles"],
        "userperm_plan_entries": up_entries,
        "roleprofile_roles_couverts": rp_covered,
        "couverture_bijective": counts["roles"] == cible == up_entries == rp_covered,
    }

    manifest = {
        "generated_from": generated_from,
        "source_version": source_version,
        "cible_rbac_roles": cible,
        "counts": counts,
        "confirmations_vps": confirmations_vps,
        "consistency": consistency,
    }

    return {
        "manifest": manifest,
        "apply_plan": [dict(step) for step in _STEPS],
    }


def step_ids() -> list[str]:
    """IDs des étapes du run-book (pour les invariants du CLI/tests)."""
    return [s["id"] for s in _STEPS]
