import uuid
from datetime import datetime
from typing import Any, Literal

from pydantic import BaseModel, ConfigDict, Field, model_validator

from app.schemas.reporting import DatasetAggregateFn, DatasetFilter, DatasetFilterItem, DatasetSortDir, DatasetSourceType

_NAME = r"^[A-Za-z_][A-Za-z0-9_]{0,59}$"

ReportSectionKind = Literal["table", "summary", "kpi", "chart", "text"]


class ReportParam(BaseModel):
    """A value asked for when the report runs, used in section filters as
    {{NAME}} (or {{param.NAME}}). A filter whose parameter is left blank is
    skipped, so optional parameters just widen the report."""

    name: str = Field(pattern=_NAME)
    label: str | None = Field(default=None, max_length=100)
    type: Literal["text", "number", "date"] = "text"
    default: str | None = Field(default=None, max_length=200)
    required: bool = False


class ReportJoin(BaseModel):
    """Columns from another source, matched row by row: records of source
    whose foreign_field equals this row's local_field (a left join — rows
    without a match keep blanks). Its columns appear as "<alias>.<column>".
    When several records match: "first" / "latest" by when they were created,
    or "all" — one report row per match."""

    alias: str = Field(pattern=_NAME)
    source_type: DatasetSourceType
    source_id: uuid.UUID
    local_field: str = Field(min_length=1, max_length=200)
    foreign_field: str = Field(min_length=1, max_length=200)
    columns: list[str] = Field(min_length=1)
    filters: list[DatasetFilterItem] = []
    match: Literal["first", "latest", "all"] = "first"


class ReportSection(BaseModel):
    id: str = Field(min_length=1, max_length=50)
    kind: ReportSectionKind
    title: str | None = Field(default=None, max_length=200)
    width: Literal["full", "half"] = "full"
    # text — a heading or notes; may hold {{NAME}} parameters.
    text: str | None = Field(default=None, max_length=20_000)
    source_type: DatasetSourceType | None = None
    source_id: uuid.UUID | None = None
    columns: list[str] = []
    # column -> heading shown in the report
    column_labels: dict[str, str] = {}
    filters: list[DatasetFilterItem] = []
    sort_field: str | None = None
    sort_dir: DatasetSortDir = "asc"
    # summary / chart: one row per distinct group_by value; kpi: one number.
    group_by: str | None = None
    aggregate_fn: DatasetAggregateFn | None = None
    aggregate_field: str | None = None
    # kpi — a custom expression over the rows instead of aggregate_fn.
    expression: str | None = Field(default=None, max_length=2000)
    chart_type: Literal["bar", "line", "pie"] = "bar"
    limit: int = Field(default=500, ge=1, le=5000)
    joins: list[ReportJoin] = []
    # table / summary — a last row with the sum of each numeric column.
    show_totals: bool = False

    @model_validator(mode="after")
    def _check(self) -> "ReportSection":
        if self.kind == "text":
            return self
        if self.source_type is None or self.source_id is None:
            raise ValueError(f"section '{self.title or self.id}': choose a data source")
        if self.kind in ("summary", "chart") and not self.group_by:
            raise ValueError(f"section '{self.title or self.id}': choose what to group by")
        if self.kind == "kpi" and not (self.aggregate_fn or self.expression):
            raise ValueError(f"section '{self.title or self.id}': choose how to calculate the number")
        if self.aggregate_fn and self.aggregate_fn != "count" and not self.aggregate_field:
            raise ValueError(f"section '{self.title or self.id}': choose the field to {self.aggregate_fn}")
        if self.joins and self.kind != "table":
            raise ValueError(f"section '{self.title or self.id}': only table sections can join other sources")
        aliases = [j.alias for j in self.joins]
        if len(aliases) != len(set(aliases)):
            raise ValueError(f"section '{self.title or self.id}': join names must be unique")
        return self


class ReportSettings(BaseModel):
    subtitle: str | None = Field(default=None, max_length=300)
    orientation: Literal["portrait", "landscape"] = "landscape"
    show_generated_at: bool = True


class _TemplateBody(BaseModel):
    parameters: list[ReportParam] = []
    sections: list[ReportSection] = []
    settings: ReportSettings = ReportSettings()

    @model_validator(mode="after")
    def _unique(self):
        for label, names in (("Parameter", [p.name for p in self.parameters]), ("Section", [s.id for s in self.sections])):
            if len(names) != len(set(names)):
                raise ValueError(f"{label} names must be unique")
        return self


class ReportTemplateCreate(_TemplateBody):
    name: str = Field(min_length=1, max_length=200)
    description: str | None = Field(default=None, max_length=500)
    role_ids: list[uuid.UUID] = []


class ReportTemplateUpdate(BaseModel):
    name: str | None = Field(default=None, min_length=1, max_length=200)
    description: str | None = Field(default=None, max_length=500)
    parameters: list[ReportParam] | None = None
    sections: list[ReportSection] | None = None
    settings: ReportSettings | None = None
    role_ids: list[uuid.UUID] | None = None
    # Refused (409) if someone else saved the template after it was opened.
    expected_updated_at: datetime | None = None


class ReportTemplateRead(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    id: uuid.UUID
    name: str
    description: str | None
    parameters: list[ReportParam]
    sections: list[ReportSection]
    settings: ReportSettings
    role_ids: list[uuid.UUID]
    # Set on a report a Public Portal user built for themselves.
    vendor_id: uuid.UUID | None = None
    created_by: uuid.UUID | None = None
    created_at: datetime
    updated_at: datetime


class SectionOverride(BaseModel):
    """What the person running a report changes for this run only (the run
    page's "Adjust"): more filters (on top of the section's own), which of
    the section's columns to show and in what order, and their headings."""

    filters: list[DatasetFilterItem] = []
    columns: list[str] | None = None
    column_labels: dict[str, str] = {}


class ReportRunRequest(BaseModel):
    params: dict[str, Any] = {}
    # section id -> this run's changes
    overrides: dict[str, SectionOverride] = {}


class ReportPreviewRequest(_TemplateBody):
    """Run an unsaved design — the designer's Preview."""

    name: str = "Preview"
    params: dict[str, Any] = {}
    overrides: dict[str, SectionOverride] = {}


class ReportSectionResult(BaseModel):
    id: str
    kind: ReportSectionKind
    title: str | None
    width: str
    chart_type: str | None = None
    text: str | None = None
    columns: list[str] = []
    column_labels: dict[str, str] = {}
    # What "Adjust" offers: the fields a filter can use, and (table) every
    # column the section can show before this run's choice.
    fields: list[str] = []
    all_columns: list[str] = []
    rows: list[dict] = []
    total: int = 0
    totals: dict[str, float] | None = None
    value: Any = None
    error: str | None = None


class ReportRunResult(BaseModel):
    name: str
    subtitle: str | None
    generated_at: datetime | None
    params: dict[str, Any]
    sections: list[ReportSectionResult]
