"""Composition contract for Visualization + Calculation Fast Lane recipes.

Recipes describe product intent above Dataset and Calculation IR. They are not
a second executor, not Reporting, and not new App Blocks.

```text
recipe != primitive
dataset != report
chart != dashboard
calculator != entity
Calculation IR != records query language
metric.card != metric.count / metric.sum
local calculator != Cloud execution
```
"""

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

RecipeKind = Literal["calculation", "visualization", "calculated_chart"]
CalculatorAuthor = Literal["feature_pack", "codegen"]
ChartType = Literal["bar", "line", "pie", "area"]
DatasetPattern = Literal[
    "distribution",
    "value_distribution",
    "trend",
    "value_trend",
    "comparison",
    "selected_record",
    "calculation_output",
]
DatasetSourceKind = Literal[
    "records.aggregate",
    "records.query",
    "static",
    "calculator.emit",
    "selected_record",
]
ValueTypeName = Literal[
    "number",
    "integer",
    "money",
    "percent",
    "date",
    "boolean",
    "text",
    "select",
]


class RecipeFieldSpec(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str = Field(min_length=1)
    type: ValueTypeName
    required: bool = True
    min: float | None = None
    label: str | None = None


class RecipeCalculatorSpec(BaseModel):
    """Declarative calculator surface for Codegen / Feature Pack install.

    Does not execute. IR remains pack-owned or Codegen-authored and immutable
    once installed. ``dataset_input`` names a resolved snapshot only.
    """

    model_config = ConfigDict(extra="forbid")
    author: CalculatorAuthor
    execution: Literal["local"] = "local"
    pack_slug: str | None = None
    calculator_id: str | None = None
    inputs: list[RecipeFieldSpec] = Field(default_factory=list)
    outputs: list[RecipeFieldSpec] = Field(default_factory=list)
    dataset_input: str | None = None

    @model_validator(mode="after")
    def author_rules(self) -> RecipeCalculatorSpec:
        if self.execution != "local":
            raise ValueError("calculator recipes may only declare execution: local")
        if self.author == "feature_pack" and not self.pack_slug:
            raise ValueError("feature_pack calculator recipes require pack_slug")
        if self.author == "codegen" and self.pack_slug:
            raise ValueError("codegen calculator recipes must not set pack_slug")
        return self


class RecipeDatasetDimension(BaseModel):
    model_config = ConfigDict(extra="forbid")
    field: str = Field(min_length=1)
    bucket: Literal["day", "week", "month"] | None = None


class RecipeDatasetMeasure(BaseModel):
    model_config = ConfigDict(extra="forbid")
    id: str = Field(min_length=1)
    aggregate: Literal["count", "sum", "avg"] = "count"
    field: str | None = None


class RecipeDatasetSpec(BaseModel):
    """Bounded Dataset binding for charts / calculator inputs.

    Entity keys are composition hints for Codegen. Resolution stays
    project-scoped. IR must not query records.
    """

    model_config = ConfigDict(extra="forbid")
    pattern: DatasetPattern | None = None
    source_kind: DatasetSourceKind = "records.aggregate"
    entity_key: str | None = None
    calculator_id: str | None = None
    dimensions: list[RecipeDatasetDimension] = Field(default_factory=list)
    measures: list[RecipeDatasetMeasure] = Field(default_factory=list)
    audience: Literal["public", "authenticated", "staff"] = "authenticated"
    freshness: Literal["live", "snapshot"] = "live"
    row_budget: int = Field(default=1000, ge=1, le=5000)

    @model_validator(mode="after")
    def source_rules(self) -> RecipeDatasetSpec:
        if self.source_kind in {"records.aggregate", "records.query"} and not self.entity_key:
            raise ValueError(f"{self.source_kind} requires entity_key")
        if self.source_kind == "calculator.emit" and not self.calculator_id:
            raise ValueError("calculator.emit requires calculator_id")
        if self.source_kind == "selected_record" and not self.entity_key:
            raise ValueError("selected_record requires entity_key")
        return self


class RecipePresentationMetric(BaseModel):
    model_config = ConfigDict(extra="forbid")
    output: str = Field(min_length=1)
    presentation: Literal["metric.card"] = "metric.card"


class RecipePresentationChart(BaseModel):
    model_config = ConfigDict(extra="forbid")
    dataset: str = Field(min_length=1)
    chart_type: ChartType = "bar"
    x: str | None = None
    y: str | None = None


class RecipePresentationSpec(BaseModel):
    model_config = ConfigDict(extra="forbid")
    primary_block: str | None = None
    metrics: list[RecipePresentationMetric] = Field(default_factory=list)
    charts: list[RecipePresentationChart] = Field(default_factory=list)


# Five chart composition patterns. Verticals bind entity/fields; chart host stays generic.
CHART_COMPOSITION_PATTERNS: dict[str, dict[str, Any]] = {
    "distribution": {
        "description": "dimension → count",
        "chart_types": ["bar", "pie"],
        "pattern": "distribution",
    },
    "value_distribution": {
        "description": "dimension → sum(value)",
        "chart_types": ["bar", "pie"],
        "pattern": "value_distribution",
    },
    "trend": {
        "description": "time → count",
        "chart_types": ["line", "area"],
        "pattern": "trend",
    },
    "value_trend": {
        "description": "time → sum(value)",
        "chart_types": ["line", "area"],
        "pattern": "value_trend",
    },
    "comparison": {
        "description": "category → scalar",
        "chart_types": ["bar"],
        "pattern": "comparison",
    },
}

DATASET_COMPOSITION_PATTERNS: tuple[str, ...] = (
    "category_aggregation",
    "value_aggregation",
    "time_series",
    "selected_record",
    "calculation_output",
    "bounded_list",
    "static_inputs",
)

COMPOSITION_INVARIANTS: tuple[str, ...] = (
    "recipe_is_not_a_primitive",
    "dataset_is_not_a_report",
    "chart_is_not_a_dashboard",
    "calculator_is_not_an_entity",
    "calculation_ir_is_not_a_records_query_language",
    "metric_card_is_not_metric_count_or_sum",
    "local_calculator_is_not_cloud_execution",
)

VIS_CALC_BLOCKS = frozenset({"visualization.chart", "calculation.calculator"})
FORBIDDEN_COMPOSITION_BLOCKS = frozenset(
    {
        "reporting.dashboard",
        "payments.checkout",
        "membership.portal",
    }
)
