"""Assistant routes."""

from __future__ import annotations

import json
import logging
import os
from pathlib import Path
from typing import Literal

from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect, status
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.core.config import settings
from app.core.db import get_db
from app.core.i18n import get_language_pack, normalize_language, translate
from app.schemas.assistant import (
    AssistantChatRequest,
    AssistantChatResponse,
    AssistantMarkdownRenderRequest,
    AssistantMarkdownRenderResponse,
    AssistantQueryRequest,
    AssistantQueryResponse,
    AssistantUiPromptRequest,
)
from app.services import assistant_service
from app.services import markdown_renderer
from app.services import runtime_config
from app.services.ui_events import event_hub
from app.services.voice_runtime import voice_runtime

LOGGER = logging.getLogger("sentineledge.assistant.ui")
STOP_RESPONSE_HOLD_SECONDS = 3.0

TEMPLATES_DIR = Path(__file__).resolve().parents[2] / "templates"
try:
    templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
except Exception:  # pragma: no cover - dependency availability issue at runtime
    templates = None
else:
    templates.env.globals["app_name"] = settings.app_name

router = APIRouter(prefix="/assistant", tags=["assistant"])


class AssistantUiModeRequest(BaseModel):
    """Payload for syncing active app-mode UI screen."""

    mode: Literal["assistant", "config"]


class AssistantUiStartupPhaseRequest(BaseModel):
    """Lightweight startup telemetry payload from Assistant/Config UI."""

    phase: str
    detail: str | None = None
    ts_ms: int | None = None


_BUSY_UI_STATES = {"transcribing", "thinking", "speaking", "cooldown", "paused"}


def _force_assistant_idle(*, reason: str) -> None:
    """Reset runtime/UI state to idle when returning from config screen."""
    voice_runtime.stop_listening()
    voice_runtime.clear_active_cycle()
    voice_runtime.update_status(
        state="idle",
        input_level_rms=0.0,
        input_level_peak=0,
        input_level_norm=0.0,
    )
    event_hub.broadcast({"type": "state", "state": "idle"})
    LOGGER.info("Assistant idle forced reason=%s", reason)


def _assistant_busy_reason() -> str | None:
    """Return busy reason when assistant input must remain locked."""
    if runtime_config.is_voice_capture_paused():
        return "paused"

    snapshot = voice_runtime.snapshot()
    current_state = str(snapshot.get("state") or "idle")
    if current_state == "paused":
        # Config screen pause can linger briefly after mode switches back.
        return None
    if current_state in _BUSY_UI_STATES:
        return current_state

    cycle_snapshot = voice_runtime.active_cycle_snapshot()
    cycle_id = str(cycle_snapshot.get("cycle_id") or "").strip()
    cycle_state = str(cycle_snapshot.get("state") or "idle").strip().lower()
    stop_requested = bool(cycle_snapshot.get("stop_requested"))
    if stop_requested:
        return "stopping"
    if cycle_id and cycle_state not in {"", "idle"}:
        return cycle_state

    ui_snapshot = event_hub.get_ui_state()
    ui_phase = str(ui_snapshot.get("uiPhase") or "idle")
    if ui_phase in {"active", "hold", "paused"} and ui_snapshot.get("cycle_id"):
        return f"ui_{ui_phase}"

    return None


def _request_response_stop() -> dict[str, object]:
    """Request active assistant response interruption and emit cooldown state."""
    stop_result = voice_runtime.request_response_stop()
    cycle_id = stop_result.get("cycle_id")

    reason = str(stop_result.get("reason") or "")
    if stop_result.get("accepted"):
        event_hub.broadcast(
            {
                "type": "state",
                "state": "audio_finished",
                "seconds": STOP_RESPONSE_HOLD_SECONDS,
                "cycle_id": cycle_id,
            },
        )
        return {
            "ok": True,
            "duplicate": False,
            "detail": "Stop requested",
            "cycle_id": cycle_id,
            "seconds": STOP_RESPONSE_HOLD_SECONDS,
        }

    if reason == "already_requested":
        return {
            "ok": True,
            "duplicate": True,
            "detail": "Stop already requested",
            "cycle_id": cycle_id,
            "seconds": STOP_RESPONSE_HOLD_SECONDS,
        }

    return {
        "ok": False,
        "duplicate": False,
        "detail": "No active assistant response",
        "cycle_id": cycle_id,
    }


def _localized_route_error(message_key: str, exc: Exception) -> str:
    """Build localized fallback error for unexpected assistant route failures."""
    language = runtime_config.get_default_language("es")
    detail = str(exc) or "unknown error"
    return translate(message_key, language, error=detail)


def _require_dev_endpoint() -> None:
    """Hide debug/test endpoints in production deployments."""
    if settings.is_production:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found")


@router.get("/ui", response_class=HTMLResponse)
def assistant_ui(request: Request) -> HTMLResponse:
    """Render Assistant Mode avatar UI."""
    previous_mode = runtime_config.get_app_ui_mode()
    runtime_config.set_app_ui_mode(runtime_config.UI_MODE_ASSISTANT)
    if previous_mode == runtime_config.UI_MODE_CONFIG:
        _force_assistant_idle(reason="assistant.ui.render")
    LOGGER.info("Assistant UI render mode=assistant from=%s", request.query_params.get("from"))
    if templates is None:
        return HTMLResponse(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            content=(
                "<html><head>"
                "<link rel='icon' type='image/png' href='/static/favicon.png'>"
                "<link rel='apple-touch-icon' href='/static/favicon.png'>"
                "</head><body><h1>Assistant UI dependencies missing</h1>"
                "<p>Install requirements to enable Jinja2 templates.</p>"
                "</body></html>"
            ),
        )

    ui_language = normalize_language(runtime_config.get_default_language("es"))
    context = {
        "request": request,
        "title": f"{settings.app_name} Assistant Mode",
        "debug_ui": not settings.is_production,
        "post_response_hold_seconds": _ui_post_response_hold_seconds(),
        "ui_language": ui_language,
        "t": lambda key, **kwargs: translate(key, ui_language, **kwargs),
        "i18n_strings": get_language_pack(ui_language, namespace="assistant"),
    }
    return templates.TemplateResponse("assistant_ui.html", context=context)


@router.websocket("/ws")
async def assistant_ui_ws(websocket: WebSocket) -> None:
    """Stream live voice-state events for Assistant Mode avatar UI."""
    await websocket.accept()
    client = websocket.client
    client_host = client.host if client else "unknown"
    snapshot = event_hub.get_ui_state()
    LOGGER.info(
        "Assistant UI WS connect from %s active_cycle=%s uiPhase=%s speaking_lock=%s",
        client_host,
        snapshot.get("cycle_id"),
        snapshot.get("uiPhase"),
        snapshot.get("is_speaking_lock"),
    )
    event_hub.register(websocket)

    try:
        await websocket.send_json(event_hub.get_reconnect_state_event())
        while True:
            raw_message = await websocket.receive_text()
            if not raw_message or raw_message == "ping":
                continue
            try:
                payload = json.loads(raw_message)
            except ValueError:
                continue
            if not isinstance(payload, dict):
                continue
            if str(payload.get("type", "")).strip().lower() != "command":
                continue

            command = str(payload.get("command", "")).strip().lower()
            if command == "ptt_start":
                snapshot = voice_runtime.snapshot()
                microphone_available = bool(snapshot.get("microphone_available"))
                if not microphone_available:
                    if not settings.is_production:
                        LOGGER.info(
                            "PTT start rejected client=%s reason=no_microphone detail=%s",
                            client_host,
                            snapshot.get("microphone_error"),
                        )
                    await websocket.send_json(
                        {
                            "type": "command_result",
                            "command": command,
                            "ok": False,
                            "detail": str(snapshot.get("microphone_error") or "No microphone detected / input unavailable"),
                        },
                    )
                    continue
                busy_reason = _assistant_busy_reason()
                if busy_reason:
                    if not settings.is_production:
                        LOGGER.info(
                            "PTT start rejected client=%s reason=busy state=%s",
                            client_host,
                            busy_reason,
                        )
                    await websocket.send_json(
                        {
                            "type": "command_result",
                            "command": command,
                            "ok": False,
                            "detail": f"Voice busy ({busy_reason})",
                        },
                    )
                    continue

                voice_runtime.start_listening()
                if not settings.is_production:
                    LOGGER.info(
                        "PTT start client=%s backend=%s device=%s idx=%s",
                        client_host,
                        snapshot.get("backend", "unknown"),
                        snapshot.get("input_device_name", "none"),
                        snapshot.get("input_device_index"),
                    )
                status_payload = {
                    "type": "voice_status",
                    "state": "listening",
                    "backend": snapshot.get("backend", "unknown"),
                    "capture_backend": snapshot.get("capture_backend", snapshot.get("backend", "unknown")),
                    "input_device_name": snapshot.get("input_device_name", "none"),
                    "input_device_index": snapshot.get("input_device_index"),
                    "input_sample_rate": snapshot.get("input_sample_rate"),
                    "input_channels": snapshot.get("input_channels"),
                    "input_dtype": snapshot.get("input_dtype"),
                    "microphone_available": microphone_available,
                    "microphone_error": snapshot.get("microphone_error"),
                }
                event_hub.broadcast({"type": "state", "state": "listening"})
                event_hub.broadcast(status_payload)
                await websocket.send_json(
                    {
                        "type": "command_result",
                        "command": command,
                        "ok": True,
                    },
                )
                continue

            if command == "ptt_stop":
                voice_runtime.stop_listening()
                snapshot = voice_runtime.snapshot()
                if not settings.is_production:
                    LOGGER.info(
                        "PTT stop client=%s reason=ui_toggle state=%s backend=%s",
                        client_host,
                        snapshot.get("state", "idle"),
                        snapshot.get("backend", "unknown"),
                    )
                status_state = str(snapshot.get("state") or "idle")
                if status_state in {"listening", "transcribing"}:
                    status_state = "idle"
                status_payload = {
                    "type": "voice_status",
                    "state": status_state,
                    "backend": snapshot.get("backend", "unknown"),
                    "capture_backend": snapshot.get("capture_backend", snapshot.get("backend", "unknown")),
                    "input_device_name": snapshot.get("input_device_name", "none"),
                    "input_device_index": snapshot.get("input_device_index"),
                    "input_sample_rate": snapshot.get("input_sample_rate"),
                    "input_channels": snapshot.get("input_channels"),
                    "input_dtype": snapshot.get("input_dtype"),
                    "microphone_available": bool(snapshot.get("microphone_available")),
                    "microphone_error": snapshot.get("microphone_error"),
                    "input_level_rms": 0.0,
                    "input_level_peak": 0,
                    "input_level_norm": 0.0,
                }
                if status_state == "idle":
                    event_hub.broadcast({"type": "state", "state": "idle"})
                event_hub.broadcast(status_payload)
                await websocket.send_json(
                    {
                        "type": "command_result",
                        "command": command,
                        "ok": True,
                    },
                )
                continue

            if command == "mic_retry":
                voice_runtime.request_microphone_retry()
                await websocket.send_json(
                    {
                        "type": "command_result",
                        "command": command,
                        "ok": True,
                    },
                )
                continue

            if command == "stop_response":
                stop_payload = _request_response_stop()
                await websocket.send_json(
                    {
                        "type": "command_result",
                        "command": command,
                        **stop_payload,
                    },
                )
                continue

            await websocket.send_json(
                {
                    "type": "command_result",
                    "command": command,
                    "ok": False,
                    "detail": "Unknown command",
                },
            )
    except WebSocketDisconnect:
        snapshot = event_hub.get_ui_state()
        LOGGER.info(
            "Assistant UI WS disconnect from %s active_cycle=%s uiPhase=%s",
            client_host,
            snapshot.get("cycle_id"),
            snapshot.get("uiPhase"),
        )
        return
    finally:
        event_hub.unregister(websocket)


def _ui_post_response_hold_seconds() -> float:
    """Read post-response hold seconds from environment."""
    raw = (
        os.getenv("SE_UI_POST_TTS_HOLD_SECONDS")
        or os.getenv("SE_UI_POST_RESPONSE_HOLD_SECONDS")
        or os.getenv("COOLDOWN_SECONDS")
        or os.getenv("SENTINELEDGE_UI_COOLDOWN_SECONDS")
        or "3"
    )
    try:
        seconds = float(raw)
    except ValueError:
        return 3.0
    return max(seconds, 0.0)


@router.get("/ws/state")
def assistant_ui_ws_state() -> dict[str, object]:
    """Return resilient assistant UI state for websocket reconnect hydration."""
    _require_dev_endpoint()
    return event_hub.get_ui_state()


@router.post("/ui-mode")
def assistant_ui_mode(payload: AssistantUiModeRequest) -> dict[str, object]:
    """Set active app-mode screen and sync paused/listening UI state."""
    previous_mode = runtime_config.get_app_ui_mode()
    mode = runtime_config.set_app_ui_mode(payload.mode)
    LOGGER.info("Assistant UI mode transition previous=%s next=%s", previous_mode, mode)
    if mode == runtime_config.UI_MODE_CONFIG and previous_mode != runtime_config.UI_MODE_CONFIG:
        voice_runtime.stop_listening()
        event_hub.broadcast({"type": "state", "state": "paused"})
    elif mode == runtime_config.UI_MODE_ASSISTANT and previous_mode == runtime_config.UI_MODE_CONFIG:
        _force_assistant_idle(reason="assistant.ui_mode.resume")
    return {
        "ok": True,
        "mode": mode,
        "voice_paused": runtime_config.is_voice_capture_paused(),
    }


@router.get("/ui-mode")
def assistant_ui_mode_state() -> dict[str, object]:
    """Return current app-mode screen and voice pause status."""
    mode = runtime_config.get_app_ui_mode()
    return {
        "mode": mode,
        "voice_paused": runtime_config.is_voice_capture_paused(),
    }


@router.get("/ui/voice-status")
def assistant_ui_voice_status() -> dict[str, object]:
    """Return current voice loop status for Assistant UI controls."""
    snapshot = voice_runtime.snapshot()
    if not snapshot.get("backend") or snapshot.get("backend") == "unknown":
        ui_state = event_hub.get_ui_state()
        snapshot = {
            **snapshot,
            "state": str(ui_state.get("voice_state") or snapshot.get("state") or "idle"),
            "backend": str(ui_state.get("stt_backend") or snapshot.get("backend") or "unknown"),
            "input_device_name": str(ui_state.get("input_device_name") or snapshot.get("input_device_name") or "none"),
            "input_device_index": ui_state.get("input_device_index", snapshot.get("input_device_index")),
            "input_sample_rate": ui_state.get("input_sample_rate", snapshot.get("input_sample_rate")),
            "input_channels": ui_state.get("input_channels", snapshot.get("input_channels")),
            "input_dtype": ui_state.get("input_dtype", snapshot.get("input_dtype")),
            "microphone_available": bool(ui_state.get("microphone_available", snapshot.get("microphone_available"))),
            "microphone_error": ui_state.get("microphone_error", snapshot.get("microphone_error")),
            "capture_backend": str(ui_state.get("capture_backend") or snapshot.get("capture_backend") or snapshot.get("backend") or "unknown"),
            "input_level_rms": ui_state.get("input_level_rms", snapshot.get("input_level_rms")),
            "input_level_peak": ui_state.get("input_level_peak", snapshot.get("input_level_peak")),
            "input_level_norm": ui_state.get("input_level_norm", snapshot.get("input_level_norm")),
        }
    return snapshot


@router.post("/ui/prompt")
def assistant_ui_prompt(payload: AssistantUiPromptRequest) -> dict[str, object]:
    """Queue one typed prompt for voice-loop processing."""
    if not voice_runtime.is_worker_active():
        raise HTTPException(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            detail="runtime not running",
        )
    busy_reason = _assistant_busy_reason()
    if busy_reason:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Assistant busy ({busy_reason})",
        )
    queue_depth = voice_runtime.enqueue_typed_prompt(payload.message)
    return {
        "ok": True,
        "queued": queue_depth,
    }


@router.post("/ui/stop")
def assistant_ui_stop() -> dict[str, object]:
    """Interrupt active assistant response when possible."""
    return _request_response_stop()


@router.post("/ui/listen")
def assistant_ui_listen() -> dict[str, object]:
    """Start push-to-talk listening when microphone is available."""
    snapshot = voice_runtime.snapshot()
    microphone_available = bool(snapshot.get("microphone_available"))
    if not microphone_available:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=str(snapshot.get("microphone_error") or "No microphone detected / input unavailable"),
        )
    busy_reason = _assistant_busy_reason()
    if busy_reason:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=f"Voice busy ({busy_reason})",
        )
    voice_runtime.start_listening()
    event_hub.broadcast({"type": "state", "state": "listening"})
    event_hub.broadcast(
        {
            "type": "voice_status",
            "state": "listening",
            "backend": snapshot.get("backend", "unknown"),
            "capture_backend": snapshot.get("capture_backend", snapshot.get("backend", "unknown")),
            "input_device_name": snapshot.get("input_device_name", "none"),
            "input_device_index": snapshot.get("input_device_index"),
            "input_sample_rate": snapshot.get("input_sample_rate"),
            "input_channels": snapshot.get("input_channels"),
            "input_dtype": snapshot.get("input_dtype"),
            "microphone_available": microphone_available,
            "microphone_error": snapshot.get("microphone_error"),
        },
    )
    return {"ok": True}


@router.post("/ui/microphone/retry")
def assistant_ui_microphone_retry() -> dict[str, object]:
    """Request immediate microphone probe retry in voice loop."""
    voice_runtime.request_microphone_retry()
    return {"ok": True}


@router.post("/ui/startup-phase")
def assistant_ui_startup_phase(payload: AssistantUiStartupPhaseRequest, request: Request) -> dict[str, object]:
    """Record lightweight startup lifecycle telemetry without blocking UI."""
    client = request.client
    client_host = client.host if client else "unknown"
    phase = str(payload.phase or "").strip().lower() or "unknown"
    detail = str(payload.detail or "").strip()
    LOGGER.info(
        "Assistant UI startup phase client=%s phase=%s ts_ms=%s detail=%s",
        client_host,
        phase,
        payload.ts_ms,
        detail,
    )
    return {"ok": True}


@router.post("/render-markdown", response_model=AssistantMarkdownRenderResponse)
def assistant_render_markdown(payload: AssistantMarkdownRenderRequest) -> AssistantMarkdownRenderResponse:
    """Render assistant markdown using server-side parser + sanitizer."""
    safe_html = markdown_renderer.render_markdown(payload.text)
    return AssistantMarkdownRenderResponse(html=safe_html)


@router.post("/query", response_model=AssistantQueryResponse)
def assistant_query(
    payload: AssistantQueryRequest,
    db: Session = Depends(get_db),
) -> AssistantQueryResponse:
    """Run assistant query with intent router."""
    try:
        result = assistant_service.query(db=db, message=payload.message)
    except Exception as exc:  # pragma: no cover - defensive runtime branch
        result = {
            "intent": "GENERAL",
            "response": _localized_route_error("config.messages.assistant_query_failed", exc),
        }

    return AssistantQueryResponse.model_validate(result)


@router.post("/chat", response_model=AssistantChatResponse)
def assistant_chat(payload: AssistantChatRequest) -> AssistantChatResponse:
    """Run free conversation mode backed by selected AI provider."""
    try:
        result = assistant_service.chat(
            message=payload.message,
            session_id=payload.session_id,
        )
    except Exception as exc:  # pragma: no cover - defensive runtime branch
        result = {
            "session_id": payload.session_id or "fallback-session",
            "response": _localized_route_error("config.messages.assistant_chat_failed", exc),
        }

    return AssistantChatResponse.model_validate(result)
