"""Presentation projection of a GenerationPlan.

This is not a capability registry. It names the design recipe, staff modules,
and notification templates that already-admitted blocks and events support.
"""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, ConfigDict, Field

from src.domain.email.catalog import EMAIL_TEMPLATE_CATALOG

DesignRecipe = Literal["neutral", "clinic", "commerce", "hospitality", "professional"]
SelectionSource = Literal["current_prompt", "installed_baseline", "discovery"]

_ADMIN_MODULES: tuple[tuple[str, str, str, Literal["staff", "admin"]], ...] = (
    ("forms.contact", "inbox", "Inbox", "staff"),
    ("forms.quote_request", "quotes", "Quote requests", "staff"),
    ("crm.customer", "customers", "Customers", "staff"),
    ("crm.lead", "leads", "Leads", "staff"),
    ("booking.appointment", "bookings", "Bookings", "staff"),
    ("catalog.product", "products", "Products", "staff"),
    ("orders.order", "orders", "Orders", "staff"),
    ("inventory.stock", "inventory", "Inventory", "staff"),
    ("content.collection", "content", "Content", "staff"),
    ("files.attachments", "files", "Files", "staff"),
    ("workflow.approval", "approvals", "Approvals", "staff"),
    ("notifications.in_app", "notifications", "Notifications", "staff"),
    ("payments.checkout", "payments", "Payments", "admin"),
)

_PUBLIC_SURFACES: tuple[tuple[str, str], ...] = (
    ("forms.contact", "contact"),
    ("forms.quote_request", "quote"),
    ("booking.appointment", "booking"),
    ("catalog.product", "catalog"),
    ("content.services", "services"),
)

_DESIGN: dict[DesignRecipe, tuple[str, str, str]] = {
    "neutral": ("#1f2937", "12px", "Inter, ui-sans-serif, system-ui, sans-serif"),
    "clinic": ("#0f766e", "16px", "Source Serif 4, Georgia, serif"),
    "commerce": ("#111827", "8px", "Inter, ui-sans-serif, system-ui, sans-serif"),
    "hospitality": ("#9a3412", "14px", "Fraunces, Georgia, serif"),
    "professional": ("#1e3a8a", "10px", "Inter, ui-sans-serif, system-ui, sans-serif"),
}


class ExperienceDesign(BaseModel):
    model_config = ConfigDict(extra="forbid")

    recipe: DesignRecipe = "neutral"
    accent: str
    radius: str
    font: str


class ExperienceModule(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    label: str
    audience: Literal["staff", "admin"]
    block_id: str


class ExperienceNotification(BaseModel):
    model_config = ConfigDict(extra="forbid")

    template_id: str
    event: str
    audience: str


class ExperienceSelection(BaseModel):
    model_config = ConfigDict(extra="forbid")

    id: str
    kind: Literal["app_block", "feature_pack", "notification"]
    source: SelectionSource


class ApplicationExperience(BaseModel):
    model_config = ConfigDict(extra="forbid")

    schema_version: Literal["experience.v1"] = "experience.v1"
    design: ExperienceDesign
    admin_modules: list[ExperienceModule] = Field(default_factory=list)
    public_surfaces: list[str] = Field(default_factory=list)
    notification_bindings: list[ExperienceNotification] = Field(default_factory=list)
    selection: list[ExperienceSelection] = Field(default_factory=list)


EXPERIENCE_SCHEMA = "experience.v1"


def published_experience(raw: object) -> dict:
    """Browser copy of an experience projection.

    Missing, malformed, or incompatible input publishes no modules. It does not
    invent an inbox or any other capability.
    """
    if not isinstance(raw, dict) or raw.get("schema_version") != EXPERIENCE_SCHEMA:
        return {
            "schema_version": EXPERIENCE_SCHEMA,
            "experience_status": "unavailable",
            "admin_modules": [],
            "public_surfaces": [],
            "notification_bindings": [],
        }
    modules = []
    for item in raw.get("admin_modules") or []:
        if not isinstance(item, dict):
            continue
        module_id = item.get("id")
        label = item.get("label")
        block_id = item.get("block_id")
        if not isinstance(module_id, str) or not module_id.strip():
            continue
        if not isinstance(label, str) or not label.strip():
            continue
        audience = item.get("audience")
        modules.append(
            {
                "id": module_id.strip(),
                "label": label.strip(),
                "audience": audience if audience in {"staff", "admin"} else "staff",
                "block_id": block_id if isinstance(block_id, str) else "",
            }
        )
    return {
        "schema_version": EXPERIENCE_SCHEMA,
        "experience_status": "ready",
        "admin_modules": modules,
        "public_surfaces": [
            item for item in (raw.get("public_surfaces") or []) if isinstance(item, str)
        ],
        "notification_bindings": [
            item for item in (raw.get("notification_bindings") or []) if isinstance(item, dict)
        ],
        "design": raw.get("design") if isinstance(raw.get("design"), dict) else {},
    }


def empty_application_experience() -> ApplicationExperience:
    """Neutral projection used when an older plan has no experience section."""
    accent, radius, font = _DESIGN["neutral"]
    return ApplicationExperience(
        design=ExperienceDesign(recipe="neutral", accent=accent, radius=radius, font=font)
    )


def design_recipe_for(*, domain: str, archetype_id: str | None, purpose: str) -> DesignRecipe:
    text = " ".join(part for part in (domain, archetype_id or "", purpose) if part).lower()
    if any(word in text for word in ("dental", "clinic", "appointment", "medical")):
        return "clinic"
    if any(word in text for word in ("shop", "store", "commerce", "catalog", "ecommerce")):
        return "commerce"
    if any(word in text for word in ("restaurant", "cafe", "hospitality", "hotel")):
        return "hospitality"
    if any(word in text for word in ("consult", "professional", "agency", "firm", "legal")):
        return "professional"
    return "neutral"


def compile_application_experience(
    *,
    domain: str = "",
    archetype_id: str | None = None,
    purpose: str = "",
    app_block_ids: list[str],
    feature_pack_ids: list[str],
    authentication_required: bool,
    fresh_block_ids: set[str] | None = None,
    baseline_block_ids: set[str] | None = None,
) -> ApplicationExperience:
    """Project presentation from admitted blocks. Missing blocks produce no module."""
    recipe = design_recipe_for(domain=domain, archetype_id=archetype_id, purpose=purpose)
    accent, radius, font = _DESIGN[recipe]
    installed = set(app_block_ids)
    modules = [
        ExperienceModule(id=module_id, label=label, audience=audience, block_id=block_id)
        for block_id, module_id, label, audience in _ADMIN_MODULES
        if block_id in installed
    ]
    if authentication_required and not any(module.id == "team" for module in modules):
        modules.append(
            ExperienceModule(id="team", label="Team", audience="admin", block_id="auth")
        )
    surfaces = [name for block_id, name in _PUBLIC_SURFACES if block_id in installed]
    bindings = _notification_bindings(installed, authentication_required)
    selection = _selection(
        app_block_ids,
        feature_pack_ids,
        bindings,
        fresh_block_ids=fresh_block_ids,
        baseline_block_ids=baseline_block_ids,
    )
    return ApplicationExperience(
        design=ExperienceDesign(recipe=recipe, accent=accent, radius=radius, font=font),
        admin_modules=modules,
        public_surfaces=surfaces,
        notification_bindings=bindings,
        selection=selection,
    )


def _notification_bindings(
    installed: set[str], authentication_required: bool
) -> list[ExperienceNotification]:
    wanted: list[str] = []
    if "forms.contact" in installed:
        wanted.extend(["contact_capture.admin_notify", "contact_capture.confirmation"])
    if authentication_required:
        wanted.append("auth.magic_link")
    bindings: list[ExperienceNotification] = []
    for template_id in wanted:
        definition = EMAIL_TEMPLATE_CATALOG.get(template_id)
        if definition is None:
            continue
        bindings.append(
            ExperienceNotification(
                template_id=definition.version_id,
                event=definition.semantic_event,
                audience=definition.audience,
            )
        )
    return bindings


def _selection(
    app_block_ids: list[str],
    feature_pack_ids: list[str],
    bindings: list[ExperienceNotification],
    *,
    fresh_block_ids: set[str] | None,
    baseline_block_ids: set[str] | None,
) -> list[ExperienceSelection]:
    rows: list[ExperienceSelection] = []
    baseline = baseline_block_ids or set()
    fresh = fresh_block_ids
    for block_id in app_block_ids:
        source: SelectionSource = "current_prompt"
        if fresh is not None and block_id not in fresh and block_id in baseline:
            source = "installed_baseline"
        rows.append(ExperienceSelection(id=block_id, kind="app_block", source=source))
    for pack_id in feature_pack_ids:
        rows.append(ExperienceSelection(id=pack_id, kind="feature_pack", source="current_prompt"))
    for binding in bindings:
        rows.append(
            ExperienceSelection(id=binding.template_id, kind="notification", source="discovery")
        )
    return rows
