from collections.abc import AsyncIterator
import json
from pathlib import Path
import secrets

from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, Response, UploadFile, status
from fastapi.responses import FileResponse, StreamingResponse
import httpx

from app.api.deps import get_optional_session, require_session
from app.core.config import get_settings
from app.core.mock_state import MOCK_TENANT, MOCK_VENUES, TENANT_MODULES
from app.models.assistant import AssistantChatRequest, AssistantChatResponse, AssistantMessage, AssistantThreadResponse
from app.models.llm_settings import MenuAssetRead, MenuAssetRenamePayload, MenuPromptSettingsPayload, MenuPromptSettingsResponse
from app.services.llm_client import (
    build_llm_headers,
    build_llm_timeout,
    describe_llm_http_error,
    request_llm_chat_completion,
    resolve_llm_model,
)
from app.services.menu_asset_service import build_menu_assets_context, ingest_menu_asset, normalize_asset_name
from app.services.operational_assistant_service import run_operational_assistant_with_trace
from app.services.venue_prompt import (
    DEFAULT_MENU_BASE_PROMPT,
    GENERIC_OPERATIONAL_CONTEXTS,
    build_menu_assistant_description,
    build_menu_prompt_preview,
)
from app.services.tenant_store import SessionIdentity, get_tenant_store
from shared.assistant_profiles import compose_assistant_prompt, get_assistant_profile, list_assistant_profiles


router = APIRouter()


def _require_menu_access(session: SessionIdentity) -> SessionIdentity:
    if not get_tenant_store().session_has_permission(session, "menu"):
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Questo account non puo accedere a Menu")
    return session


def _assistant_access_denied_response(
    *,
    session: SessionIdentity,
    payload: AssistantChatRequest,
) -> AssistantChatResponse:
    reply = "Non possiedi l'autorizzazione per usare questo strumento. Chiedi all'amministratore del locale di abilitare l'Assistente dal pannello Account."
    store = get_tenant_store()
    thread = store.ensure_assistant_thread(session, surface="home", thread_id=payload.thread_id)
    thread, _ = store.append_assistant_messages(
        thread.id,
        [
            {"role": "user", "content": payload.message.strip()},
            {"role": "assistant", "content": reply},
        ],
    )
    store.create_assistant_run(
        thread_id=thread.id,
        session=session,
        surface="home",
        route="home-access-denied",
        model="policy",
        user_message=payload.message.strip(),
        assistant_reply=reply,
        trace={"surface": "home", "reason": "assistant_permission_missing"},
    )
    return AssistantChatResponse(
        assistant_name=get_settings().assistant_display_name,
        reply=reply,
        model="policy",
        assistant_surface="home",
        thread_id=thread.id,
        updated_at=thread.updated_at,
    )


def _require_surface_access(session: SessionIdentity, surface: str) -> SessionIdentity:
    if surface == "menu":
        return _require_menu_access(session)
    return session


def _headers() -> dict[str, str]:
    return build_llm_headers()


def _base_url() -> str:
    return get_settings().llm_base_url.rstrip("/")


def _require_internal_llm_proxy_access(request: Request) -> None:
    expected_token = (get_settings().llm_proxy_internal_token or "").strip()
    if not expected_token:
        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Proxy LLM interno non configurato")

    provided_token = (request.headers.get("X-Internal-API-Token") or "").strip()
    if not provided_token or not secrets.compare_digest(provided_token, expected_token):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token proxy LLM non valido")


async def _resolve_proxy_payload(
    request: Request,
    *,
    client: httpx.AsyncClient,
    headers: dict[str, str],
    base_url: str,
) -> dict[str, object]:
    try:
        payload = await request.json()
    except Exception as exc:
        raise HTTPException(status_code=400, detail=f"Payload JSON non valido: {exc}") from exc

    if not isinstance(payload, dict):
        raise HTTPException(status_code=400, detail="Il payload LLM deve essere un oggetto JSON")

    normalized_payload = dict(payload)
    model = normalized_payload.get("model")
    if not model:
        normalized_payload["model"] = await resolve_llm_model(client, base_url, headers)

    return normalized_payload


def _resolve_venue_name(session: SessionIdentity) -> str:
    context = get_tenant_store().get_tenant_context(session.tenant_id)
    venues = context["venues"]
    if venues:
        return venues[0].name
    return context["tenant"].name


def _read_menu_generated_assets_context(stored_payload: dict[str, object]) -> str:
    raw_value = stored_payload.get("uploaded_assets_context")
    return raw_value.strip() if isinstance(raw_value, str) else ""


def _read_menu_assets_context_override(stored_payload: dict[str, object]) -> str:
    raw_value = stored_payload.get("uploaded_assets_context_override")
    return raw_value.strip() if isinstance(raw_value, str) else ""


def _build_menu_navigation_urls() -> dict[str, str]:
    settings = get_settings()
    portal_base = settings.portal_public_url.rstrip("/")
    return {
        "dashboard_url": f"{portal_base}/dashboard",
        "documents_url": f"{portal_base}/documents",
        "ordini_url": settings.ordini_frontend_public_url,
        "prenotazioni_url": settings.prenotazioni_frontend_public_url,
        "menu_url": settings.menu_legacy_public_url,
        "homemade_url": f"{portal_base}/modules/homemade",
    }


def _serialize_menu_asset(asset) -> MenuAssetRead:
    return MenuAssetRead(
        id=asset.id,
        original_name=asset.original_name,
        display_name=asset.display_name,
        mime_type=asset.mime_type,
        kind=asset.kind,
        file_size_bytes=asset.file_size_bytes,
        status=asset.status,
        error_detail=asset.error_detail,
        analysis_text=asset.analysis_text,
        created_at=asset.created_at,
        updated_at=asset.updated_at,
    )


def _serialize_menu_settings(
    session: SessionIdentity,
    payload: MenuPromptSettingsPayload,
    *,
    updated_at: str | None,
    uploaded_assets_context_generated: str = "",
    uploaded_assets_context_override: str = "",
    assets: list | None = None,
) -> MenuPromptSettingsResponse:
    venue_name = _resolve_venue_name(session)
    serialized_assets = [_serialize_menu_asset(asset) for asset in assets or []]
    normalized_payload = MenuPromptSettingsPayload(
        sales_focus=payload.sales_focus.strip(),
        menu_text="",
        cocktail_list_text="",
        uploaded_assets_context_override=payload.uploaded_assets_context_override.strip(),
    )
    effective_uploaded_assets_context = (
        normalized_payload.uploaded_assets_context_override or uploaded_assets_context_generated.strip()
    )
    navigation_urls = _build_menu_navigation_urls()
    return MenuPromptSettingsResponse(
        tenant_name=session.tenant_name,
        tenant_slug=session.tenant_slug,
        venue_name=venue_name,
        base_prompt=DEFAULT_MENU_BASE_PROMPT,
        prompt_preview=build_menu_prompt_preview(
            venue_name=venue_name,
            sales_focus=normalized_payload.sales_focus,
            uploaded_assets_context=effective_uploaded_assets_context,
        ),
        uploaded_assets_context=effective_uploaded_assets_context,
        uploaded_assets_context_generated=uploaded_assets_context_generated.strip(),
        assistant_description=build_menu_assistant_description(venue_name),
        assets=serialized_assets,
        asset_total_count=len(serialized_assets),
        asset_ready_count=sum(1 for asset in serialized_assets if asset.status == "ready"),
        updated_at=updated_at,
        **navigation_urls,
        **normalized_payload.model_dump(),
    )


def _load_menu_settings(session: SessionIdentity) -> MenuPromptSettingsResponse:
    store = get_tenant_store()
    stored_payload, updated_at = store.get_llm_settings(session.tenant_id, "menu")
    payload = MenuPromptSettingsPayload.model_validate(stored_payload)
    return _serialize_menu_settings(
        session,
        payload,
        updated_at=updated_at,
        uploaded_assets_context_generated=_read_menu_generated_assets_context(stored_payload),
        uploaded_assets_context_override=_read_menu_assets_context_override(stored_payload),
        assets=store.list_menu_assets(session.tenant_id),
    )


def _build_menu_chat_prompt(session: SessionIdentity) -> tuple[str, dict[str, object]]:
    store = get_tenant_store()
    stored_payload, _ = store.get_llm_settings(session.tenant_id, "menu")
    payload = MenuPromptSettingsPayload.model_validate(stored_payload)
    uploaded_assets_context_generated = _read_menu_generated_assets_context(stored_payload)
    uploaded_assets_context_override = _read_menu_assets_context_override(stored_payload)
    effective_uploaded_assets_context = uploaded_assets_context_override or uploaded_assets_context_generated
    assets = store.list_menu_assets(session.tenant_id)
    prompt = build_menu_prompt_preview(
        venue_name=_resolve_venue_name(session),
        sales_focus=payload.sales_focus,
        uploaded_assets_context=effective_uploaded_assets_context,
    )
    trace_context: dict[str, object] = {
        "surface": "menu",
        "venue_name": _resolve_venue_name(session),
        "sales_focus_configured": bool(payload.sales_focus.strip()),
        "assets_count": len(assets),
        "uploaded_assets_context_present": bool(effective_uploaded_assets_context.strip()),
    }
    return prompt, trace_context


async def _run_surface_llm_chat(
    *,
    session: SessionIdentity,
    payload: AssistantChatRequest,
    surface: str,
    system_prompt: str,
    trace_context: dict[str, object] | None = None,
) -> AssistantChatResponse:
    store = get_tenant_store()
    thread = store.ensure_assistant_thread(session, surface=surface, thread_id=payload.thread_id)
    stored_messages = store.list_assistant_messages(thread.id, limit=40)
    if not stored_messages and payload.conversation:
        seed_messages = [message.model_dump() for message in payload.conversation[-24:]]
        thread, _ = store.append_assistant_messages(thread.id, seed_messages)
        stored_messages = store.list_assistant_messages(thread.id, limit=40)

    messages = [{"role": "system", "content": system_prompt}]
    messages.extend({"role": message.role, "content": message.content} for message in stored_messages[-24:])
    user_message = payload.message.strip()
    messages.append({"role": "user", "content": user_message})

    reply, model = await request_llm_chat_completion(messages)
    thread, _ = store.append_assistant_messages(
        thread.id,
        [
            {"role": "user", "content": user_message},
            {"role": "assistant", "content": reply},
        ],
    )
    trace = {
        "surface": surface,
        "message_count_before": len(stored_messages),
        **(trace_context or {}),
    }
    store.create_assistant_run(
        thread_id=thread.id,
        session=session,
        surface=surface,
        route=f"{surface}-llm-chat",
        model=model,
        user_message=user_message,
        assistant_reply=reply,
        trace=trace,
    )
    return AssistantChatResponse(
        assistant_name=get_settings().assistant_display_name,
        reply=reply,
        model=model,
        assistant_surface=surface,  # type: ignore[arg-type]
        thread_id=thread.id,
        updated_at=thread.updated_at,
    )


def _merge_menu_settings(session: SessionIdentity, updates: dict[str, object]) -> str:
    store = get_tenant_store()
    current_payload, _ = store.get_llm_settings(session.tenant_id, "menu")
    merged_payload = dict(current_payload)
    merged_payload.update(updates)
    return store.upsert_llm_settings(session.tenant_id, "menu", merged_payload)


async def _refresh_menu_assets_context(session: SessionIdentity) -> None:
    store = get_tenant_store()
    context = await build_menu_assets_context(_resolve_venue_name(session), store.list_menu_assets(session.tenant_id))
    current_payload, _ = store.get_llm_settings(session.tenant_id, "menu")
    merged_payload = dict(current_payload)
    if context:
        merged_payload["uploaded_assets_context"] = context
    else:
        merged_payload.pop("uploaded_assets_context", None)
    store.upsert_llm_settings(session.tenant_id, "menu", merged_payload)


def build_assistant_prompt(session: SessionIdentity | None = None) -> str:
    settings = get_settings()
    home_profile = get_assistant_profile("home")
    configured_system_prompt = settings.assistant_system_prompt.strip()
    system_prompt = configured_system_prompt or home_profile.base_prompt
    if configured_system_prompt == "Sei l'assistente operativo interno del locale. Rispondi sempre in italiano, con tono sobrio, pratico e orientato all'azione.":
        system_prompt = home_profile.base_prompt
    operational_context = settings.assistant_operational_context.strip()
    extra_sections: tuple[tuple[str, str], ...] = ()
    if operational_context and operational_context not in GENERIC_OPERATIONAL_CONTEXTS:
        extra_sections = (("Contesto operativo aggiuntivo", operational_context),)

    return compose_assistant_prompt(
        "home",
        venue_name=_resolve_venue_name(session) if session is not None else None,
        extra_sections=extra_sections,
        override_base_prompt=system_prompt,
    )


def _serialize_assistant_thread(
    session: SessionIdentity,
    *,
    surface: str = "home",
    thread_id: str | None = None,
) -> AssistantThreadResponse:
    store = get_tenant_store()
    thread = store.get_assistant_thread(thread_id) if thread_id else store.get_active_assistant_thread(session, surface)
    if (
        thread is None
        or thread.status != "active"
        or thread.tenant_id != session.tenant_id
        or thread.user_id != session.user_id
        or thread.surface != surface
    ):
        return AssistantThreadResponse(
            assistant_name=get_settings().assistant_display_name,
            assistant_surface=surface,  # type: ignore[arg-type]
            thread_id=None,
            updated_at=None,
            messages=[],
        )

    messages = [
        AssistantMessage(role=message.role, content=message.content)
        for message in store.list_assistant_messages(thread.id, limit=40)
    ]
    return AssistantThreadResponse(
        assistant_name=get_settings().assistant_display_name,
        assistant_surface=surface,  # type: ignore[arg-type]
        thread_id=thread.id,
        updated_at=thread.updated_at,
        messages=messages,
    )


@router.get("/config")
def llm_config(_: SessionIdentity = Depends(require_session)) -> dict[str, object]:
    settings = get_settings()
    base_url = _base_url()
    return {
        "base_url": base_url,
        "models_url": f"{base_url}/models",
        "chat_completions_url": f"{base_url}/chat/completions",
        "api_key_configured": bool(settings.llm_api_key),
        "assistant_name": settings.assistant_display_name,
        "assistant_model": settings.assistant_model,
    }


@router.get("/profiles")
def assistant_profiles(_: SessionIdentity = Depends(require_session)) -> dict[str, object]:
    profiles = list_assistant_profiles()
    return {
        "items": [
            {
                "key": profile.key,
                "label": profile.label,
                "summary": profile.summary,
                "allowed_tools": list(profile.allowed_tools),
            }
            for profile in profiles
        ]
    }


@router.get("/upstream-health")
async def llm_upstream_health(_: SessionIdentity = Depends(require_session)) -> dict[str, object]:
    settings = get_settings()
    base_url = _base_url()
    headers = _headers()

    try:
        async with httpx.AsyncClient(timeout=build_llm_timeout()) as client:
            response = await client.get(f"{base_url}/models", headers=headers)
            response.raise_for_status()
            payload = response.json()
    except httpx.HTTPError as exc:
        return {
            "reachable": False,
            "base_url": base_url,
            "detail": describe_llm_http_error(exc),
        }

    models = payload.get("data", [])
    model_ids = [item.get("id") for item in models if isinstance(item, dict) and item.get("id")]

    return {
        "reachable": True,
        "base_url": base_url,
        "model_count": len(model_ids),
        "models": model_ids[:10],
    }


@router.get("/assistant-profile")
def assistant_profile(session: SessionIdentity | None = Depends(get_optional_session)) -> dict[str, object]:
    settings = get_settings()
    home_profile = get_assistant_profile("home")
    if session is not None:
        context = get_tenant_store().get_tenant_context(session.tenant_id)
        return {
            "assistant_name": settings.assistant_display_name,
            "assistant_surface": home_profile.key,
            "assistant_summary": home_profile.summary,
            "tenant_slug": context["tenant"].slug,
            "venue_names": [venue.name for venue in context["venues"]],
            "enabled_modules": [entry.module_key for entry in context["tenant_modules"] if entry.enabled],
        }

    return {
        "assistant_name": settings.assistant_display_name,
        "assistant_surface": home_profile.key,
        "assistant_summary": home_profile.summary,
        "tenant_slug": MOCK_TENANT.slug,
        "venue_names": [venue.name for venue in MOCK_VENUES],
        "enabled_modules": [entry.module_key for entry in TENANT_MODULES if entry.enabled],
    }


@router.get("/chat/thread", response_model=AssistantThreadResponse)
def assistant_chat_thread(
    thread_id: str | None = Query(default=None),
    surface: str = Query(default="home", pattern="^(home|documents|menu)$"),
    session: SessionIdentity = Depends(require_session),
) -> AssistantThreadResponse:
    session = _require_surface_access(session, surface)
    return _serialize_assistant_thread(session, surface=surface, thread_id=thread_id)


@router.delete("/chat/thread", status_code=status.HTTP_204_NO_CONTENT)
def clear_assistant_chat_thread(
    thread_id: str | None = Query(default=None),
    surface: str = Query(default="home", pattern="^(home|documents|menu)$"),
    session: SessionIdentity = Depends(require_session),
) -> Response:
    session = _require_surface_access(session, surface)
    get_tenant_store().archive_assistant_thread(session, surface=surface, thread_id=thread_id)
    return Response(status_code=status.HTTP_204_NO_CONTENT)


@router.get("/chat/thread/runs")
def assistant_chat_thread_runs(
    thread_id: str,
    limit: int = Query(default=10, ge=1, le=50),
    session: SessionIdentity = Depends(require_session),
) -> dict[str, object]:
    thread = get_tenant_store().get_assistant_thread(thread_id)
    if thread is None or thread.tenant_id != session.tenant_id or thread.user_id != session.user_id:
        raise HTTPException(status_code=404, detail="Thread assistente non trovato.")

    runs = get_tenant_store().list_assistant_runs(thread_id, limit=limit)
    return {
        "thread_id": thread_id,
        "items": [
            {
                "id": run.id,
                "surface": run.surface,
                "route": run.route,
                "model": run.model,
                "user_message": run.user_message,
                "assistant_reply": run.assistant_reply,
                "trace": json.loads(run.trace_json),
                "created_at": run.created_at,
            }
            for run in runs
        ],
    }


@router.get("/menu-settings", response_model=MenuPromptSettingsResponse)
def menu_settings(session: SessionIdentity = Depends(require_session)) -> MenuPromptSettingsResponse:
    return _load_menu_settings(_require_menu_access(session))


@router.put("/menu-settings", response_model=MenuPromptSettingsResponse)
def update_menu_settings(
    payload: MenuPromptSettingsPayload,
    session: SessionIdentity = Depends(require_session),
) -> MenuPromptSettingsResponse:
    session = _require_menu_access(session)
    normalized_payload = MenuPromptSettingsPayload(
        sales_focus=payload.sales_focus.strip(),
        menu_text="",
        cocktail_list_text="",
        uploaded_assets_context_override=payload.uploaded_assets_context_override.strip(),
    )
    _merge_menu_settings(session, normalized_payload.model_dump())
    return _load_menu_settings(session)


@router.post("/menu-settings/assets", response_model=MenuPromptSettingsResponse)
async def upload_menu_assets(
    files: list[UploadFile] = File(...),
    session: SessionIdentity = Depends(require_session),
) -> MenuPromptSettingsResponse:
    session = _require_menu_access(session)
    if not files:
        raise HTTPException(status_code=400, detail="Seleziona almeno un file da caricare.")

    venue_name = _resolve_venue_name(session)
    for upload in files:
        await ingest_menu_asset(session, upload, venue_name=venue_name)

    await _refresh_menu_assets_context(session)
    return _load_menu_settings(session)


@router.patch("/menu-settings/assets/{asset_id}", response_model=MenuPromptSettingsResponse)
def rename_menu_asset(
    asset_id: str,
    payload: MenuAssetRenamePayload,
    session: SessionIdentity = Depends(require_session),
) -> MenuPromptSettingsResponse:
    session = _require_menu_access(session)
    normalized_name = normalize_asset_name(payload.display_name)
    asset = get_tenant_store().get_menu_asset(session.tenant_id, asset_id)
    if asset is None:
        raise HTTPException(status_code=404, detail="File menu non trovato.")

    get_tenant_store().update_menu_asset(session.tenant_id, asset_id, display_name=normalized_name)
    return _load_menu_settings(session)


@router.delete("/menu-settings/assets/{asset_id}", response_model=MenuPromptSettingsResponse)
async def delete_menu_asset(
    asset_id: str,
    session: SessionIdentity = Depends(require_session),
) -> MenuPromptSettingsResponse:
    session = _require_menu_access(session)
    asset = get_tenant_store().delete_menu_asset(session.tenant_id, asset_id)
    if asset is None:
        raise HTTPException(status_code=404, detail="File menu non trovato.")

    storage_path = Path(asset.storage_path)
    if storage_path.exists():
        storage_path.unlink()

    await _refresh_menu_assets_context(session)
    return _load_menu_settings(session)


@router.get("/menu-settings/assets/{asset_id}/download")
def download_menu_asset(
    asset_id: str,
    session: SessionIdentity = Depends(require_session),
):
    session = _require_menu_access(session)
    asset = get_tenant_store().get_menu_asset(session.tenant_id, asset_id)
    if asset is None:
        raise HTTPException(status_code=404, detail="File menu non trovato.")

    storage_path = Path(asset.storage_path)
    if not storage_path.exists():
        raise HTTPException(status_code=404, detail="Il file caricato non e' piu disponibile sul server.")

    return FileResponse(storage_path, media_type=asset.mime_type, filename=asset.display_name)


@router.post("/openai/chat/completions")
async def llm_openai_chat_completions(request: Request):
    _require_internal_llm_proxy_access(request)
    base_url = _base_url()
    headers = _headers()

    if request.headers.get("accept", "").lower().find("text/event-stream") != -1:
        headers["Accept"] = "text/event-stream"

    async with httpx.AsyncClient(timeout=build_llm_timeout()) as client:
        payload = await _resolve_proxy_payload(request, client=client, headers=headers, base_url=base_url)

    wants_stream = bool(payload.get("stream"))
    endpoint = f"{base_url}/chat/completions"

    if wants_stream:
        client = httpx.AsyncClient(timeout=None)
        upstream_request = client.build_request(
            "POST",
            endpoint,
            headers=headers,
            json=payload,
        )
        try:
            upstream_response = await client.send(upstream_request, stream=True)
        except httpx.HTTPError as exc:
            await client.aclose()
            raise HTTPException(status_code=502, detail=f"Errore comunicazione LLM: {describe_llm_http_error(exc)}") from exc

        if upstream_response.is_error:
            error_text = await upstream_response.aread()
            await upstream_response.aclose()
            await client.aclose()
            detail = error_text.decode("utf-8", errors="ignore").strip() or upstream_response.reason_phrase
            raise HTTPException(
                status_code=502,
                detail=f"Errore comunicazione LLM: {upstream_response.status_code} {detail}",
            )

        async def stream_upstream() -> AsyncIterator[bytes]:
            try:
                async for chunk in upstream_response.aiter_bytes():
                    if chunk:
                        yield chunk
            finally:
                await upstream_response.aclose()
                await client.aclose()

        media_type = upstream_response.headers.get("content-type", "text/event-stream")
        return StreamingResponse(stream_upstream(), status_code=upstream_response.status_code, media_type=media_type)

    try:
        async with httpx.AsyncClient(timeout=build_llm_timeout()) as client:
            response = await client.post(endpoint, headers=headers, json=payload)
            response.raise_for_status()
    except httpx.HTTPError as exc:
        raise HTTPException(status_code=502, detail=f"Errore comunicazione LLM: {describe_llm_http_error(exc)}") from exc

    return response.json()


@router.post("/chat", response_model=AssistantChatResponse)
async def assistant_chat(
    payload: AssistantChatRequest,
    session: SessionIdentity = Depends(require_session),
) -> AssistantChatResponse:
    if not get_tenant_store().session_has_permission(session, "assistant"):
        return _assistant_access_denied_response(session=session, payload=payload)
    store = get_tenant_store()
    thread = store.ensure_assistant_thread(session, surface="home", thread_id=payload.thread_id)
    stored_messages = store.list_assistant_messages(thread.id, limit=40)
    stored_thread_state = store.get_assistant_thread_state(thread.id)
    if not stored_messages and payload.conversation:
        seed_messages = [message.model_dump() for message in payload.conversation[-24:]]
        thread, _ = store.append_assistant_messages(thread.id, seed_messages)
        stored_messages = store.list_assistant_messages(thread.id, limit=40)

    outcome = await run_operational_assistant_with_trace(
        session=session,
        message=payload.message.strip(),
        conversation=[{"role": message.role, "content": message.content} for message in stored_messages[-24:]],
        thread_state=stored_thread_state,
    )
    thread, _ = store.append_assistant_messages(
        thread.id,
        [
            {"role": "user", "content": payload.message.strip()},
            {"role": "assistant", "content": outcome.reply},
        ],
    )
    if outcome.thread_state is not None:
        thread = store.update_assistant_thread_state(thread.id, outcome.thread_state)
    store.create_assistant_run(
        thread_id=thread.id,
        session=session,
        surface="home",
        route=outcome.route,
        model=outcome.model,
        user_message=payload.message.strip(),
        assistant_reply=outcome.reply,
        trace=outcome.trace,
    )
    return AssistantChatResponse(
        assistant_name=get_settings().assistant_display_name,
        reply=outcome.reply,
        model=outcome.model,
        assistant_surface="home",
        thread_id=thread.id,
        updated_at=thread.updated_at,
    )


@router.post("/menu-chat", response_model=AssistantChatResponse)
async def menu_chat(
    payload: AssistantChatRequest,
    session: SessionIdentity = Depends(require_session),
) -> AssistantChatResponse:
    session = _require_menu_access(session)
    system_prompt, trace_context = _build_menu_chat_prompt(session)
    return await _run_surface_llm_chat(
        session=session,
        payload=payload,
        surface="menu",
        system_prompt=system_prompt,
        trace_context=trace_context,
    )
