"""Pydantic models for Looker dashboard and look specs."""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Dict, List, Literal, Optional

from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator

from g_gremlin.looker.errors import LookerSpecError, summarize_pydantic_errors


class LookerQuerySpecV1(BaseModel):
    """Inline query definition for a tile or look."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    model: str
    explore: str
    fields: List[str] = Field(default_factory=list)
    filters: Dict[str, Any] = Field(default_factory=dict)
    sorts: List[str] = Field(default_factory=list)
    limit: Optional[int] = None

    @model_validator(mode="before")
    @classmethod
    def _normalize_explore_alias(cls, value: Any) -> Any:
        if not isinstance(value, dict):
            return value
        payload = dict(value)
        if not payload.get("explore") and payload.get("view"):
            payload["explore"] = payload.get("view")
        return payload

    @model_validator(mode="after")
    def _validate_required(self) -> "LookerQuerySpecV1":
        if not self.model.strip():
            raise ValueError("query.model is required")
        if not self.explore.strip():
            raise ValueError("query.explore is required")
        if not self.fields:
            raise ValueError("query.fields must include at least one field")
        return self


class LookerDashboardSpecMetaV1(BaseModel):
    """Dashboard identity and container metadata."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    title: str
    space_id: Optional[str] = None
    folder_id: Optional[str] = None
    dashboard_id: Optional[str] = None
    description: Optional[str] = None

    @model_validator(mode="before")
    @classmethod
    def _normalize_space_alias(cls, value: Any) -> Any:
        if not isinstance(value, dict):
            return value
        payload = dict(value)
        if not payload.get("space_id") and payload.get("folder_id"):
            payload["space_id"] = payload.get("folder_id")
        return payload

    @model_validator(mode="after")
    def _validate_target(self) -> "LookerDashboardSpecMetaV1":
        if not self.dashboard_id and not self.space_id:
            raise ValueError("dashboard.space_id (or folder_id) is required when dashboard_id is not set")
        return self


class LookerTileSpecV1(BaseModel):
    """Dashboard tile specification."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    title: str
    query: LookerQuerySpecV1
    element_id: Optional[str] = None
    type: Optional[str] = None
    vis_config: Dict[str, Any] = Field(default_factory=dict)
    layout: Dict[str, Any] = Field(default_factory=dict)


class LookerFilterSpecV1(BaseModel):
    """Dashboard filter specification."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    name: str
    title: str
    dimension: str
    type: Optional[str] = None
    default_value: Optional[str] = None

    @model_validator(mode="after")
    def _validate_dimension(self) -> "LookerFilterSpecV1":
        if not self.dimension.strip():
            raise ValueError("filter.dimension must be a non-empty field reference")
        return self


class LookerDashboardSpecV1(BaseModel):
    """Versioned dashboard provisioning spec."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    spec_version: Literal[1]
    kind: Literal["dashboard", "looker_dashboard"] = "dashboard"
    dashboard: LookerDashboardSpecMetaV1
    tiles: List[LookerTileSpecV1] = Field(default_factory=list)
    filters: List[LookerFilterSpecV1] = Field(default_factory=list)
    apply_mode: Optional[Literal["merge", "replace"]] = None
    metadata: Dict[str, Any] = Field(default_factory=dict)


class LookerLookSpecMetaV1(BaseModel):
    """Look metadata for create operations."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    title: str
    space_id: Optional[str] = None
    folder_id: Optional[str] = None
    description: Optional[str] = None

    @model_validator(mode="before")
    @classmethod
    def _normalize_space_alias(cls, value: Any) -> Any:
        if not isinstance(value, dict):
            return value
        payload = dict(value)
        if not payload.get("space_id") and payload.get("folder_id"):
            payload["space_id"] = payload.get("folder_id")
        return payload

    @model_validator(mode="after")
    def _validate_space(self) -> "LookerLookSpecMetaV1":
        if not self.space_id:
            raise ValueError("look.space_id (or folder_id) is required")
        return self


class LookerLookSpecV1(BaseModel):
    """Versioned look creation spec."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    spec_version: Literal[1]
    kind: Literal["look", "looker_look"] = "look"
    look: LookerLookSpecMetaV1
    query: LookerQuerySpecV1
    metadata: Dict[str, Any] = Field(default_factory=dict)


def _raise_spec_error(prefix: str, exc: ValidationError) -> None:
    detail = summarize_pydantic_errors(exc.errors())
    raise LookerSpecError(f"{prefix}: {detail}") from exc


def parse_dashboard_spec(data: Dict[str, Any]) -> LookerDashboardSpecV1:
    """Validate and parse a dashboard spec payload."""
    try:
        return LookerDashboardSpecV1.model_validate(data)
    except ValidationError as exc:
        _raise_spec_error("Invalid dashboard spec", exc)
    raise AssertionError("unreachable")


def parse_look_spec(data: Dict[str, Any]) -> LookerLookSpecV1:
    """Validate and parse a look spec payload."""
    try:
        return LookerLookSpecV1.model_validate(data)
    except ValidationError as exc:
        _raise_spec_error("Invalid look spec", exc)
    raise AssertionError("unreachable")


def parse_tile_spec(data: Dict[str, Any]) -> LookerTileSpecV1:
    """Validate and parse a tile spec payload."""
    try:
        return LookerTileSpecV1.model_validate(data)
    except ValidationError as exc:
        _raise_spec_error("Invalid tile spec", exc)
    raise AssertionError("unreachable")


def parse_filter_spec(data: Dict[str, Any]) -> LookerFilterSpecV1:
    """Validate and parse a filter spec payload."""
    try:
        return LookerFilterSpecV1.model_validate(data)
    except ValidationError as exc:
        _raise_spec_error("Invalid filter spec", exc)
    raise AssertionError("unreachable")


def dashboard_to_spec_payload(dashboard: Dict[str, Any]) -> Dict[str, Any]:
    """
    Convert a Looker dashboard payload into a round-trip dashboard spec payload.

    The returned payload is validated against ``LookerDashboardSpecV1`` before
    returning to guarantee describe -> edit -> apply compatibility.
    """
    dashboard_id = str(dashboard.get("id") or "")
    space_id = dashboard.get("space_id") or dashboard.get("folder_id")
    title = dashboard.get("title") or f"Dashboard {dashboard_id or 'unknown'}"

    elements: List[Dict[str, Any]] = list(
        dashboard.get("dashboard_elements") or dashboard.get("elements") or []
    )
    filters: List[Dict[str, Any]] = list(
        dashboard.get("dashboard_filters") or dashboard.get("filters") or []
    )

    tile_specs: List[Dict[str, Any]] = []
    non_query_elements: List[Dict[str, Any]] = []
    for element in elements:
        query = element.get("query") or {}
        if not isinstance(query, dict):
            query = {}

        fields = query.get("fields") or []
        if isinstance(fields, str):
            fields = [fields]
        elif not isinstance(fields, list):
            fields = []

        query_model = str(query.get("model") or "").strip()
        query_explore = str(query.get("view") or query.get("explore") or "").strip()
        query_fields = [str(item) for item in fields if str(item).strip()]
        if not query_model or not query_explore or not query_fields:
            non_query_elements.append(
                {
                    "element_id": str(element.get("id")) if element.get("id") is not None else None,
                    "title": element.get("title"),
                    "type": element.get("type"),
                    "reason": "element missing query model/explore/fields",
                }
            )
            continue

        query_payload: Dict[str, Any] = {
            "model": query_model,
            "explore": query_explore,
            "fields": query_fields,
        }

        query_filters = query.get("filters") or {}
        if isinstance(query_filters, dict):
            query_payload["filters"] = query_filters

        sorts = query.get("sorts") or []
        if isinstance(sorts, str):
            query_payload["sorts"] = [sorts]
        elif isinstance(sorts, list):
            query_payload["sorts"] = sorts

        if query.get("limit") is not None:
            query_payload["limit"] = query.get("limit")

        layout: Dict[str, Any] = {}
        for key in ("row", "column", "width", "height"):
            if element.get(key) is not None:
                layout[key] = element.get(key)

        tile_specs.append(
            {
                "title": str(element.get("title") or f"Element {element.get('id') or ''}").strip() or "Untitled Tile",
                "element_id": str(element.get("id")) if element.get("id") is not None else None,
                "type": element.get("type"),
                "query": query_payload,
                "vis_config": element.get("vis_config") or {},
                "layout": layout,
            }
        )

    filter_specs: List[Dict[str, Any]] = []
    skipped_filters: List[Dict[str, Any]] = []
    for filt in filters:
        dimension = str(filt.get("dimension") or "").strip()
        if not dimension:
            skipped_filters.append(
                {
                    "filter_id": str(filt.get("id")) if filt.get("id") is not None else None,
                    "name": filt.get("name"),
                    "title": filt.get("title"),
                    "reason": "filter missing dimension",
                }
            )
            continue
        filter_specs.append(
            {
                "name": str(filt.get("name") or filt.get("title") or "").strip() or "filter",
                "title": str(filt.get("title") or filt.get("name") or "").strip() or "Filter",
                "dimension": dimension,
                "type": filt.get("type"),
                "default_value": filt.get("default_value"),
            }
        )

    payload = {
        "spec_version": 1,
        "kind": "dashboard",
        "dashboard": {
            "title": title,
            "space_id": str(space_id) if space_id is not None else None,
            "dashboard_id": dashboard_id or None,
            "description": dashboard.get("description"),
        },
        "tiles": tile_specs,
        "filters": filter_specs,
        "metadata": {
            "source": "looker_api",
            "described_at": datetime.now(timezone.utc).isoformat(),
            "source_dashboard_id": dashboard_id or None,
            "non_query_elements": non_query_elements,
            "skipped_filters": skipped_filters,
        },
    }

    return parse_dashboard_spec(payload).model_dump(mode="json")
