"""Modèle Frappe/ERPNext v15 : mapping `portail` RBAC → `Role Profile`.

Pourquoi ce module en plus de `../fixtures_gen` et `../userperm_gen`
-------------------------------------------------------------------
`fixtures_gen`  répond à « QUELS verbes sur QUEL DocType » (Role + Custom DocPerm).
`userperm_gen`  répond à « SUR QUELLES LIGNES » (scope_donnees → User Permission).
Il manque le **bundle assignable** : comment donner à un utilisateur, en UN seul
geste, l'ensemble des rôles de SON portail ? ERPNext v15 le fait nativement avec
le DocType `Role Profile` (contrainte #1 « ERPNext natif = priorité absolue » :
on n'invente aucun mécanisme de regroupement externe).

Référence Frappe (v15, `frappe/core/doctype/role_profile`) :
  - DocType `Role Profile` : champ `role_profile` (Data, nom du profil, unique)
    + table `roles` (child `Has Role`). Assigné à un utilisateur via le champ
    `User.role_profile_name` (ou la table `role_profiles`) → applique d'un coup
    tous les rôles du profil (comportement natif v15).
  - Child `Has Role` : un champ `role` (Link → `Role`) par ligne. Frappe
    auto-nomme la ligne et pose `parent`/`idx` à l'import → on ne les invente pas.

Anti-invention (#6) : les rôles proviennent tous du contrat `rbac_50_roles.json`
(champ `erpnext_role_name`). Le NOM du profil est une convention de nommage
déterministe dérivée de la clé `portail` (aucun chiffre ni libellé métier
fabriqué), documentée ci-dessous.

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

from __future__ import annotations

from typing import Any

# --------------------------------------------------------------------------- #
# Convention de nommage du profil (non un chiffre métier — une étiquette stable
# dérivée de la clé `portail`). Préfixe « OTO Portail » cohérent avec le préfixe
# « OTO » déjà porté par les rôles du contrat (anti-collision desk natif).
# --------------------------------------------------------------------------- #
ROLE_PROFILE_PREFIX = "OTO Portail"

# DocTypes ERPNext v15 natifs mobilisés (aucun DocType custom → rien à confirmer
# côté VPS pour ce livrable, contrairement aux `Custom DocPerm`).
ROLE_PROFILE_DOCTYPE = "Role Profile"
HAS_ROLE_DOCTYPE = "Has Role"


def role_profile_name(portail: str) -> str:
    """Nom Frappe déterministe d'un `Role Profile` pour une clé de portail.

    « ventes » → « OTO Portail Ventes ». La casse de titre est purement
    cosmétique et reproductible ; la clé brute reste tracée dans le manifeste.
    """
    if not portail:
        raise ValueError("portail vide : impossible de nommer le Role Profile.")
    return f"{ROLE_PROFILE_PREFIX} {portail.capitalize()}"


def has_role_row(erpnext_role_name: str) -> dict[str, Any]:
    """Construit une ligne enfant `Has Role` (référence un `Role` existant).

    `parent`/`idx`/`name` sont volontairement omis → Frappe les pose à l'import
    (on n'invente aucun identifiant de ligne).
    """
    return {
        "doctype": HAS_ROLE_DOCTYPE,
        "parentfield": "roles",
        "parenttype": ROLE_PROFILE_DOCTYPE,
        "role": erpnext_role_name,
    }


def role_profile_fixture(
    portail: str, role_names: list[str]
) -> dict[str, Any]:
    """Construit un enregistrement de fixture `Role Profile` avec ses `Has Role`.

    `role_names` est supposé déjà trié (déterminisme géré par le builder). Le
    champ `name` = `role_profile` (Frappe nomme un Role Profile par son libellé).
    """
    if not role_names:
        raise ValueError(f"portail {portail!r} sans rôle : Role Profile vide refusé.")
    name = role_profile_name(portail)
    return {
        "doctype": ROLE_PROFILE_DOCTYPE,
        "name": name,
        "role_profile": name,
        "roles": [has_role_row(r) for r in role_names],
    }
