"""Modèle Frappe/ERPNext v15 : mapping RBAC → fixtures `Role` + `Custom DocPerm`.

Sépare la CONNAISSANCE FRAPPE (noms de champs, ordre des colonnes de permission,
valeurs par défaut) de la logique de construction (`builder.py`). Aucun chiffre
métier ici — uniquement la structure native ERPNext (contrainte #1 « ERPNext
natif = priorité absolue » : on n'invente aucun modèle de permission externe).

Références Frappe :
  - DocType `Role`          → un rôle desk (préfixe « OTO » déjà porté par le
    contrat `rbac_50_roles.json`, anti-collision natif).
  - DocType `Custom DocPerm`→ personnalise les permissions d'un DocType existant
    SANS modifier le DocType lui-même (idéal pour appliquer par fixtures +
    `bench migrate`, réversible, ne casse pas les mises à jour d'app).

Ce module ne touche JAMAIS le VPS : il produit des dicts sérialisables.
"""

from __future__ import annotations

from typing import Any

# --------------------------------------------------------------------------- #
# Colonnes de permission d'un DocPerm Frappe, dans l'ordre natif du DocType.
# Toutes défaut 0 ; on ne lève à 1 que les verbes déclarés dans le contrat RBAC.
# --------------------------------------------------------------------------- #
DOCPERM_FLAGS: tuple[str, ...] = (
    "select",
    "read",
    "write",
    "create",
    "delete",
    "submit",
    "cancel",
    "amend",
    "report",
    "export",
    "import",
    "print",
    "email",
    "share",
    "set_user_permissions",
)

# Verbes que le contrat RBAC (`permissions_cibles[].actions`) peut émettre.
# TOUS correspondent 1:1 à un flag DocPerm natif ci-dessus → aucune traduction
# arbitraire, aucune invention. Un verbe hors de cet ensemble = erreur de contrat.
RBAC_ACTIONS: frozenset[str] = frozenset(
    {
        "read",
        "write",
        "create",
        "delete",
        "submit",
        "cancel",
        "report",
        "export",
        "print",
        "email",
        "share",
        "set_user_permissions",
    }
)

# Verbes DocPerm jamais pilotés par le contrat RBAC (restent à 0) — documentés
# pour prouver qu'aucun flag n'est activé « en douce ».
NON_PILOTES: frozenset[str] = frozenset(set(DOCPERM_FLAGS) - RBAC_ACTIONS)

# permlevel par défaut (0 = champs de base). Le contrat RBAC ne segmente pas par
# niveau de champ → on reste au niveau 0 (pas d'invention de permlevel).
DEFAULT_PERMLEVEL = 0

# `scope_donnees == "own"` → restriction Frappe native `if_owner` au niveau
# DocPerm. Les autres portées (equipe/entite/groupe) relèvent des `User
# Permission` par utilisateur (hors périmètre worker · appliqué VPS §7 du SPEC).
SCOPE_IF_OWNER = "own"


def role_fixture(erpnext_role_name: str) -> dict[str, Any]:
    """Construit un enregistrement de fixture `Role` Frappe minimal mais valide.

    `bench import-fixtures` complète les autres champs par leurs défauts DocType.
    On force `desk_access=1` (rôles back-office ERPNext) et `disabled=0`.
    """
    return {
        "doctype": "Role",
        "name": erpnext_role_name,
        "role_name": erpnext_role_name,
        "desk_access": 1,
        "disabled": 0,
        "two_factor_auth": 0,
        "restrict_to_domain": None,
    }


def docperm_fixture(
    doctype: str,
    role_name: str,
    actions: list[str],
    *,
    if_owner: bool,
    permlevel: int = DEFAULT_PERMLEVEL,
) -> dict[str, Any]:
    """Construit un enregistrement `Custom DocPerm` pour (DocType, Role).

    Les flags absents de `actions` restent à 0 (défaut sûr). `name` est
    volontairement omis → Frappe l'auto-nomme à l'import (hash déterministe côté
    serveur), ce qui évite d'inventer un identifiant ici.
    """
    unknown = set(actions) - RBAC_ACTIONS
    if unknown:
        raise ValueError(
            f"Verbe(s) RBAC inconnu(s) pour {doctype}/{role_name} : {sorted(unknown)}"
        )
    rec: dict[str, Any] = {
        "doctype": "Custom DocPerm",
        "parent": doctype,
        "parenttype": "DocType",
        "parentfield": "permissions",
        "role": role_name,
        "permlevel": permlevel,
        "if_owner": 1 if if_owner else 0,
    }
    wanted = set(actions)
    for flag in DOCPERM_FLAGS:
        rec[flag] = 1 if flag in wanted else 0
    return rec
