"""Construction du bundle de Workspaces depuis le contrat RBAC + la spec portails.

Entrees :
  - contract : rbac_50_roles.json (deja valide par rbac.schema.json en CI). Source
    de verite des ROLES par portail et des DOCTYPES autorises (permissions_cibles).
  - spec     : portails_spec.json. Ne fait QUE la mise en page (cartes, raccourcis,
    theme). Ne declare aucun DocType ni role hors du contrat.

Sortie : bundle deterministe :
  - workspace -> 1 fixture `Workspace` par portail metier (5), avec ses tables
    enfant `shortcuts` / `links` / `roles`.
  - manifest  -> tracabilite (comptes, roles couverts, DocTypes custom a creer
    cote VPS, tokens de marque, note module a_confirmer).

Anti-invention (#6), garanti ici + par les invariants du CLI :
  - un lien de carte / un raccourci ne peut viser qu'un DocType present dans les
    `permissions_cibles` du portail (le portail a prouvablement le droit dessus) ;
  - la couverture est exhaustive (chaque DocType autorise apparait dans EXACTEMENT
    une carte) : aucun DocType silencieusement oublie, aucun invente ;
  - le flag `custom` (natif v15 vs DocType OTO a creer) vient du CONTRAT, jamais
    de la spec (source unique) ;
  - les roles proviennent du contrat (meme logique que le generateur Role Profile).

Determinisme : portails dans l'ordre de la spec ; roles tries par nom ; liens dans
l'ordre spec des cartes -> sortie reproductible bit-a-bit (diffable, re-generable
sans horodatage).
"""

from __future__ import annotations

from typing import Any

from . import frappe


def roles_by_portail(contract: dict) -> dict[str, list[str]]:
    """`erpnext_role_name` tries par portail (source de verite des roles)."""
    grouped: dict[str, list[str]] = {}
    for r in contract["roles"]:
        grouped.setdefault(r["portail"], []).append(r["erpnext_role_name"])
    for portail in grouped:
        grouped[portail] = sorted(set(grouped[portail]))
    return grouped


def doctypes_by_portail(contract: dict) -> dict[str, set[str]]:
    """Ensemble des DocTypes cites dans les permissions_cibles, par portail."""
    grouped: dict[str, set[str]] = {}
    for r in contract["roles"]:
        bucket = grouped.setdefault(r["portail"], set())
        for perm in r.get("permissions_cibles", []):
            if isinstance(perm, dict) and perm.get("doctype"):
                bucket.add(perm["doctype"])
    return grouped


def custom_flags(contract: dict) -> dict[str, bool]:
    """Flag `custom` par DocType (natif v15 = False), depuis le contrat (source unique)."""
    flags: dict[str, bool] = {}
    for r in contract["roles"]:
        for perm in r.get("permissions_cibles", []):
            if isinstance(perm, dict) and perm.get("doctype"):
                flags[perm["doctype"]] = bool(perm.get("custom", False))
    return flags


def _links_for_portail(portail: dict) -> list[dict[str, Any]]:
    """Table `Workspace Link` : Card Break + Link, dans l'ordre de la spec."""
    links: list[dict[str, Any]] = []
    for carte in portail["cartes"]:
        doctypes = carte["doctypes"]
        links.append(frappe.card_break_row(carte["titre"], len(doctypes)))
        for dt in doctypes:
            links.append(frappe.link_row(dt))
    return links


def build_bundle(contract: dict, spec: dict) -> dict[str, Any]:
    """Transforme (contrat RBAC + spec portails) en bundle de Workspaces."""
    roles_map = roles_by_portail(contract)
    dt_map = doctypes_by_portail(contract)
    flags = custom_flags(contract)

    workspaces: list[dict] = []
    ws_meta: list[dict] = []
    for portail in spec["portails"]:
        key = portail["key"]
        roles = roles_map.get(key, [])
        links = _links_for_portail(portail)
        workspaces.append(frappe.workspace_fixture(portail, roles, links))

        couverts = sorted({dt for carte in portail["cartes"] for dt in carte["doctypes"]})
        customs = sorted(dt for dt in couverts if flags.get(dt, False))
        ws_meta.append(
            {
                "portail": key,
                "workspace": portail["label"],
                "nb_cartes": len(portail["cartes"]),
                "nb_raccourcis": len(portail["raccourcis"]),
                "nb_liens": len(couverts),
                "nb_roles": len(roles),
                "doctypes_custom": customs,
            }
        )

    total_customs = sorted({dt for m in ws_meta for dt in m["doctypes_custom"]})
    manifest = {
        "generated_from": spec["generated_from"],
        "spec_version": spec["version"],
        "source_version": contract["version"],
        "roadmap_ref": spec["roadmap_ref"],
        "counts": {
            "workspaces": len(workspaces),
            "roles_couverts": sum(m["nb_roles"] for m in ws_meta),
            "liens_total": sum(m["nb_liens"] for m in ws_meta),
            "raccourcis_total": sum(m["nb_raccourcis"] for m in ws_meta),
        },
        "brand": spec["brand"],
        "module_note": (
            "Workspace.module laisse a None (a_confirmer) : l'agent ERPNext Backend "
            "le fixe au module de l'app OTO a l'import (#8). Le worker ne fabrique "
            "pas de nom d'app."
        ),
        "doctypes_custom_a_creer": total_customs,
        "workspaces": ws_meta,
    }

    return {"manifest": manifest, "workspace": workspaces}
