"""Explicit, bounded Dataset contract.

A Dataset is resolved *before* a chart or calculator runs. Calculation IR
must not query records. Visualization must not invent a query engine.
Reporting is a different product: saved report definitions over live
``records.aggregate``. A chart bound to a Dataset is not Reporting.
"""

from __future__ import annotations

from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

DATASET_CONTRACT_VERSION = 1

DatasetSourceKind = Literal[
    "records.query",
    "records.aggregate",
    "calculator.emit",
    "static",
]
DatasetShapeKind = Literal["series", "table", "scalar", "list"]
DatasetAudience = Literal["public", "authenticated", "staff"]
DatasetFreshnessMode = Literal["live", "snapshot"]
DatasetProjectScope = Literal["current"]


class DatasetTenancyError(ValueError):
    """Dataset resolution attempted to leave the current project."""


class DatasetSource(BaseModel):
    model_config = ConfigDict(extra="forbid")
    kind: DatasetSourceKind
    entity_key: str | None = None
    aggregate: dict[str, Any] | None = None
    calculator_id: str | None = None
    output_key: str | None = None
    static_rows: list[dict[str, Any]] = Field(default_factory=list)

    @model_validator(mode="after")
    def source_matches_kind(self) -> DatasetSource:
        if self.kind in {"records.query", "records.aggregate"}:
            if not self.entity_key:
                raise ValueError(f"{self.kind} requires entity_key")
            if self.calculator_id or self.static_rows:
                raise ValueError(f"{self.kind} cannot set calculator_id or static_rows")
        elif self.kind == "calculator.emit":
            if not self.calculator_id:
                raise ValueError("calculator.emit requires calculator_id")
            if self.entity_key or self.static_rows:
                raise ValueError("calculator.emit cannot set entity_key or static_rows")
        elif self.kind == "static":
            if self.entity_key or self.calculator_id:
                raise ValueError("static source cannot set entity_key or calculator_id")
        if self.kind != "records.aggregate" and self.aggregate:
            raise ValueError("aggregate is only valid on records.aggregate")
        return self


class DatasetShape(BaseModel):
    model_config = ConfigDict(extra="forbid")
    kind: DatasetShapeKind
    fields: list[str] = Field(default_factory=list)


class DatasetScope(BaseModel):
    """Project tenancy plus source filters. Not a vertical schema."""

    model_config = ConfigDict(extra="forbid")
    project: DatasetProjectScope = "current"
    filters: dict[str, Any] = Field(default_factory=dict)

    @model_validator(mode="after")
    def project_is_current_only(self) -> DatasetScope:
        if self.project != "current":
            raise DatasetTenancyError(
                "Dataset resolution is always project-scoped and may never "
                "silently cross workspace/project boundaries"
            )
        return self


class DatasetFreshness(BaseModel):
    model_config = ConfigDict(extra="forbid")
    mode: DatasetFreshnessMode = "live"


class DatasetRowBudget(BaseModel):
    model_config = ConfigDict(extra="forbid")
    max: int = Field(default=1000, ge=1, le=5000)


class DatasetContract(BaseModel):
    model_config = ConfigDict(extra="forbid")
    schema_version: Literal[1] = 1
    id: str = Field(min_length=1)
    source: DatasetSource
    shape: DatasetShape
    scope: DatasetScope = Field(default_factory=DatasetScope)
    audience: DatasetAudience = "authenticated"
    freshness: DatasetFreshness = Field(default_factory=DatasetFreshness)
    row_budget: DatasetRowBudget = Field(default_factory=DatasetRowBudget)


def validate_dataset(payload: DatasetContract | dict[str, Any]) -> DatasetContract:
    if isinstance(payload, DatasetContract):
        return payload
    return DatasetContract.model_validate(payload)
