"""Query execution and saved question management."""

from __future__ import annotations

from typing import Any

from metaverse.app import mcp
from metaverse.client import as_dict, as_list
from metaverse.client import delete as api_delete
from metaverse.client import get as api_get
from metaverse.client import post as api_post
from metaverse.client import put as api_put
from metaverse.config import settings


def _extract_sql(dataset_query: dict[str, Any]) -> str:
    """Extract the SQL string from a Metabase dataset_query.

    Handles both the current MBQL format (stages[0].native) and the legacy
    format (native.query) transparently.
    """
    stages = dataset_query.get("stages")
    if stages and isinstance(stages, list) and stages:
        return str(stages[0].get("native", ""))
    native = dataset_query.get("native")
    if isinstance(native, dict):
        return str(native.get("query", ""))
    return ""


def _format_result(data: dict[str, Any], limit: int) -> dict[str, Any]:
    """Normalise a Metabase dataset response into a clean table dict."""
    inner: dict[str, Any] = data.get("data", data)  # type: ignore[assignment]
    cols: list[str] = [c["name"] for c in inner.get("cols", [])]
    rows: list[Any] = inner.get("rows", [])
    return {
        "columns": cols,
        "rows": rows,
        "row_count": len(rows),
        "truncated": len(rows) >= limit,
    }


@mcp.tool()
async def run_query(database_id: int, sql: str, limit: int = 100) -> dict[str, Any]:
    """Execute a native SQL query and return the results as a table.

    Args:
        database_id: The database ID from list_databases.
        sql: The SQL query to run. Always validate column names with describe_table first.
        limit: Maximum number of rows to return (default 100, max 2000).

    Returns {columns, rows, row_count, truncated}.
    If truncated is true, the full result set is larger — refine the query or add a LIMIT.
    """
    limit = min(limit, 2000)
    payload: dict[str, Any] = {
        "database": database_id,
        "type": "native",
        "native": {"query": sql},
        "parameters": [],
        "middleware": {"js-int-to-string?": False},
    }
    data = as_dict(await api_post("/dataset", json=payload))
    return _format_result(data, limit)


@mcp.tool()
async def list_questions() -> list[dict[str, Any]]:
    """List all saved questions (cards) in Metabase.

    Returns [{id, name, description, database_id, created_at}].
    """
    data = await api_get("/card")
    return [
        {
            "id": c["id"],
            "name": c["name"],
            "description": c.get("description") or "",
            "database_id": c.get("database_id"),
            "created_at": c.get("created_at", ""),
        }
        for c in as_list(data)
    ]


@mcp.tool()
async def get_question(card_id: int) -> dict[str, Any]:
    """Get the full details of a saved question, including its SQL query.

    Args:
        card_id: The question ID from list_questions.

    Returns {id, name, description, database_id, sql, created_at}.
    """
    data = as_dict(await api_get(f"/card/{card_id}"))
    return {
        "id": data["id"],
        "name": data["name"],
        "description": data.get("description") or "",
        "database_id": data.get("database_id"),
        "sql": _extract_sql(data.get("dataset_query") or {}),
        "created_at": data.get("created_at", ""),
    }


_CHART_DISPLAYS = {"bar", "line", "area", "row", "combo"}


@mcp.tool()
async def create_question(
    name: str,
    database_id: int,
    sql: str,
    description: str = "",
    display: str = "table",
    x_column: str | None = None,
    y_columns: list[str] | None = None,
    visualization_settings: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Save a SQL query as a reusable Metabase question.

    Args:
        name: A clear, descriptive name (e.g. "Orders by Customer - Last 30 Days").
        database_id: The database ID from list_databases.
        sql: The validated SQL query.
        description: Optional description of what the question shows.
        display: Visualization type — "table" (default), "scalar", "bar", "line",
                 "area", "pie", "row", "combo".
        x_column: Column name for the X axis (required for bar/line/area/row/combo).
                  Use the categorical or date column from the query result.
        y_columns: Column name(s) for the Y axis (required for bar/line/area/row/combo).
                   Use the numeric/aggregated column(s) from the query result.
        visualization_settings: Additional Metabase visualization settings. x_column and
                                 y_columns are merged in automatically when provided.

    Returns {id, name, url}.
    """
    viz: dict[str, Any] = dict(visualization_settings or {})
    if display in _CHART_DISPLAYS:
        if x_column:
            viz.setdefault("graph.dimensions", [x_column])
        if y_columns:
            viz.setdefault("graph.metrics", y_columns)

    payload: dict[str, Any] = {
        "name": name,
        "description": description or None,
        "database_id": database_id,
        "dataset_query": {
            "database": database_id,
            "type": "native",
            "native": {"query": sql},
        },
        "display": display,
        "visualization_settings": viz,
    }
    data = as_dict(await api_post("/card", json=payload))
    return {
        "id": data["id"],
        "name": data["name"],
        "url": f"{settings.url}/question/{data['id']}",
    }


@mcp.tool()
async def update_question(
    card_id: int,
    name: str | None = None,
    sql: str | None = None,
    description: str | None = None,
    display: str | None = None,
    visualization_settings: dict[str, Any] | None = None,
) -> dict[str, Any]:
    """Update an existing saved question.

    Only the fields you provide will be changed; everything else is preserved.

    Args:
        card_id: The question ID to update.
        name: New name, if changing.
        sql: New SQL query, if changing.
        description: New description, if changing.
        display: New visualization type ("table", "scalar", "bar", "line",
                 "area", "pie", "row", "combo"), if changing.
        visualization_settings: New visualization settings dict, if changing.

    Returns {id, name, url}.
    """
    current = as_dict(await api_get(f"/card/{card_id}"))
    payload: dict[str, Any] = {
        "name": current["name"],
        "description": current.get("description") or None,
        "display": current.get("display", "table"),
        "visualization_settings": current.get("visualization_settings") or {},
        "dataset_query": current["dataset_query"],
    }
    if name is not None:
        payload["name"] = name
    if description is not None:
        payload["description"] = description or None
    if display is not None:
        payload["display"] = display
    if visualization_settings is not None:
        payload["visualization_settings"] = visualization_settings
    if sql is not None:
        dq: dict[str, Any] = current["dataset_query"]
        stages = dq.get("stages")
        if stages and isinstance(stages, list) and stages:
            updated_stages = [dict(stages[0], native=sql), *stages[1:]]
            payload["dataset_query"] = {**dq, "stages": updated_stages}
        else:
            payload["dataset_query"] = {**dq, "native": {"query": sql}}
    data = as_dict(await api_put(f"/card/{card_id}", json=payload))
    return {
        "id": data["id"],
        "name": data["name"],
        "url": f"{settings.url}/question/{data['id']}",
    }


@mcp.tool()
async def execute_question(card_id: int, limit: int = 100) -> dict[str, Any]:
    """Run a saved question and return its results.

    Args:
        card_id: The question ID from list_questions.
        limit: Maximum rows to return (default 100).

    Returns {columns, rows, row_count, truncated}.
    """
    limit = min(limit, 2000)
    raw = await api_post(f"/card/{card_id}/query/json", json={})
    # /query/json returns a list of row-dicts; normalise to columnar table format.
    if isinstance(raw, list):
        columns = list(raw[0].keys()) if raw else []
        rows = [[row.get(c) for c in columns] for row in raw[:limit]]
        return {
            "columns": columns,
            "rows": rows,
            "row_count": len(rows),
            "truncated": len(raw) > limit,
        }
    return _format_result(as_dict(raw), limit)


@mcp.tool()
async def delete_question(card_id: int) -> dict[str, Any]:
    """Delete a saved question.

    Args:
        card_id: The question ID to delete.
    """
    await api_delete(f"/card/{card_id}")
    return {"deleted": True, "card_id": card_id}
