"""AI-suggested dashboard widgets (POST /dashboards/ai-widgets).

The user describes what they want to see ("RTI applications overview: total,
by district, monthly trend, my pending ones"); the model answers with widget
specs naming real tables and fields from the tenant's catalog. Nothing the
model says is trusted: every table is resolved by name, every field, operator
and aggregate is checked against the real schema, and a widget that can't be
made valid is dropped rather than guessed at — so a hallucinated answer can
only ever produce fewer widgets, never a broken one.
"""

import json
from typing import Any

from app.models.master import Master
from app.models.workflow import Form

AUDIT_FIELDS = ["created_at", "updated_at", "created_by", "updated_by"]
_OPERATORS = {"equals", "not_equals", "gt", "gte", "lt", "lte", "in", "contains", "is_empty", "is_not_empty", "between"}
_AGGREGATES = {"count", "sum", "avg", "min", "max"}
_BUCKETS = {"none", "day", "week", "month"}
_SIZES = {"small", "medium", "large"}
_PERIODS = {"day", "week", "month", "quarter", "year"}
_FORMATS = {"number", "currency", "percent"}

AXIS_TYPES = {"bar_chart", "line_chart", "area_chart", "combo_chart", "radar_chart"}
CATEGORY_TYPES = {"pie_chart", "funnel_chart", "treemap", "leaderboard"}
VALUE_TYPES = {"kpi_card", "gauge"}
WIDGET_TYPES = AXIS_TYPES | CATEGORY_TYPES | VALUE_TYPES | {"table"}
MAX_WIDGETS = 8

SYSTEM_PROMPT = """You design dashboards inside FlowFabric. The user describes what they want to see; answer with \
the widgets that show it, built ONLY from the tables and fields listed under AVAILABLE TABLES. Never invent a table \
or field name.

Every table also has these system fields (not listed per table): created_at and updated_at (timestamps), \
created_by and updated_by (the user who created / last changed the record). To mean "the person viewing the \
dashboard" use the filter value "{{{{current_user.id}}}}", e.g. {{"field": "created_by", "operator": "equals", \
"value": "{{{{current_user.id}}}}"}} for "my records".

Widget types:
- kpi_card: one number — a count of records (value_aggregate "count"), or sum/avg/min/max of value_field. \
Set trend_period ("day"|"week"|"month"|"quarter"|"year") to compare with the previous period.
- gauge: one number against a target (set "target").
- bar_chart, line_chart, area_chart, combo_chart, radar_chart: x_field is the category or date field; series is a \
list of {{"field": <field or null for count>, "aggregate": "count"|"sum"|"avg"|"min"|"max", "label": "..."}}. \
Use x_bucket "day"|"week"|"month" when x_field is a date (e.g. created_at for trends over time). combo_chart series \
may set "chart": "bar"|"line"|"area".
- pie_chart, funnel_chart, treemap, leaderboard: slice_field is the category; value_field + value_aggregate (or \
value_aggregate "count" to count records). leaderboard = ranked top-N list.
- table: columns (list of field names) shows the records themselves.

Respond with ONLY this JSON, no prose outside it:
{{"message": "<one short sentence>",
 "widgets": [{{"type": "<widget type>", "title": "<short title>", "source_type": "master"|"form",
   "source_name": "<exact table name>", "size": "small"|"medium"|"large",
   "x_field": null, "x_bucket": "none", "series": [],
   "slice_field": null, "value_field": null, "value_aggregate": "count",
   "columns": [], "filters": [{{"field": "...", "operator": "equals|not_equals|gt|gte|lt|lte|in|contains|is_empty|is_not_empty", "value": ...}}],
   "number_format": "number"|"currency"|"percent", "trend_period": null, "target": null, "top_n": null}}]}}

Give at most {max_widgets} widgets; lead with KPIs, then charts, then a table if useful. Use small for KPIs and \
gauges, medium for most charts, large for wide trends and tables. If nothing maps to the tables, answer \
{{"message": "<why>", "widgets": []}}.

AVAILABLE TABLES:
{catalog}
"""


def _source(masters: list[Master], forms: list[Form], source_type: Any, name: Any) -> tuple[str, str, set[str]] | None:
    wanted = str(name or "").strip().lower()
    if source_type == "master":
        m = next((m for m in masters if m.name.lower() == wanted), None)
        if m:
            return f"master:{m.id}", m.name, {f["name"] for f in m.field_schema}
    elif source_type == "form":
        f = next((f for f in forms if f.name.lower() == wanted), None)
        if f:
            return f"form:{f.form_id}", f.name, {fd["name"] for fd in f.field_definitions}
    return None


def _field(value: Any, valid: set[str]) -> str | None:
    return value if isinstance(value, str) and value in valid else None


def _filters(raw: Any, valid: set[str]) -> list[dict]:
    out = []
    for f in raw if isinstance(raw, list) else []:
        if isinstance(f, dict) and f.get("field") in valid and f.get("operator") in _OPERATORS:
            out.append({"field": f["field"], "operator": f["operator"], "value": f.get("value")})
    return out


def _series(raw: Any, valid: set[str], combo: bool) -> list[dict]:
    out = []
    for s in raw if isinstance(raw, list) else []:
        if not isinstance(s, dict):
            continue
        agg = s.get("aggregate") if s.get("aggregate") in _AGGREGATES else "count"
        field = _field(s.get("field"), valid)
        if agg != "count" and not field:
            continue  # sum/avg/min/max need a real field
        item = {"field": field if agg != "count" else None, "aggregate": agg, "label": str(s.get("label") or ("Count" if agg == "count" else f"{agg} of {field}"))[:60]}
        if combo and s.get("chart") in ("bar", "line", "area"):
            item["chart"] = s["chart"]
        out.append(item)
    return out[:6]


def validate_widget(raw: Any, masters: list[Master], forms: list[Form]) -> dict | None:
    """A model-proposed widget made safe, or None when it can't be."""
    if not isinstance(raw, dict) or raw.get("type") not in WIDGET_TYPES:
        return None
    wtype = raw["type"]
    source = _source(masters, forms, raw.get("source_type"), raw.get("source_name"))
    if source is None:
        return None
    data_source, source_name, declared = source
    valid = declared | set(AUDIT_FIELDS) | {"id"}

    config: dict[str, Any] = {"title": str(raw.get("title") or source_name)[:80], "filters": _filters(raw.get("filters"), valid)}
    if raw.get("number_format") in _FORMATS and raw["number_format"] != "number":
        config["number_format"] = {"style": raw["number_format"]}
    top_n = raw.get("top_n")
    if isinstance(top_n, int) and 1 <= top_n <= 100:
        config["top_n"] = top_n

    agg = raw.get("value_aggregate") if raw.get("value_aggregate") in _AGGREGATES else "count"
    value_field = _field(raw.get("value_field"), valid)
    if agg != "count" and not value_field:
        agg, value_field = "count", None

    if wtype in AXIS_TYPES:
        x_field = _field(raw.get("x_field"), valid)
        if not x_field:
            return None
        config.update(x_field=x_field, x_bucket=raw.get("x_bucket") if raw.get("x_bucket") in _BUCKETS else "none")
        config["series"] = _series(raw.get("series"), valid, wtype == "combo_chart") or [{"field": None, "aggregate": "count", "label": "Count"}]
        if x_field in ("created_at", "updated_at") and config["x_bucket"] == "none":
            config["x_bucket"] = "month"
    elif wtype in CATEGORY_TYPES:
        slice_field = _field(raw.get("slice_field"), valid)
        if not slice_field:
            return None
        config["slice_field"] = slice_field
        if agg != "count":
            config.update(value_field=value_field, value_aggregate=agg)
        if wtype == "leaderboard":
            config.setdefault("top_n", 10)
            config["sort"] = "value_desc"
    elif wtype in VALUE_TYPES:
        if agg != "count":
            config.update(value_field=value_field, value_aggregate=agg)
        if wtype == "kpi_card" and raw.get("trend_period") in _PERIODS:
            config.update(trend_period=raw["trend_period"], trend_field="created_at")
        if isinstance(raw.get("target"), (int, float)) and not isinstance(raw.get("target"), bool):
            config["target"] = float(raw["target"])
    else:  # table
        config["columns"] = [c for c in (raw.get("columns") or []) if isinstance(c, str) and c in valid][:12]

    size = raw.get("size") if raw.get("size") in _SIZES else ("small" if wtype in VALUE_TYPES else "medium")
    return {"type": wtype, "data_source": data_source, "source_name": source_name, "size": size, "config": config}


def parse_suggestions(content: str, masters: list[Master], forms: list[Form]) -> tuple[str, list[dict]]:
    from app.core.ai_client import strip_code_fence

    parsed = json.loads(strip_code_fence(content))
    widgets = [w for w in (validate_widget(raw, masters, forms) for raw in (parsed.get("widgets") or [])[: MAX_WIDGETS * 2]) if w]
    return str(parsed.get("message") or ""), widgets[:MAX_WIDGETS]
