"""Assemblage du fixture DocType `OTO Dossier Vente` — cross-cohérent workflow.

Entrées :
  - `doctype_spec.json`         : structure métier (libellés + types de champ).
  - `workflow_vente_spec.json`  : le pipeline (source UNIQUE du nom du DocType,
    du champ d'état, des valeurs de statut, des rôles). Réutilisé, jamais copié.
  - `RoleResolver` (rbac_50_roles.json) : `role_id → erpnext_role_name`.

Sortie : un bundle déterministe `{manifest, doctype}` reproductible bit-à-bit.

Anti-dérive (#6, zéro invention / zéro duplication · workflow #5) :
  - Le NOM du DocType, le champ d'état (`workflow_state`) et le champ de valeur
    machine (`statut_pipeline`) proviennent du contrat workflow — jamais réécrits
    en dur ici. Si le workflow renomme le champ, ce DocType suit automatiquement.
  - `is_submittable` est DÉDUIT des `doc_status` du workflow (1/2 ⇒ soumissible).
  - Les permissions sont DÉDUITES des rôles réellement cités par le workflow
    (édition de brouillon ⇒ write/create ; transition vers soumis ⇒ submit ;
    vers annulé ⇒ cancel). Aucun rôle ni chiffre fabriqué.
"""

from __future__ import annotations

import os
import sys
from typing import Any

from . import frappe

# Réutilisation (workflow #5) du module CRM voisin : le champ de valeur machine
# (`_UPDATE_FIELD`) et le résolveur de rôles sont importés, jamais redéfinis.
_HERE = os.path.dirname(os.path.abspath(__file__))
_CRM = os.path.normpath(os.path.join(_HERE, "..", ".."))  # 05_deliverables_mvp/crm/
sys.path.insert(0, _CRM)

from workflow_vente.wflib.builder import _UPDATE_FIELD  # noqa: E402
from workflow_vente.wflib.rbac import RoleResolver  # noqa: E402

WORKFLOW_SPEC_REL = os.path.join("..", "workflow_vente", "workflow_vente_spec.json")

# Drapeaux de docfield que le spec métier peut porter (le reste = mise en page).
_SPEC_FLAGS = ("reqd", "read_only", "in_list_view", "in_standard_filter", "hidden", "bold")


def _ordered_unique(seq: list[str]) -> list[str]:
    seen: set[str] = set()
    out: list[str] = []
    for x in seq:
        if x not in seen:
            seen.add(x)
            out.append(x)
    return out


def _spec_docfield(f: dict) -> dict[str, Any]:
    flags = {k: f[k] for k in _SPEC_FLAGS if k in f}
    return frappe.docfield(
        f["fieldname"],
        f["fieldtype"],
        label=f.get("label"),
        options=f.get("options"),
        default=f.get("default"),
        flags=flags,
    )


def _derive_permissions(wf_spec: dict, resolver: RoleResolver) -> tuple[list[dict], dict[str, set[str]]]:
    """Déduit les DocPerm des rôles cités par le workflow.

    Retour : (lignes de permission triées, capabilities par role_id) — la 2e
    valeur alimente le manifeste de traçabilité.
    """
    docstatus_by_state = {s["state"]: s["doc_status"] for s in wf_spec["states"]}
    caps: dict[str, set[str]] = {}

    def grant(role_id: str, perms: set[str]) -> None:
        caps.setdefault(role_id, set()).update(perms)

    # Un rôle `allow_edit` sur un état édite le dossier dans cet état ⇒ read+write
    # (Frappe exige write pour modifier, y compris un document soumis). Seuls les
    # états BROUILLON (doc_status 0) sont créables ⇒ + create.
    for s in wf_spec["states"]:
        grant(s["role_id"], {"read", "write"})
        if s["doc_status"] == "0":
            grant(s["role_id"], {"create"})
    for t in wf_spec["transitions"]:
        grant(t["role_id"], {"read"})
        nxt = docstatus_by_state[t["next_state"]]
        if nxt == "1":
            grant(t["role_id"], {"submit"})
        elif nxt == "2":
            grant(t["role_id"], {"cancel", "amend"})

    rows = [
        frappe.permission_row(resolver.erpnext_name(rid), perms)
        for rid, perms in sorted(caps.items(), key=lambda kv: resolver.erpnext_name(kv[0]))
    ]
    return rows, caps


def build_bundle(spec: dict, wf_spec: dict, resolver: RoleResolver) -> dict[str, Any]:
    """Transforme les contrats en bundle `{manifest, doctype}` déterministe."""
    dt_cfg = spec["doctype"]

    # --- 1. Facettes dérivées du workflow (source unique, anti-dérive) --------
    doctype_name = wf_spec["document_type"]
    state_field = wf_spec["workflow_state_field"]          # p.ex. "workflow_state"
    state_names = _ordered_unique([s["state"] for s in wf_spec["states"]])
    pipeline_values = _ordered_unique([s["update_value"] for s in wf_spec["states"]])
    max_docstatus = max(s["doc_status"] for s in wf_spec["states"])  # "0" < "1" < "2"
    is_submittable = 1 if max_docstatus >= "1" else 0

    # --- 2. Champs de pilotage (injectés, non re-saisis dans le spec métier) --
    fields: list[dict] = [
        frappe.naming_series_field(dt_cfg["naming_series"]),
        frappe.section_break("sb_pipeline", "Pipeline"),
        frappe.docfield(
            state_field, "Select",
            label="État du workflow",
            options="\n" + "\n".join(state_names),
            flags={"read_only": 1, "in_list_view": 1, "in_standard_filter": 1},
        ),
        frappe.docfield(
            _UPDATE_FIELD, "Select",
            label="Statut pipeline (machine)",
            options="\n" + "\n".join(pipeline_values),
            flags={"read_only": 1, "hidden": 1},
        ),
    ]

    # --- 3. Champs métier (spec) : une Section Break par groupe ---------------
    linked_doctypes: set[str] = set()
    for gi, group in enumerate(spec["field_groups"]):
        slug = "sb_" + "".join(
            c if (c.isascii() and c.isalnum()) else "_" for c in group["section"].lower()
        ).strip("_")
        fields.append(frappe.section_break(f"{slug}_{gi}", group["section"]))
        for f in group["fields"]:
            fields.append(_spec_docfield(f))
            if f["fieldtype"] == "Link" and f.get("options"):
                linked_doctypes.add(f["options"])

    # --- 4. Permissions déduites du workflow ----------------------------------
    permissions, caps = _derive_permissions(wf_spec, resolver)

    # --- 5. Document DocType ---------------------------------------------------
    doctype = frappe.doctype_doc(
        name=doctype_name,
        module=dt_cfg["module"],
        is_submittable=is_submittable,
        title_field=dt_cfg["title_field"],
        search_fields=dt_cfg["search_fields"],
        track_changes=dt_cfg.get("track_changes", 1),
        track_seen=dt_cfg.get("track_seen", 0),
        fields=fields,
        permissions=permissions,
    )

    # --- 6. Manifeste de traçabilité ------------------------------------------
    data_fields = [f for f in fields if f["fieldtype"] not in frappe.LAYOUT_FIELDTYPES]
    sections = [f for f in fields if f["fieldtype"] == "Section Break"]
    manifest = {
        "generated_from": "doctype_spec.json",
        "workflow_source": "workflow_vente_spec.json",
        "rbac_source": "rbac_50_roles.json",
        "source_version": spec["version"],
        "workflow_version": wf_spec["version"],
        "doctype_name": doctype_name,
        "custom": bool(doctype["custom"]),
        "is_submittable": bool(is_submittable),
        "workflow_state_field": state_field,
        "pipeline_value_field": _UPDATE_FIELD,
        "counts": {
            "fields": len(fields),
            "data_fields": len(data_fields),
            "sections": len(sections),
            "permissions": len(permissions),
            "pipeline_states": len(state_names),
            "pipeline_values": len(pipeline_values),
        },
        # Non natif : à créer / confirmer sur le VPS avant import (SPEC §7).
        "module_a_confirmer": dt_cfg["module"],
        "doctypes_lies_a_confirmer": sorted(linked_doctypes),
        "roles_rbac_utilises": [
            {
                "role_id": rid,
                "erpnext_role_name": resolver.erpnext_name(rid),
                "permissions": sorted(perms),
            }
            for rid, perms in sorted(caps.items())
        ],
        "hand_off_vps": (
            "Importer ce DocType (bench migrate / import-fixtures) AVANT le "
            "Workflow 'OTO Vente Pipeline' qui le cible (crm/workflow_vente/out/)."
        ),
    }

    return {"manifest": manifest, "doctype": doctype}
