"""Reusable App Block presentation variants (FL-D2).

Variants are authored display/edit modes of existing Starter contracts.
They are not capabilities, App Blocks, Starters, or Feature Packs.
"""

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from src.domain.app_blocks.schemas import AppBlockManifest

VARIANT_OWNERS: dict[str, str] = {
    "status_grouping": "admin.record_table",
    "split_list_detail": "admin.record_table",
    "compact_dashboard": "admin.record_table",
    "kanban_grouping": "admin.record_table",
    "relationship_view": "admin.record_table",
    "portal_read_only": "admin.record_detail",
    "staff_table": "booking.appointment",
    "calendar": "booking.appointment",
}

VARIANT_CONTRACTS: dict[str, str] = {
    "status_grouping": "record.table",
    "split_list_detail": "record.table",
    "compact_dashboard": "record.summary",
    "kanban_grouping": "record.table",
    "relationship_view": "record.table",
    "portal_read_only": "record.detail",
    "staff_table": "record.table",
    "calendar": "record.calendar",
}

COMPACT_DASHBOARD_FORBIDDEN = frozenset(
    {
        "reporting",
        "analytics",
        "time_series",
        "report_builder",
        "metric_definition",
        "historical_aggregation",
    }
)
KANBAN_FORBIDDEN = frozenset(
    {
        "workflow.engine",
        "assignment.engine",
        "automation",
        "transition_graph_authoring",
    }
)
HELD_PORTAL_BLOCKS = frozenset({"membership.member", "billing.invoice"})
FORBIDDEN_PRODUCT_TABLE_BLOCKS = frozenset(
    {
        "customer-table",
        "member-table",
        "lead-table",
        "ticket-table",
        "appointment-table",
        "invoice-table",
        "case-kanban",
        "customer-dashboard",
        "member-dashboard",
    }
)

CODEGEN_SURFACE_RESOLUTION: dict[str, tuple[str, str]] = {
    "ticket queue grouped by status": ("support.ticket", "status_grouping"),
    "case split-view": ("case.management", "split_list_detail"),
    "lead kanban": ("crm.lead", "kanban_grouping"),
    "appointment calendar": ("booking.appointment", "calendar"),
    "appointment table": ("booking.appointment", "list"),
    "attachment upload": ("files.attachments", "upload"),
    "read-only portal record": ("admin.record_detail", "portal_read_only"),
}


class PresentationVariant(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str
    owner_block: str
    component_contract: str
    mutation_contract: str
    data_contract: str
    new_capability_required: bool = False
    guards: list[str] = Field(default_factory=list)


def variant_catalog() -> dict[str, PresentationVariant]:
    return {
        "status_grouping": PresentationVariant(
            id="status_grouping",
            owner_block="admin.record_table",
            component_contract="record.table",
            mutation_contract="existing status editor / workflow.transition only",
            data_contract="data.entity listRecords",
            guards=["must_not_author_transitions", "must_not_create_sla"],
        ),
        "split_list_detail": PresentationVariant(
            id="split_list_detail",
            owner_block="admin.record_table",
            component_contract="record.table",
            mutation_contract="none",
            data_contract="data.entity listRecords + getRecord",
            guards=["layout_only"],
        ),
        "compact_dashboard": PresentationVariant(
            id="compact_dashboard",
            owner_block="admin.record_table",
            component_contract="record.summary",
            mutation_contract="none",
            data_contract="existing listRecords counts and grouping",
            guards=["not_reporting", "no_analytical_backend"],
        ),
        "kanban_grouping": PresentationVariant(
            id="kanban_grouping",
            owner_block="admin.record_table",
            component_contract="record.table",
            mutation_contract="existing patchRecord status only",
            data_contract="data.entity listRecords",
            guards=["not_workflow_engine", "no_invented_transitions"],
        ),
        "relationship_view": PresentationVariant(
            id="relationship_view",
            owner_block="admin.record_table",
            component_contract="record.table",
            mutation_contract="none",
            data_contract="authored subject_entity_key / subject_record_id",
            guards=["no_synthetic_joins"],
        ),
        "portal_read_only": PresentationVariant(
            id="portal_read_only",
            owner_block="admin.record_detail",
            component_contract="record.detail",
            mutation_contract="none",
            data_contract="existing getRecord + field visibility",
            guards=["not_membership_portal", "not_billing_portal"],
        ),
        "staff_table": PresentationVariant(
            id="staff_table",
            owner_block="booking.appointment",
            component_contract="record.table",
            mutation_contract="booking.cancel when reservation id is known",
            data_contract="GET /api/bookings/resources",
            guards=["not_data_entity", "not_calendar_sync"],
        ),
        "calendar": PresentationVariant(
            id="calendar",
            owner_block="booking.appointment",
            component_contract="record.calendar",
            mutation_contract="booking.reserve / booking.cancel",
            data_contract="GET /api/bookings/resources/:key/availability",
            guards=["not_generic_calendar_capability", "not_google_calendar"],
        ),
    }


def slot_presentation(slot: Any) -> str:
    value = getattr(slot, "presentation", None)
    if value:
        return str(value)
    if getattr(slot, "id", None) in VARIANT_OWNERS:
        return str(slot.id)
    return "default"


def presentations_on(manifest: AppBlockManifest) -> frozenset[str]:
    found: set[str] = set()
    for slot in manifest.slots:
        if slot.type != "ui_slot":
            continue
        kind = slot_presentation(slot)
        if kind and kind != "default":
            found.add(kind)
    return frozenset(found)


def resolve_codegen_surface(need: str) -> tuple[str, str]:
    """Map an application presentation need to an authored block slot."""
    resolved = CODEGEN_SURFACE_RESOLUTION.get(need.strip().lower())
    if resolved is None:
        raise KeyError(need)
    return resolved


def assert_variant_guards(manifest: AppBlockManifest) -> None:
    if "compact_dashboard" in presentations_on(manifest):
        provided = {item.id for item in manifest.provides}
        if provided & COMPACT_DASHBOARD_FORBIDDEN:
            raise ValueError(f"{manifest.id} compact_dashboard collided with Reporting")
    if "kanban_grouping" in presentations_on(manifest) and any(
        dep.id in KANBAN_FORBIDDEN for dep in manifest.requires
    ):
        raise ValueError(f"{manifest.id} kanban must not require a workflow engine")
    if "portal_read_only" in presentations_on(manifest) and manifest.id in HELD_PORTAL_BLOCKS:
        raise ValueError(f"{manifest.id} portal remains held")
    if manifest.id in FORBIDDEN_PRODUCT_TABLE_BLOCKS:
        raise ValueError(f"forbidden product-specific table block {manifest.id}")


def validate_variant_slot(manifest: AppBlockManifest, slot: Any) -> None:
    kind = slot.presentation
    if not kind or kind == "default":
        return
    if kind not in VARIANT_OWNERS:
        raise ValueError(f"{manifest.id} unknown presentation {kind}")
    expected = VARIANT_CONTRACTS[kind]
    if slot.component_contract != expected:
        raise ValueError(
            f"{manifest.id} slot {slot.id} presentation {kind} must use {expected}"
        )
    if kind == "portal_read_only" and manifest.id in HELD_PORTAL_BLOCKS:
        raise ValueError(f"{manifest.id} must not author portal_read_only")
    if kind == "compact_dashboard" and slot.component_contract != "record.summary":
        raise ValueError("compact_dashboard is record.summary, not Reporting")
    if kind == "kanban_grouping" and slot.group_by_field not in {None, "status", "stage"}:
        raise ValueError("kanban_grouping may only group by an authored status/stage field")
    if kind in {"staff_table", "calendar"} and manifest.id != "booking.appointment":
        raise ValueError(f"{kind} is owned by booking.appointment")
    if kind == "staff_table" and "data.entity" in {
        dep.id for dep in manifest.requires if dep.kind == "primitive"
    }:
        raise ValueError("booking staff_table must not bind data.entity")
    if kind == "calendar" and slot.query_source != "primitive:booking.availability":
        raise ValueError("booking calendar must query booking.availability")
    if kind == "relationship_view" and not slot.group_by_field:
        if manifest.id != "admin.record_table":
            raise ValueError(f"{manifest.id} relationship_view needs an authored field")
