import asyncio
import logging
import threading
import time

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from ..config.settings import settings
from .health import router as health_router
from .session_router import router as session_router
from .twilio_webhook import router as twilio_router
from .test_tts import router as test_tts_router
from .livekit_token import router as livekit_token_router

logger = logging.getLogger(__name__)

app = FastAPI(
    title="IT0 Voice Service",
    description="Real-time voice dialogue engine powered by Pipecat",
    version="0.1.0",
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(health_router, tags=["health"])
app.include_router(session_router, prefix="/api/v1/voice", tags=["sessions"])
app.include_router(twilio_router, prefix="/api/v1/twilio", tags=["twilio"])
app.include_router(test_tts_router, prefix="/api/v1/test", tags=["test"])
app.include_router(livekit_token_router, prefix="/api/v1/voice", tags=["livekit"])


# ---------------------------------------------------------------------------
# Top-level WebSocket route for Kong's voice-ws route (/ws/voice/*)
# ---------------------------------------------------------------------------
# Kong forwards the full path /ws/voice/{session_id} without JWT.
# We import the handler from session_router and re-expose it here so the
# path matches exactly.
from fastapi import WebSocket as _WS  # noqa: E402

@app.websocket("/ws/voice/{session_id}")
async def voice_ws_proxy(websocket: _WS, session_id: str):
    """Proxy route that delegates to the session_router WebSocket handler."""
    from .session_router import voice_websocket
    await voice_websocket(websocket, session_id)


async def _session_cleanup_loop():
    """Periodically remove sessions that have been disconnected longer than session_ttl.

    Runs every 30 seconds.  Sessions in "disconnected" state whose
    ``disconnected_at`` timestamp is older than ``settings.session_ttl``
    are removed from ``app.state.sessions``.
    """
    ttl = settings.session_ttl
    while True:
        await asyncio.sleep(30)
        try:
            sessions: dict = getattr(app.state, "sessions", {})
            now = time.time()
            expired = [
                sid
                for sid, s in sessions.items()
                if s.get("status") == "disconnected"
                and (now - s.get("disconnected_at", 0)) > ttl
            ]
            for sid in expired:
                sessions.pop(sid, None)
                logger.info("Cleaned up expired session %s", sid)
        except Exception:
            logger.exception("Error in session cleanup loop")


def _load_models_sync():
    """Load ML models in a background thread (all blocking calls)."""
    from ..config.settings import settings

    def _p(msg):
        print(msg, flush=True)

    _p(f"[bg] Loading models (stt={settings.stt_provider}, tts={settings.tts_provider}, device={settings.device})...")

    # OpenAI client (shared by STT and TTS if either uses OpenAI)
    if settings.stt_provider == "openai" or settings.tts_provider == "openai":
        try:
            from openai import OpenAI
            import httpx as _httpx

            kwargs = {"api_key": settings.openai_api_key}
            if settings.openai_base_url:
                kwargs["base_url"] = settings.openai_base_url
                kwargs["http_client"] = _httpx.Client(verify=False)
            app.state.openai_client = OpenAI(**kwargs)
            _p(f"[bg] OpenAI client initialized (stt_model={settings.openai_stt_model}, tts_model={settings.openai_tts_model})")
        except Exception as e:
            app.state.openai_client = None
            _p(f"[bg] WARNING: OpenAI client init failed: {e}")
    else:
        app.state.openai_client = None

    # STT — only load local model if provider is "local"
    if settings.stt_provider == "local":
        try:
            from ..stt.whisper_service import WhisperSTTService
            from faster_whisper import WhisperModel

            stt = WhisperSTTService(
                model=settings.whisper_model,
                device=settings.device,
                language=settings.whisper_language,
            )
            compute_type = "float16" if settings.device == "cuda" else "int8"
            try:
                stt._model = WhisperModel(stt.model_name, device=stt.device, compute_type=compute_type)
            except Exception as e:
                _p(f"[bg] Whisper fallback to CPU: {e}")
                if stt.device != "cpu":
                    stt._model = WhisperModel(stt.model_name, device="cpu", compute_type="int8")
            app.state.stt = stt
            _p(f"[bg] STT loaded: {settings.whisper_model}")
        except Exception as e:
            app.state.stt = None
            _p(f"[bg] WARNING: STT failed: {e}")
    else:
        app.state.stt = None
        _p(f"[bg] STT: using OpenAI ({settings.openai_stt_model})")

    # TTS — only load local model if provider is "local"
    if settings.tts_provider == "local":
        try:
            from ..tts.kokoro_service import KokoroTTSService, _patch_misaki_compat

            _patch_misaki_compat()
            from kokoro import KPipeline

            tts = KokoroTTSService(model=settings.kokoro_model, voice=settings.kokoro_voice)
            tts._pipeline = KPipeline(lang_code='z')
            app.state.tts = tts
            _p(f"[bg] TTS loaded: {settings.kokoro_model} voice={settings.kokoro_voice}")
        except Exception as e:
            app.state.tts = None
            _p(f"[bg] WARNING: TTS failed: {e}")
    else:
        app.state.tts = None
        _p(f"[bg] TTS: using OpenAI ({settings.openai_tts_model}, voice={settings.openai_tts_voice})")

    # VAD — always load (needed for speech detection regardless of provider)
    try:
        from ..vad.silero_service import SileroVADService
        import torch

        vad = SileroVADService()
        vad._model, vad._utils = torch.hub.load(
            repo_or_dir='snakers4/silero-vad', model='silero_vad', force_reload=False
        )
        app.state.vad = vad
        _p("[bg] VAD loaded: Silero VAD")
    except Exception as e:
        app.state.vad = None
        _p(f"[bg] WARNING: VAD failed: {e}")

    _p("[bg] Model loading complete.")


@app.on_event("startup")
async def startup():
    """Start server immediately, load models in background thread."""
    print("Voice service starting up...", flush=True)

    app.state.stt = None
    app.state.tts = None
    app.state.vad = None
    app.state.sessions = {}

    # Load models in background thread so server responds to healthchecks immediately
    thread = threading.Thread(target=_load_models_sync, daemon=True)
    thread.start()

    # Start background session cleanup task
    app.state._cleanup_task = asyncio.create_task(_session_cleanup_loop())
    logger.info(
        "Session cleanup task started (ttl=%ds, check every 30s)",
        settings.session_ttl,
    )

    print("Voice service ready (models loading in background).", flush=True)


@app.on_event("shutdown")
async def shutdown():
    """Cleanup on shutdown."""
    print("Voice service shutting down...", flush=True)

    # Cancel session cleanup task
    cleanup_task = getattr(app.state, "_cleanup_task", None)
    if cleanup_task is not None and not cleanup_task.done():
        cleanup_task.cancel()
        try:
            await cleanup_task
        except (asyncio.CancelledError, Exception):
            pass
