"""The FleetSecure GRC app's portal pages, menus and role grants — one page
per mockup screen, built from Page Designer blocks.

Every page is a fluid canvas holding one Responsive Layout that stacks its
children and fits each to its content, so headers, toolbars, grids, tabs
and card lists flow like the mockups at any width.
"""

import uuid

W = 1200  # authored canvas width


def uid() -> str:
    return str(uuid.uuid4())


def block(type_: str, h: int = 80, **kw) -> dict:
    return {"id": uid(), "type": type_, "layout": {"x": 0, "y": 0, "w": W, "h": h}, **kw}


def fit(b: dict) -> dict:
    b["flow_height"] = "auto"
    return b


def stack(children: list[dict], gap: int = 16) -> dict:
    return block("layout", 400, flow_mode="stack", flow_gap=gap, children=[fit(c) for c in children])


def row(children: list[tuple[dict, float]], gap: int = 16) -> dict:
    kids = []
    for c, ratio in children:
        c["width_ratio"] = ratio
        kids.append(fit(c))
    return block("layout", 80, flow_mode="row", flow_gap=gap, flow_align_y="center", children=kids)


def header(title: str, subtitle: str = "", back: dict | None = None) -> dict:
    b = block("page_header", 64, label=title, subtitle=subtitle or None)
    if back:
        b["back_label"], b["back_action"] = back["label"], back["action"]
    return b


def tabs(groups: list[tuple[str, list[dict]]]) -> dict:
    return block("tabs", 600, groups=[{"id": uid(), "label": label, "children": [stack(children)]} for label, children in groups])


def text(t: str, style: str = "paragraph") -> dict:
    return block("text", 32, text=t, text_style=style)


def popup(label: str, children: list[dict], mode: str = "drawer") -> dict:
    p = block("popup", 640, label=label, popup_mode=mode, children=[stack(children)])
    p["layout"] = {"x": 0, "y": 0, "w": 520, "h": 640}
    return p


def navigate(page_id: str, pass_row: bool = True, params: list[dict] | None = None) -> dict:
    a = {"type": "navigate", "target_type": "page", "target_id": page_id, "pass_row_id": pass_row}
    if params:
        a["params"] = params
    return a


def flt(field: str, value: str, op: str = "equals") -> dict:
    return {"field": field, "operator": op, "value": value}


class Pages:
    def __init__(self, build):
        self.b = build
        self.ids: dict[str, str] = {}  # page key -> page id

    def ds(self, key: str) -> str:
        if key in self.b.forms:
            return f"form:{self.b.forms[key]['form_id']}"
        return f"master:{self.b.masters[key]['id']}"

    def noun(self, key: str) -> str:
        return self.b.defs[key]["noun"] if key in self.b.defs else ""

    def search(self, key: str) -> str:
        return self.b.defs[key]["search"] if key in self.b.defs else "Search..."

    # ---- data blocks ----------------------------------------------------------------

    def grid(self, key, columns, *, noun=None, search=None, crud=True, filters=None, page_size=10, view=None, h=420, **kw) -> dict:
        b = block(
            "data_list", h, data_source=self.ds(key), columns=columns, page_size=page_size, live_pagination=True,
            allow_create=crud, allow_edit=crud, allow_delete=crud, allow_column_tools=view in (None, "table"),
            record_noun=noun if noun is not None else self.noun(key), search_placeholder=search or self.search(key), **kw,
        )
        if filters:
            b["filters"] = filters
        if view:
            b["view_mode"] = view
        return b

    def cards(self, key, columns, *, title, badge=None, subtitle=None, description=None, per_row=3, **kw) -> dict:
        return self.grid(
            key, columns, view="cards", card_title_field=title, card_badge_field=badge, card_subtitle_field=subtitle,
            card_description_field=description, card_columns=per_row, allow_view_switch=True, **kw,
        )

    def tree(self, key, parent_field, columns, title, badge="status", subtitle=None, filters=None, h=420) -> dict:
        b = block(
            "tree", h, data_source=self.ds(key), tree_parent_field=parent_field, columns=columns, card_title_field=title,
            card_badge_field=badge, card_subtitle_field=subtitle, tree_expand_all=True, page_size=200,
        )
        if filters:
            b["filters"] = filters
        return b

    def detail(self, key, columns, h=280) -> dict:
        return block("detail_view", h, data_source=self.ds(key), columns=columns, filters=[flt("id", "{{recordId}}")])

    def stat(self, key, label, filters=None, color="#2563eb", param=None, value=None) -> dict:
        item = {"id": uid(), "label": label, "filters": filters or [], "color": color}
        if param:
            item["param_name"], item["param_value"] = param, value
        return item

    def stats(self, key, items, filters=None) -> dict:
        b = block("stat_cards", 96, data_source=self.ds(key), stat_items=items)
        if filters:
            b["filters"] = filters
        return b

    def picker(self, param: str, label: str, master_key: str, display: str) -> dict:
        field = {"name": param, "type": "lookup", "label": label, "required": False, "visibility_rules": [], "master_id": self.b.masters[master_key]["id"], "master_display_field": display}
        return block("form_field", 72, form_field=field, param_name=param)

    # ---- page saving ----------------------------------------------------------------

    def save(self, key: str, name: str, blocks: list[dict], parameters: list[str] | None = None) -> str:
        api = self.b.api
        existing = {p["name"]: p for p in api.get("/pages")}
        body = {"name": name, "blocks": blocks, "parameters": [{"name": p} for p in (parameters or [])], "fit_mode": "fluid", "canvas_width": W}
        if name in existing:
            page = api.patch(f"/pages/{existing[name]['id']}", body)
        else:
            page = api.post("/pages", body)
        api.post(f"/pages/{page['id']}/publish")
        self.ids[key] = page["id"]
        return page["id"]

    def simple(self, key, name, subtitle, ds_key, columns, **kw) -> str:
        return self.save(key, name, [stack([header(name, subtitle), self.grid(ds_key, columns, **kw)])])

    # ---- Core pages -----------------------------------------------------------------

    def build_core(self) -> None:
        S = self.simple
        S("country", "Country Management", "Manage countries master data.", "country", ["country_name", "iso_code", "default_currency", "time_zone", "status"])
        S("currency", "Currency Management", "Manage currencies master data.", "currency", ["code", "currency_name", "symbol", "status"])
        S("timezone", "Timezone Management", "Manage time zone master data.", "timezone", ["time_zone_code", "time_zone_name", "utc_offset", "status"])
        S("sector", "Sector Management", "Manage sector master data.", "sector", ["sector_code", "sector_name", "description", "status"])
        S("industry", "Industry Management", "Industry types, their sector groups and how widely they are adopted.", "industry",
          ["industry_name", "industry_code", "sector_group", "description", "frameworks_tagged", "tenants", "status"], noun="Industry")
        S("control_type", "Control Type Management", "Manage control types used across the control library.", "control_type", ["code", "control_type_name", "description", "status"])
        S("control_category", "Control Category Management", "Manage control categories used across the control library.", "control_category", ["code", "category_name", "description", "status"])
        S("evidence_type", "Evidence Type Management", "Manage the kinds of evidence a control point can accept.", "evidence_type", ["code", "evidence_type_name", "description", "status"])

        self.save("unit", "Unit Management", [stack([
            header("Unit Management", "Unit type hierarchy, the sections of each unit and the fields they capture."),
            tabs([
                ("Unit Types", [
                    row([(text("Root Unit Types", "subheading"), 1)]),
                    self.tree("unit_type", "parent_unit_type", ["unit_type_name", "unit_type_code", "industry", "status"], "unit_type_name", subtitle="unit_type_code", h=300),
                    self.grid("unit_type", ["unit_type_name", "unit_type_code", "industry", "parent_unit_type", "status"]),
                ]),
                ("Sections", [self.grid("unit_section", ["section_name", "section_code", "unit_type", "display_order", "status"])]),
                ("Field Definitions", [self.grid("unit_field", ["field_label", "field_slug", "field_type", "required", "unit_type", "section", "status"])]),
            ]),
        ])])

        self.save("result_type", "Result Type Management", [stack([
            header("Result Type Management", "How a KPI's result is captured, and the fields each result type asks for."),
            tabs([
                ("Result Type", [self.grid("result_type", ["code", "result_type_name", "description", "status"])]),
                ("Type Fields", [self.grid("result_type_field", ["result_type", "label", "field_type", "placeholder", "minimum", "maximum", "required", "status"])]),
            ]),
        ])])

        self.save("tenant", "Tenant Management", [stack([
            header("Tenant Management", "Onboard client tenants: company profile, subscription, deployment and their first administrator."),
            self.grid("client_tenant", ["display_name", "tenant_code", "url", "country", "package", "deployment", "admin_work_email", "status"], noun="Tenant", search="Search tenants..."),
        ])])

        self.save("package", "Package Management", [stack([
            header("Package Management", "Packages, the modules and features they include, and which industries they are offered to."),
            tabs([
                ("Assign Package", [self.grid("package_assignment", ["industry", "package", "is_default", "is_mandatory", "status"])]),
                ("Package", [self.grid("package", ["package_name", "code", "description", "max_users", "monthly_price", "annual_price", "status"])]),
                ("Module", [self.grid("module", ["module_name", "code", "description", "package", "monthly_price", "annual_price", "status"])]),
                ("Feature", [self.grid("feature", ["feature_name", "code", "description", "module", "included", "monthly_price", "status"])]),
            ]),
        ])])

        self.save("asset", "Asset Management", [stack([
            header("Asset Management", "Asset types and categories by industry, how categories nest and which types belong to them."),
            tabs([
                ("Asset Types", [self.grid("asset_type", ["code", "asset_type_name", "industry", "description", "status"])]),
                ("Asset Categories", [
                    self.tree("asset_category", "parent_category", ["category_name", "code", "industry", "status"], "category_name", subtitle="code", h=320),
                    self.grid("asset_category", ["code", "category_name", "industry", "parent_category", "description", "status"]),
                ]),
                ("Category Relations", [self.grid("category_relation", ["parent_category", "child_category", "notes", "status"])]),
                ("Type-Category Mapping", [self.grid("type_category_mapping", ["asset_type", "asset_category", "status"])]),
            ]),
        ])])

        self.save("tenant_user_core", "Tenant User Management", [stack([
            header("Tenant User Management", "People across client tenants, their app roles and hierarchy scope."),
            self.grid("tenant_user", ["full_name", "work_email", "app_roles", "hierarchy_scope", "status"]),
        ])])
        self.save("system_user_core", "System User Management", [stack([
            header("System User Management", "Platform staff and the roles they hold."),
            tabs([
                ("Users", [self.grid("system_user", ["full_name", "work_email", "roles", "last_login", "status"])]),
                ("Roles", [self.grid("system_role", ["role_name", "description", "status"])]),
            ]),
        ])])

        # Framework detail first — the framework cards open it.
        fw = "{{recordId}}"
        detail = self.save("framework_detail", "Framework Details", [stack([
            header("Framework", "Overview, versions, structure and mapped controls."),
            self.detail("framework", ["framework_code", "framework_name", "type", "regulator", "category", "active_version", "description", "industries", "status"], h=300),
            row([
                (self.stats("framework_version", [self.stat("framework_version", "Versions", color="#2563eb")], filters=[flt("framework", fw)]), 1),
                (self.stats("framework_domain", [self.stat("framework_domain", "Domains", [flt("node_type", "Domain")], "#7c3aed")], filters=[flt("framework", fw)]), 1),
                (self.stats("framework_objective", [self.stat("framework_objective", "Objectives", color="#0891b2")], filters=[flt("framework", fw)]), 1),
                (self.stats("control_mapping", [self.stat("control_mapping", "Mapped Controls", color="#059669")], filters=[flt("framework", fw)]), 1),
            ]),
            tabs([
                ("Versions", [self.grid("framework_version", ["version_label", "applicable_from", "applicable_to", "start_from", "mapped_controls", "tenants_adopting", "status"], filters=[flt("framework", fw)])]),
                ("Structure (Domains & Objectives)", [
                    self.tree("framework_domain", "parent_node", ["number", "title", "node_type", "status"], "title", subtitle="number", filters=[flt("framework", fw)], h=300),
                    self.grid("framework_objective", ["number", "title", "node_type", "domain", "parent_objective", "status"], filters=[flt("framework", fw)]),
                ]),
                ("Control Mapping", [self.grid("control_mapping", ["objective", "control", "mandatory", "cross_framework", "status"], filters=[flt("framework", fw)])]),
                ("Control Points", [self.grid("kpi", ["kpi_id", "kpi_title", "controls", "result_type", "required", "status"], crud=False)]),
            ]),
        ])], ["recordId"])
        listing = self.save("framework", "Framework Management", [stack([
            header("Framework Management", "Manage compliance frameworks and their lifecycle."),
            self.cards("framework", ["framework_code", "framework_name", "status", "type", "active_version", "domains", "objectives"], title="framework_name", subtitle="framework_code",
                       badge="status", search="Search frameworks…", row_click_action=navigate(detail), page_size=9),
        ])])
        self._fix_back_link("framework_detail", "Frameworks", listing)

        self.save("control", "Controls Management", [stack([
            header("Controls Management", "The control library, its KPIs and the types and categories that classify them."),
            tabs([
                ("Controls", [self.grid("control", ["control_id", "control_title", "category", "control_type", "requirement_statement", "status"])]),
                ("KPIs", [self.grid("kpi", ["kpi_id", "kpi_title", "controls", "description", "result_type", "required", "status"])]),
                ("Control Types", [self.grid("control_type", ["control_type_name", "code", "description", "status"])]),
                ("Control Categories", [self.grid("control_category", ["category_name", "code", "description", "status"])]),
            ]),
        ])])

        self.save("risk", "Risk Management", [stack([
            header("Risk Management", "Risk categories by industry and the risk universe they form."),
            tabs([
                ("Risk Category", [self.grid("risk_category", ["code", "category_name", "industry", "parent_category", "description", "status"])]),
                ("Risk Universe", [self.tree("risk_category", "parent_category", ["category_name", "code", "industry", "status"], "category_name", subtitle="code", h=480)]),
            ]),
        ])])

    def _fix_back_link(self, key: str, label: str, target: str) -> None:
        """A detail page's back link names the list page, which is saved after
        it (the list opens the detail page, so the detail page comes first)."""
        api = self.b.api
        page = api.get(f"/pages/{self.ids[key]}")

        def walk(bs):
            for b in bs or []:
                if b.get("type") == "page_header":
                    b["back_label"], b["back_action"] = label, navigate(target, False)
                    return True
                if walk(b.get("children")) or any(walk(g.get("children")) for g in b.get("groups") or []):
                    return True
            return False

        walk(page["blocks"])
        api.patch(f"/pages/{page['id']}", {"blocks": page["blocks"]})
        api.post(f"/pages/{page['id']}/publish")

    # ---- Tenant pages ---------------------------------------------------------------

    def build_tenant(self) -> None:
        dept_popup = popup("Department details", [self.detail("department", ["department_name", "code", "parent_department", "department_head", "location", "status"])])
        self.save("department", "Department Management", [
            stack([
                header("Department Management", "Departments of the organisation and how they nest."),
                tabs([
                    ("Departments", [self.grid("department", ["code", "department_name", "parent_department", "department_head", "status"],
                                                row_click_action={"type": "open_popup", "popup_block_id": dept_popup["id"], "pass_row_id": True})]),
                    ("Hierarchy", [self.tree("department", "parent_department", ["department_name", "code", "department_head", "status"], "department_name", subtitle="department_head", h=420)]),
                ]),
            ]),
            dept_popup,
        ])

        self.save("role", "Role Management", [stack([
            header("Role Management", "Tenant roles and what each may do in every module and feature."),
            tabs([
                ("Role Management", [self.grid("tenant_role", ["code", "role_name", "description", "status"])]),
                ("Permission Management", [self.grid("role_permission", ["role", "module", "feature", "can_view", "can_add", "can_edit", "can_delete", "can_approve"])]),
            ]),
        ])])

        self.save("system_user_tenant", "System Users", [stack([
            header("System User Management", "Accounts that administer this tenant."),
            self.grid("system_user", ["full_name", "work_email", "roles", "last_login", "status"]),
        ])])

        user_popup = popup("User details", [self.detail("tenant_user", ["full_name", "work_email", "employee_id", "phone", "primary_unit", "operational_role", "app_roles", "hierarchy_scope", "status"], h=360)])
        self.save("tenant_user", "Tenant Users", [
            stack([
                header("Tenant User Management", "People in this tenant, their app roles and the units they cover."),
                self.grid("tenant_user", ["full_name", "work_email", "app_roles", "hierarchy_scope", "status"],
                          row_click_action={"type": "open_popup", "popup_block_id": user_popup["id"], "pass_row_id": True}),
            ]),
            user_popup,
        ])

        kpi_cols = ["kpi", "status", "frequency", "every", "unit", "start_date", "notes"]
        self.save("kpi_assignment", "KPI Assignment", [stack([
            header("Assign KPIs", "Assign KPIs at organisational or unit level, edit collection frequency, and add more KPIs."),
            row([(self.picker("assignee", "Search and select a user to manage assigned KPIs", "tenant_user", "full_name"), 1)]),
            tabs([
                ("Organisational", [self.cards("kpi_assignment", kpi_cols, title="kpi", badge="status", subtitle="frequency", description="notes", noun="KPI Assignment",
                                              search="Search assigned KPIs...", filters=[flt("assignee", "{{assignee}}"), flt("level", "Organisational")])]),
                ("Unit level", [self.cards("kpi_assignment", kpi_cols, title="kpi", badge="status", subtitle="unit", description="notes", noun="KPI Assignment",
                                          search="Search assigned KPIs...", filters=[flt("assignee", "{{assignee}}"), flt("level", "Unit level")])]),
            ]),
        ])], ["assignee"])

        result_popup = popup("Entry details", [self.detail("kpi_result", ["kpi", "assignee", "scope", "unit", "period_label", "entry_date", "result_value", "outcome", "entry_status", "next_entry_date", "notes", "evidence"], h=420)])
        status_filter = [flt("entry_status", "{{entry_status}}", "contains")]
        self.save("kpi_results", "My KPI Results", [
            stack([
                header("My KPI Results", "View KPIs assigned to you, check their status, and open a KPI to review previous entries or submit new results."),
                self.stats("kpi_result", [
                    self.stat("kpi_result", "Assigned", color="#0f172a", param="entry_status", value=""),
                    self.stat("kpi_result", "Pending", [flt("entry_status", "Pending")], "#d97706", "entry_status", "Pending"),
                    self.stat("kpi_result", "Due Soon", [flt("entry_status", "Due Soon")], "#ea580c", "entry_status", "Due Soon"),
                    self.stat("kpi_result", "Overdue", [flt("entry_status", "Overdue")], "#dc2626", "entry_status", "Overdue"),
                    self.stat("kpi_result", "Up to Date", [flt("entry_status", "Up to Date")], "#059669", "entry_status", "Up to Date"),
                ]),
                self.cards("kpi_result", ["kpi", "entry_status", "period_label", "result_value", "outcome", "entry_date", "next_entry_date", "notes"], title="kpi", badge="entry_status",
                           subtitle="period_label", description="notes", noun="Result", search="Search assigned KPIs...", filters=status_filter, page_size=9,
                           row_click_action={"type": "open_popup", "popup_block_id": result_popup["id"], "pass_row_id": True}),
            ]),
            result_popup,
        ], ["entry_status"])

        unit_popup = popup("Unit details", [
            self.detail("unit", ["unit_name", "unit_code", "unit_type", "parent_unit", "operating_region", "location", "description", "status"], h=300),
            text("Assets", "subheading"),
            self.grid("unit_asset", ["asset_name", "asset_code", "asset_type", "location", "status"], filters=[flt("unit", "{{recordId}}")], page_size=5),
        ])
        self.save("fleet", "Fleet Management", [
            stack([
                header("Fleet Management", "Fleets, rigs and zones — open one for its details and assets."),
                self.cards("unit", ["unit_name", "status", "unit_code", "unit_type", "parent_unit", "operating_region", "location", "description"], title="unit_name", subtitle="unit_code",
                           badge="status", description="description", noun="Fleet Unit", page_size=9,
                           row_click_action={"type": "open_popup", "popup_block_id": unit_popup["id"], "pass_row_id": True}),
            ]),
            unit_popup,
        ])

        soa_detail = self.save("soa_details", "SOA Details", [stack([
            header("SOA Details", "The statement of applicability and its change history."),
            self.detail("soa", ["soa_title", "scope", "unit", "framework", "framework_version", "applicability", "domains_in_scope", "controls_in_scope", "kpis_in_scope", "excluded_controls", "revision", "status", "published_on", "owner", "justification"], h=420),
            text("Change history", "subheading"),
            block("timeline", 420, data_source=self.ds("soa_change"), columns=["summary", "change_type", "changed_by", "framework"], card_title_field="summary", card_badge_field="change_type",
                  card_subtitle_field="changed_by", timeline_date_field="changed_on", timeline_before_field="before_value", timeline_after_field="after_value",
                  filters=[flt("soa", "{{recordId}}")], page_size=100),
        ])], ["recordId"])
        changelog = self.save("soa_changelog", "SOA Change Log", [stack([
            header("SOA Change Log", "Every change to every SOA — newest first."),
            self.stats("soa_change", [
                self.stat("soa_change", "All changes", color="#0f172a", param="change_type", value=""),
                self.stat("soa_change", "Published", [flt("change_type", "Published")], "#059669", "change_type", "Published"),
                self.stat("soa_change", "Version upgraded", [flt("change_type", "Version upgraded")], "#7c3aed", "change_type", "Version upgraded"),
                self.stat("soa_change", "Applicability changed", [flt("change_type", "Applicability changed")], "#d97706", "change_type", "Applicability changed"),
            ]),
            block("timeline", 520, data_source=self.ds("soa_change"), columns=["summary", "change_type", "soa", "changed_by", "framework"], card_title_field="summary", card_badge_field="change_type",
                  card_subtitle_field="soa", timeline_date_field="changed_on", timeline_before_field="before_value", timeline_after_field="after_value",
                  filters=[flt("change_type", "{{change_type}}", "contains")], page_size=200),
            self.grid("soa_change", ["changed_on", "soa", "change_type", "summary", "changed_by"]),
        ])], ["change_type"])
        scope_filter = [flt("scope", "{{scope}}", "contains")]
        soa_list = self.save("soa", "SOA Management", [stack([
            header("SOA Management", "One organizational SOA, plus fleet and rig SOAs that can apply to multiple units."),
            self.stats("soa", [
                self.stat("soa", "All SOAs", color="#0f172a", param="scope", value=""),
                self.stat("soa", "Organizational", [flt("scope", "Organizational")], "#059669", "scope", "Organizational"),
                self.stat("soa", "Fleet", [flt("scope", "Fleet")], "#2563eb", "scope", "Fleet"),
                self.stat("soa", "Rig", [flt("scope", "Rig")], "#7c3aed", "scope", "Rig"),
            ]),
            self.cards("soa", ["soa_title", "status", "framework", "framework_version", "scope", "unit", "revision", "controls_in_scope", "published_on"], title="soa_title", subtitle="framework",
                       badge="status", noun="SOA", search="Search SOAs...", filters=scope_filter, page_size=9,
                       row_actions=[
                           {"id": uid(), "label": "View Details", "button_style": "secondary", "action": navigate(soa_detail), "param_mappings": []},
                           {"id": uid(), "label": "View Change Log", "button_style": "secondary", "action": navigate(changelog, False), "param_mappings": []},
                       ]),
        ])], ["scope"])
        self._fix_back_link("soa_details", "SOA Management", soa_list)
        self._fix_back_link("soa_changelog", "SOA Management", soa_list)

        self.save("audit_settings", "Audit Settings", [stack([
            header("Audit Settings", "How audit results are scored, the rating scales, and the escalation rules per audit type."),
            tabs([
                ("Scoring Heads", [self.grid("audit_scoring_head", ["display_order", "head_name", "weight", "description", "status"])]),
                ("Rating Scale", [self.grid("audit_rating_level", ["scale", "display_order", "label", "value", "colour", "status"])]),
                ("Audit Profiles", [self.grid("audit_setting", ["profile_name", "audit_type", "composition_rule", "auto_approved_extensions", "escalated_approver", "require_independent_validation", "status"])]),
            ]),
        ])])

        audit_detail = self.save("audit_detail", "Audit Details", [stack([
            header("Audit Details", "Plan, scope and the controls assigned to auditors."),
            self.detail("audit_plan", ["audit_title", "audit_type", "status", "soa_scope", "unit", "frequency", "start_date", "end_date", "lead_auditor", "framework", "framework_version", "controls_in_scope", "description"], h=380),
            text("Assigned controls", "subheading"),
            self.grid("audit_task", ["task_title", "control", "assignee", "due_date", "priority", "status"], filters=[flt("audit", "{{recordId}}")]),
        ])], ["recordId"])
        audits = self.save("audit", "Audit Management", [stack([
            header("Audit Management", "Plan, scope and assign audits against your Statement of Applicability. Track drafts, running work and completed reviews from one place."),
            self.stats("audit_plan", [
                self.stat("audit_plan", "All", color="#0f172a", param="status", value=""),
                self.stat("audit_plan", "In Progress", [flt("status", "In Progress")], "#d97706", "status", "In Progress"),
                self.stat("audit_plan", "Upcoming", [flt("status", "Upcoming")], "#2563eb", "status", "Upcoming"),
                self.stat("audit_plan", "Drafts", [flt("status", "Draft")], "#64748b", "status", "Draft"),
                self.stat("audit_plan", "Completed", [flt("status", "Completed")], "#059669", "status", "Completed"),
            ]),
            self.cards("audit_plan", ["audit_title", "status", "audit_type", "soa_scope", "unit", "start_date", "end_date", "lead_auditor", "description"], title="audit_title", badge="status",
                       subtitle="audit_type", description="description", noun="Audit", search="Search audits...", filters=[flt("status", "{{status}}", "contains")], page_size=9,
                       row_actions=[{"id": uid(), "label": "View details", "button_style": "secondary", "action": navigate(audit_detail), "param_mappings": []}]),
        ])], ["status"])
        self._fix_back_link("audit_detail", "Audit Management", audits)

        assess = self.b.forms["audit_assessment"]["form_id"]
        self.save("my_tasks", "My Tasks", [stack([
            header("My Tasks", "Controls assigned to you for audit — record an assessment when your testing is done."),
            self.stats("audit_task", [
                self.stat("audit_task", "All tasks", color="#0f172a", param="status", value=""),
                self.stat("audit_task", "Open", [flt("status", "Open")], "#2563eb", "status", "Open"),
                self.stat("audit_task", "In Progress", [flt("status", "In Progress")], "#d97706", "status", "In Progress"),
                self.stat("audit_task", "Rework", [flt("status", "Rework")], "#dc2626", "status", "Rework"),
                self.stat("audit_task", "Submitted", [flt("status", "Submitted")], "#7c3aed", "status", "Submitted"),
                self.stat("audit_task", "Approved", [flt("status", "Approved")], "#059669", "status", "Approved"),
            ]),
            self.cards("audit_task", ["task_title", "status", "audit", "control", "assignee", "due_date", "priority", "notes"], title="task_title", badge="status", subtitle="audit",
                       description="notes", noun="Task", search="Search my tasks...", filters=[flt("status", "{{status}}", "contains")], page_size=9,
                       row_actions=[{"id": uid(), "label": "Record assessment", "button_style": "primary", "action": {"type": "open_form", "form_id": assess}, "param_mappings": [{"row_field": "id", "param_name": "audit_task"}]}]),
            text("Assessments", "subheading"),
            self.grid("audit_assessment", ["audit_task", "design_effectiveness", "operational_effectiveness", "result", "assessed_on", "findings"], noun="Assessment", search="Search assessments..."),
        ])], ["status"])

    # ---- menus and role grants ------------------------------------------------------

    CORE_MENU = [
        ("Administration & Master Data", [("Country Management", "globe", "country"), ("Currency Management", "coins", "currency"), ("Timezone Management", "clock", "timezone"),
                                          ("Industry Management", "factory", "industry"), ("Sector Management", "layers", "sector"), ("Unit Management", "ruler", "unit"),
                                          ("Result Type Management", "badge-check", "result_type"), ("Control Type Management", "tags", "control_type"),
                                          ("Control Category Management", "folder", "control_category")]),
        ("Organization & Governance", [("Tenant Management", "building", "tenant"), ("Package Management", "package", "package"), ("Asset Management", "boxes", "asset")]),
        ("User Management", [("Tenant User Management", "users", "tenant_user_core"), ("System User Management", "user-cog", "system_user_core")]),
        ("GRC & Compliance", [("Framework Management", "shield-check", "framework"), ("Control Management", "list-checks", "control")]),
        ("Risk Management", [("Risk Category Management", "shield-alert", "risk")]),
        ("Evidence & Documentation", [("Evidence Type Management", "folder-open", "evidence_type")]),
    ]
    TENANT_MENU = [
        (None, [("Department Management", "network", "department"), ("Role Management", "id-card", "role"), ("System User Management", "user-cog", "system_user_tenant"),
                ("Tenant User Management", "users", "tenant_user"), ("KPI Assignment", "target", "kpi_assignment"), ("My KPI Results", "pen-line", "kpi_results"),
                ("Fleet Management", "ship", "fleet"), ("SOA Management", "file-check", "soa")]),
        ("Audit", [("Audit Settings", "sliders", "audit_settings"), ("Audit Management", "clipboard-check", "audit"), ("My Tasks", "square-check", "my_tasks")]),
    ]
    # Pages reached from a list page rather than the menu.
    CORE_EXTRA = ["framework_detail"]
    TENANT_EXTRA = ["soa_details", "soa_changelog", "audit_detail"]

    def build_menus(self) -> None:
        api = self.b.api
        existing = {(m["label"], m.get("group_label"), m.get("target_id")) for m in api.get("/menus", limit=500)}
        order = 100
        for groups in (self.CORE_MENU, self.TENANT_MENU):
            for group, items in groups:
                for label, icon, page_key in items:
                    order += 1
                    if (label, group, self.ids[page_key]) in existing:
                        continue
                    api.post("/menus", {"label": label, "icon": icon, "link_type": "existing_feature", "target_type": "page", "target_id": self.ids[page_key],
                                        "group_label": group, "sort_order": order, "status": "active", "audience": "vendor"})

    def _sources(self, blocks: list[dict]) -> tuple[set[str], set[str]]:
        masters, forms = set(), set()

        def walk(bs):
            for b in bs or []:
                src = b.get("data_source") or ""
                if src.startswith("master:"):
                    masters.add(src[7:])
                elif src.startswith("form:"):
                    forms.add(src[5:])
                ff = b.get("form_field") or {}
                if ff.get("master_id"):
                    masters.add(ff["master_id"])
                for a in [b.get("row_click_action"), *[ra.get("action") for ra in b.get("row_actions") or []]]:
                    if a and a.get("form_id"):
                        forms.add(a["form_id"])
                walk(b.get("children"))
                for g in b.get("groups") or []:
                    walk(g.get("children"))

        walk(blocks)
        return masters, forms

    def grant_roles(self) -> None:
        """Each role sees its own pages; it may use every datasource and form
        those pages show, plus whatever their links and lookups point at."""
        from .build import ROLE_CORE, ROLE_TENANT

        api = self.b.api
        by_id = {m["id"]: m for m in api.get("/masters", limit=500)}
        forms_by_id = {f["form_id"]: f for f in api.get("/forms")}
        plan = {
            ROLE_CORE: ([k for _, items in self.CORE_MENU for *_, k in items] + self.CORE_EXTRA, "country"),
            ROLE_TENANT: ([k for _, items in self.TENANT_MENU for *_, k in items] + self.TENANT_EXTRA, "soa"),
        }
        for role_name, (page_keys, landing) in plan.items():
            masters, forms = set(), set()
            for k in page_keys:
                page = api.get(f"/pages/{self.ids[k]}")
                m, f = self._sources(page["blocks"])
                masters |= m
                forms |= f
            # Links and lookups reach further, a few levels deep.
            for _ in range(4):
                for fid in list(forms):
                    for fd in forms_by_id.get(fid, {}).get("field_definitions", []):
                        if fd.get("master_id"):
                            masters.add(fd["master_id"])
                for mid in list(masters):
                    for fd in by_id.get(mid, {}).get("field_schema", []):
                        if fd.get("reference_master_id"):
                            masters.add(fd["reference_master_id"])
                        if fd.get("reference_form_id"):
                            forms.add(fd["reference_form_id"])
            role = self.b.roles[role_name]
            matrix = {"pages": [self.ids[k] for k in page_keys], "masters": sorted(masters), "forms": sorted(forms)}
            api.patch(f"/roles/{role['id']}", {"action_matrix": matrix, "default_page_id": self.ids[landing]})
            self.b.log(f"  {role_name}: {len(page_keys)} pages, {len(masters)} datasources, {len(forms)} forms")

    def grant_all_data(self) -> None:
        """Before the pages exist: let both roles use every form, so the portal
        users can add the sample submissions."""
        for role in self.b.roles.values():
            self.b.api.patch(f"/roles/{role['id']}", {"action_matrix": {"forms": [f["form_id"] for f in self.b.forms.values()], "masters": [m["id"] for m in self.b.masters.values()]}})
