"""Bark/conversation — visitor chat engine and Matrix bark endpoint."""

import asyncio
import json
import logging
import time
from collections import deque

from fastapi import APIRouter
from fastapi.responses import JSONResponse
from pydantic import BaseModel

from infrastructure.presence import produce_bark

from .commitments import (
    _build_commitment_context,
    _record_commitments,
    _tick_commitments,
)

logger = logging.getLogger(__name__)

matrix_router = APIRouter(prefix="/api/matrix", tags=["matrix"])

# Rate limiting: 1 request per 3 seconds per visitor_id
_BARK_RATE_LIMIT_SECONDS = 3
_bark_last_request: dict[str, float] = {}

# Recent conversation buffer — kept in memory for the Workshop overlay.
# Stores the last _MAX_EXCHANGES (visitor_text, timmy_text) pairs.
_MAX_EXCHANGES = 3
_conversation: deque[dict] = deque(maxlen=_MAX_EXCHANGES)

_WORKSHOP_SESSION_ID = "workshop"

# Conversation grounding — anchor to opening topic so Timmy doesn't drift.
_ground_topic: str | None = None
_ground_set_at: float = 0.0
_GROUND_TTL = 300  # seconds of inactivity before the anchor expires


class BarkRequest(BaseModel):
    """Request body for POST /api/matrix/bark."""

    text: str
    visitor_id: str


@matrix_router.post("/bark")
async def post_matrix_bark(request: BarkRequest) -> JSONResponse:
    """Generate a bark response for a visitor message.

    HTTP fallback for when WebSocket isn't available. The Matrix frontend
    can POST a message and get Timmy's bark response back as JSON.

    Rate limited to 1 request per 3 seconds per visitor_id.

    Request body:
        - text: The visitor's message text
        - visitor_id: Unique identifier for the visitor (used for rate limiting)

    Returns:
        - 200: Bark message in produce_bark() format
        - 429: Rate limit exceeded (try again later)
        - 422: Invalid request (missing/invalid fields)
    """
    # Validate inputs
    text = request.text.strip() if request.text else ""
    visitor_id = request.visitor_id.strip() if request.visitor_id else ""

    if not text:
        return JSONResponse(
            status_code=422,
            content={"error": "text is required"},
        )

    if not visitor_id:
        return JSONResponse(
            status_code=422,
            content={"error": "visitor_id is required"},
        )

    # Rate limiting check
    now = time.time()
    last_request = _bark_last_request.get(visitor_id, 0)
    time_since_last = now - last_request

    if time_since_last < _BARK_RATE_LIMIT_SECONDS:
        retry_after = _BARK_RATE_LIMIT_SECONDS - time_since_last
        return JSONResponse(
            status_code=429,
            content={"error": "Rate limit exceeded. Try again later."},
            headers={"Retry-After": str(int(retry_after) + 1)},
        )

    # Record this request
    _bark_last_request[visitor_id] = now

    # Generate bark response
    try:
        reply = await _generate_bark(text)
    except Exception as exc:
        logger.warning("Bark generation failed: %s", exc)
        reply = "Hmm, my thoughts are a bit tangled right now."

    # Build bark response using produce_bark format
    bark = produce_bark(agent_id="timmy", text=reply, style="speech")

    return JSONResponse(
        content=bark,
        headers={"Cache-Control": "no-cache, no-store"},
    )


def reset_conversation_ground() -> None:
    """Clear the conversation grounding anchor (e.g. after inactivity)."""
    global _ground_topic, _ground_set_at
    _ground_topic = None
    _ground_set_at = 0.0


def _refresh_ground(visitor_text: str) -> None:
    """Set or refresh the conversation grounding anchor.

    The first visitor message in a session (or after the TTL expires)
    becomes the anchor topic. Subsequent messages are grounded against it.
    """
    global _ground_topic, _ground_set_at
    now = time.time()
    if _ground_topic is None or (now - _ground_set_at) > _GROUND_TTL:
        _ground_topic = visitor_text[:120]
        logger.debug("Ground topic set: %s", _ground_topic)
    _ground_set_at = now


async def _bark_and_broadcast(visitor_text: str) -> None:
    """Generate a bark response and broadcast it to all Workshop clients."""
    from .websocket import _broadcast

    await _broadcast(json.dumps({"type": "timmy_thinking"}))

    # Notify Pip that a visitor spoke
    try:
        from timmy.familiar import pip_familiar

        pip_familiar.on_event("visitor_spoke")
    except Exception:
        logger.debug("Pip familiar notification failed (optional)")

    _refresh_ground(visitor_text)
    _tick_commitments()
    reply = await _generate_bark(visitor_text)
    _record_commitments(reply)

    _conversation.append({"visitor": visitor_text, "timmy": reply})

    await _broadcast(
        json.dumps(
            {
                "type": "timmy_speech",
                "text": reply,
                "recentExchanges": list(_conversation),
            }
        )
    )


async def _generate_bark(visitor_text: str) -> str:
    """Generate a short in-character bark response.

    Uses the existing Timmy session with a dedicated workshop session ID.
    When a grounding anchor exists, the opening topic is prepended so the
    model stays on-topic across long sessions.
    Gracefully degrades to a canned response if inference fails.
    """
    try:
        from timmy import session as _session

        grounded = visitor_text
        commitment_ctx = _build_commitment_context()
        if commitment_ctx:
            grounded = f"{commitment_ctx}\n{grounded}"
        if _ground_topic and visitor_text != _ground_topic:
            grounded = f"[Workshop conversation topic: {_ground_topic}]\n{grounded}"
        response = await _session.chat(grounded, session_id=_WORKSHOP_SESSION_ID)
        return response
    except Exception as exc:
        logger.warning("Bark generation failed: %s", exc)
        return "Hmm, my thoughts are a bit tangled right now."


def _log_bark_failure(task: asyncio.Task) -> None:
    """Log unhandled exceptions from fire-and-forget bark tasks."""
    if task.cancelled():
        return
    exc = task.exception()
    if exc is not None:
        logger.error("Bark task failed: %s", exc)


async def _handle_client_message(raw: str) -> None:
    """Dispatch an incoming WebSocket frame from the Workshop client."""
    try:
        data = json.loads(raw)
    except (json.JSONDecodeError, TypeError):
        return  # ignore non-JSON keep-alive pings

    if data.get("type") == "visitor_message":
        text = (data.get("text") or "").strip()
        if text:
            task = asyncio.create_task(_bark_and_broadcast(text))
            task.add_done_callback(_log_bark_failure)
