"""NL-query runner — validate a {intent, slots} request, run the curated query, return rows.

This is the trust boundary. Whatever produced the request (a local model in W2, the web UI,
or a test) is untrusted: the runner accepts ONLY a known intent key and slot VALUES, coerces
each value to its declared type, and rejects anything off-spec — it never lets a caller name
a table/column, write SQL, or choose an operator. The intents do the rest with fixed,
parameterized SQL (see intents.py). All failure modes return a structured error dict; the
runner never raises to the caller (a bad `limit=abc` must not crash the request thread).
"""
import sqlite3

from .intents import INTENTS


def _coerce_slot(name, spec, raw):
    """Coerce/validate one slot value against its spec. Returns (value, error). Exactly one
    of the two is meaningful: error is None on success, else a human-readable string."""
    t = spec["type"]
    provided = raw is not None and not (isinstance(raw, str) and raw.strip() == "")

    if not provided:
        if "default" in spec:
            return spec["default"], None
        if spec.get("required"):
            return None, f"slot '{name}' is required"
        return None, None  # optional, absent

    if t == "int":
        try:
            v = int(raw)
        except (TypeError, ValueError):
            return None, f"slot '{name}' must be an integer (got {raw!r})"
        if "min" in spec:
            v = max(spec["min"], v)
        if "max" in spec:
            v = min(spec["max"], v)
        return v, None

    if t == "enum":
        v = str(raw).strip().lower()
        if v not in spec["choices"]:
            if "default" in spec:
                return spec["default"], None
            return None, f"slot '{name}' must be one of {spec['choices']} (got {raw!r})"
        return v, None

    if t == "text":
        v = str(raw).strip()
        maxlen = spec.get("maxlen", 200)
        if len(v) > maxlen:
            v = v[:maxlen]
        return v, None

    return None, f"slot '{name}' has unknown type {t!r}"  # registry bug, fail visibly


def validate(intent_key, raw_slots):
    """Validate an intent + raw slots WITHOUT running. Returns (clean_slots, error_dict).
    Useful to the translator/UI for a dry-run check. error_dict is None on success."""
    if intent_key not in INTENTS:
        return None, {"error": "unknown_intent", "intent": intent_key,
                      "detail": f"unknown intent; known: {sorted(INTENTS)}"}
    spec = INTENTS[intent_key]["slots"]
    raw_slots = raw_slots or {}
    # Reject unexpected slot keys rather than ignore them — a request shaped wrong is a
    # misunderstanding worth surfacing, not silently dropping.
    unexpected = [k for k in raw_slots if k not in spec]
    if unexpected:
        return None, {"error": "bad_slot", "intent": intent_key,
                      "detail": f"unexpected slot(s): {unexpected}; allowed: {sorted(spec)}"}
    clean = {}
    for name, sspec in spec.items():
        v, err = _coerce_slot(name, sspec, raw_slots.get(name))
        if err:
            return None, {"error": "bad_slot", "intent": intent_key, "detail": err}
        if v is not None or "default" in sspec:
            clean[name] = v
    return clean, None


def run_query(conn, intent_key, raw_slots=None, *, audit_fn=None, actor=None, source="api"):
    """Validate and execute a curated NL query. Always returns a dict — either a result
    {intent, slots, columns, rows, row_count, truncated, summary} or an error
    {error, intent, detail}. Records an audit row via audit_fn (if given) so a query made
    through a leaked/automated credential is detectable.

    audit_fn signature: audit_fn({actor, source, intent, slots, row_count, error}).
    """
    clean, err = validate(intent_key, raw_slots)
    if err:
        if audit_fn:
            try:
                audit_fn({"actor": actor, "source": source, "intent": intent_key,
                          "slots": raw_slots, "row_count": 0, "error": err["error"]})
            except Exception:
                pass
        return err

    try:
        result = INTENTS[intent_key]["run"](conn, clean)
    except sqlite3.Error as exc:
        # Surface a query failure (e.g. a missing optional table) as a visible error — never
        # swallow it and hand back an empty result that reads as an authoritative "none".
        if audit_fn:
            try:
                audit_fn({"actor": actor, "source": source, "intent": intent_key,
                          "slots": clean, "row_count": 0, "error": "query_failed"})
            except Exception:
                pass
        return {"error": "query_failed", "intent": intent_key, "detail": str(exc)}

    out = {"intent": intent_key, "slots": clean, "row_count": len(result.get("rows", [])),
           **result}
    if audit_fn:
        try:
            audit_fn({"actor": actor, "source": source, "intent": intent_key,
                      "slots": clean, "row_count": out["row_count"], "error": None})
        except Exception:
            pass
    return out


def catalog():
    """The queryable surface as data: every intent's key, summary, slot specs and example.
    Single source of truth for the W2 translator prompt and any UI hint list."""
    return [{"intent": k, "summary": v["summary"], "slots": v["slots"],
             "example": v.get("example", "")} for k, v in INTENTS.items()]
