"""Ephemeral ``/btw`` side-question handler.

``/btw <question>`` lets the user ask a one-off question that is **invisible**
to every long-term memory system Mugi maintains:

- Not written to ``chat_storage`` (sqlite messages table).
- Not indexed by ``session_search_db`` (FTS5 + vector store).
- Not observed by ``Active Memory`` pre-reply extraction.
- Not ingested by ``memory_consolidator`` or any ``dreaming`` phase.
- Not logged to the agent-plane turn log in ``utils/agent_plane.py``.

Design decisions (Phase 2a):

1. **Slash command only.** We do not auto-detect "side question" intent. The
   user must type ``/btw …`` explicitly; this is by user preference.
2. **Recent context.** We pass the last ``_RECENT_MSG_LIMIT`` messages from the
   current thread as a compact snippet so the utility model has just enough
   grounding to answer follow-ups like "who did we decide should do that?".
3. **Utility model first.** We run locally on the utility model (mlx-vlm) by
   default because the whole point of ``/btw`` is "cheap, fast, forgotten". We
   fall back to the ``btw`` subagent profile only when the utility model is
   disabled or unavailable — and even then with no tools and no personality
   injection (see ``SUBAGENT_PROFILES['btw']``).
4. **Invisibility.** All persistence layers check for a ``btw_`` thread-id
   prefix and no-op. ``utils.btw`` constructs a ``btw_<short uuid>`` thread id
   for its single run and delivers the answer via the LiveView event bus with a
   distinct ``is_btw`` marker so the UI can render it as ephemeral.
5. **Ledger visibility only.** Runs *are* recorded in
   ``utils/task_ledger.py`` (kind=``btw``) with redacted metadata — this is a
   security/observability concession, not a memory leak: the ledger is
   operational and has a bounded retention policy.

The public entry point is ``handle_btw(question, thread_id, deliver)`` where
``deliver`` is a callable ``(text, is_btw=True) -> None`` that the caller wires
to its LiveView event-push mechanism.
"""
from __future__ import annotations

import logging
import threading
import time
import uuid
from typing import Callable

from utils.redaction import cap_str, redact_text
from utils.task_ledger import record_run

_log = logging.getLogger(__name__)

BTW_THREAD_PREFIX = "btw_"
_RECENT_MSG_LIMIT = 10
_RECENT_CHAR_CAP = 2_400  # per-message cap when building the snippet
_MAX_QUESTION_CHARS = 1_500
_MAX_ANSWER_CHARS = 2_400
_MAX_CONTEXT_CHARS = 8_000


def _short_btw_id() -> str:
    return new_btw_thread_id()


def new_btw_thread_id() -> str:
    """Public helper: return a fresh ``btw_<10 hex>`` ephemeral thread id."""
    return f"{BTW_THREAD_PREFIX}{uuid.uuid4().hex[:10]}"


def _summarize_message(msg: dict) -> str:
    role = (msg.get("role") or "").strip().lower() or "user"
    content = msg.get("content")
    if isinstance(content, list):
        parts: list[str] = []
        for b in content:
            if isinstance(b, dict):
                if b.get("type") in ("text", "output_text"):
                    parts.append(str(b.get("text") or ""))
                elif b.get("type") == "image_url":
                    parts.append("[image]")
                else:
                    parts.append(str(b.get("text") or b.get("content") or ""))
            else:
                parts.append(str(b))
        content = " ".join(p for p in parts if p)
    else:
        content = str(content or "")
    content = content.replace("\r", " ").replace("\n", " ").strip()
    content = cap_str(content, _RECENT_CHAR_CAP)
    if role == "assistant":
        prefix = "Mugi"
    elif role == "user":
        prefix = "You"
    else:
        prefix = role.capitalize() or "System"
    return f"{prefix}: {content}" if content else ""


def _build_recent_snippet(parent_thread_id: str | None) -> str:
    """Return a plain-text snippet of the last ~10 messages for context.

    Returns ``""`` if there is no parent thread or chat_storage is unavailable.
    """
    if not parent_thread_id or parent_thread_id.startswith(BTW_THREAD_PREFIX):
        return ""
    try:
        from chat_storage import get_messages
        msgs = get_messages(parent_thread_id)
    except Exception:
        return ""
    if not msgs:
        return ""
    tail = msgs[-_RECENT_MSG_LIMIT:]
    lines = [_summarize_message(m) for m in tail]
    lines = [ln for ln in lines if ln]
    if not lines:
        return ""
    snippet = "\n".join(lines)
    snippet = cap_str(snippet, _MAX_CONTEXT_CHARS)
    return snippet


def _build_btw_messages(question: str, context_snippet: str) -> list[dict]:
    system = (
        "You are the user's assistant answering a one-off side question marked with /btw. "
        "This turn will NOT be saved to memory or future conversation. "
        "Use only the brief recent-turn snippet below plus general knowledge. "
        "Be direct and helpful. No preamble. No follow-up questions."
    )
    user_parts: list[str] = []
    if context_snippet:
        user_parts.append("<recent_turns>\n" + context_snippet + "\n</recent_turns>")
    user_parts.append("<side_question>\n" + question + "\n</side_question>")
    return [
        {"role": "system", "content": system},
        {"role": "user", "content": "\n\n".join(user_parts)},
    ]


def _run_utility(question: str, context_snippet: str) -> str | None:
    try:
        from utility_model import utility_complete
    except Exception:
        return None
    messages = _build_btw_messages(question, context_snippet)
    try:
        text = utility_complete(
            messages,
            max_tokens=768,
            temperature=0.4,
            activity_operation=None,
        )
    except Exception as e:
        _log.debug("utility_complete raised for /btw: %s", e)
        return None
    if not text or not str(text).strip():
        return None
    return cap_str(str(text).strip(), _MAX_ANSWER_CHARS)


def _run_subagent_fallback(question: str, context_snippet: str) -> str | None:
    try:
        from subagent_runner import run_subagent
    except Exception:
        return None
    try:
        text = run_subagent(
            task_description=question,
            agent_type="btw",
            context_snippet=context_snippet or None,
            timeout_seconds=30,
            force_utility_model=False,
        )
    except Exception as e:
        _log.debug("subagent fallback failed for /btw: %s", e)
        return None
    if not text or not str(text).strip():
        return None
    return cap_str(str(text).strip(), _MAX_ANSWER_CHARS)


def _deliver_safely(
    deliver: Callable[..., None] | None,
    text: str,
    *,
    is_btw: bool = True,
) -> None:
    if deliver is None:
        return
    try:
        deliver(text, is_btw=is_btw)
    except TypeError:
        try:
            deliver(text)
        except Exception as e:
            _log.debug("btw deliver failed: %s", e)
    except Exception as e:
        _log.debug("btw deliver failed: %s", e)


def handle_btw(
    question: str,
    parent_thread_id: str | None,
    deliver: Callable[..., None] | None,
    *,
    prefer_utility: bool = True,
    btw_thread_id: str | None = None,
    cancellation_check: Callable[[], bool] | None = None,
) -> dict:
    """Run a single ``/btw`` side-question and deliver the answer.

    Returns a dict with keys ``ok``, ``source`` (``utility``/``subagent``/
    ``none``), ``btw_thread_id``, ``duration_ms``, and ``error`` (if any).

    ``btw_thread_id`` lets the caller pre-assign the ephemeral thread id so
    cancellation bookkeeping can reference it before the model call finishes.
    ``cancellation_check`` is polled before delivering the answer and any
    ledger row; when it returns True, the function returns early with
    ``{"ok": False, "error": "cancelled"}`` and the ledger row is marked
    ``status="skipped"`` with ``skip_reason="cancelled"``.

    This function is safe to call from a web-request thread; it does its own
    best-effort exception handling and always records a ledger entry.
    """
    def _cancelled() -> bool:
        if cancellation_check is None:
            return False
        try:
            return bool(cancellation_check())
        except Exception:
            return False

    q = cap_str(str(question or "").strip(), _MAX_QUESTION_CHARS)
    btw_tid = (btw_thread_id or _short_btw_id()).strip() or _short_btw_id()
    if not btw_tid.startswith(BTW_THREAD_PREFIX):
        btw_tid = _short_btw_id()
    t0 = time.monotonic()
    result: dict = {
        "ok": False,
        "source": "none",
        "btw_thread_id": btw_tid,
        "duration_ms": 0,
        "error": None,
    }
    if not q:
        result["error"] = "empty_question"
        _deliver_safely(
            deliver,
            "/btw needs a question — e.g. `/btw what is MLX?`",
            is_btw=True,
        )
        try:
            record_run(
                kind="btw",
                task_name="side_question",
                thread_id=btw_tid,
                status="skipped",
                skip_reason="empty_question",
                duration_ms=int((time.monotonic() - t0) * 1000),
                meta={"parent_thread_id": str(parent_thread_id or "") or None},
            )
        except Exception:
            pass
        return result

    context_snippet = _build_recent_snippet(parent_thread_id)
    answer: str | None = None
    source = "none"

    if prefer_utility:
        answer = _run_utility(q, context_snippet)
        if answer:
            source = "utility"
    if not answer:
        answer = _run_subagent_fallback(q, context_snippet)
        if answer:
            source = "subagent"

    duration_ms = int((time.monotonic() - t0) * 1000)
    result["duration_ms"] = duration_ms

    if _cancelled():
        result["error"] = "cancelled"
        try:
            record_run(
                kind="btw",
                task_name="side_question",
                thread_id=btw_tid,
                status="skipped",
                skip_reason="cancelled",
                duration_ms=duration_ms,
                meta={"had_answer": bool(answer), "source": source},
            )
        except Exception:
            pass
        return result

    if answer:
        result["ok"] = True
        result["source"] = source
        _deliver_safely(deliver, answer, is_btw=True)
        try:
            record_run(
                kind="btw",
                task_name="side_question",
                thread_id=btw_tid,
                status="ok",
                duration_ms=duration_ms,
                meta={
                    "source": source,
                    "has_context": bool(context_snippet),
                    # Do not record the question or answer body — /btw is
                    # intentionally invisible. Only a redacted preview is kept
                    # to help operators diagnose failures.
                    "question_preview": redact_text(cap_str(q, 120)),
                },
            )
        except Exception:
            pass
        return result

    # Both paths failed.
    err_msg = (
        "/btw couldn't produce an answer. The utility model may be disabled or the "
        "subagent path timed out. Try again, or ask normally without /btw."
    )
    _deliver_safely(deliver, err_msg, is_btw=True)
    result["error"] = "no_answer"
    try:
        record_run(
            kind="btw",
            task_name="side_question",
            thread_id=btw_tid,
            status="error",
            duration_ms=duration_ms,
            error_summary="no_answer",
            meta={
                "has_context": bool(context_snippet),
                "question_preview": redact_text(cap_str(q, 120)),
            },
        )
    except Exception:
        pass
    return result


def handle_btw_async(
    question: str,
    parent_thread_id: str | None,
    deliver: Callable[..., None] | None,
    *,
    prefer_utility: bool = True,
    btw_thread_id: str | None = None,
    cancellation_check: Callable[[], bool] | None = None,
) -> threading.Thread:
    """Fire-and-forget version used by slash-command handlers.

    Returns the started daemon thread so tests can join on completion.
    """
    th = threading.Thread(
        target=handle_btw,
        args=(question, parent_thread_id, deliver),
        kwargs={
            "prefer_utility": prefer_utility,
            "btw_thread_id": btw_thread_id,
            "cancellation_check": cancellation_check,
        },
        daemon=True,
        name=f"btw-{uuid.uuid4().hex[:6]}",
    )
    th.start()
    return th
