"""TTS and HTTP duplex cancel routes extracted from [mugi_backend._build_app](mugi_backend.py)."""
from __future__ import annotations

import asyncio
import json
import os
import shutil
from collections.abc import Callable
from typing import Any

from fastapi import Body, Request as FRequest
from fastapi.responses import JSONResponse, Response, StreamingResponse

from run_context import sanitize_run_id


def register_voice_tts_routes(
    fastapi_app: Any,
    *,
    load_app_prefs: Callable[[], dict[str, Any]],
    delegate: Any,
    duplex_sessions: Any,
    duplex_metrics: Any,
) -> None:
    """Register /tts/* and POST /duplex/cancel."""

    @fastapi_app.post("/tts/speak")
    async def tts_speak(request: FRequest):
        """Speak text using voice_output (Apple or Supertonic per prefs). Used by Swift app for speak-agent-replies."""
        try:
            body = await request.json()
        except Exception:
            body = {}
        text = (body.get("text") or "").strip()
        if not text:
            return JSONResponse(content={"ok": True})
        prefs = load_app_prefs()
        if not prefs.get("speak_agent_replies"):
            return JSONResponse(content={"ok": True})
        try:
            import voice_output
        except Exception as exc:
            return JSONResponse(content={"ok": False, "error": f"voice_output not available: {exc}"}, status_code=500)
        tts_engine = str(prefs.get("tts_engine") or "").strip().lower()
        if tts_engine not in ("apple", "supertonic", "chatterbox"):
            tts_engine = "supertonic" if bool(prefs.get("use_supertonic_tts", False)) else "apple"
        voice_id = (prefs.get("tts_voice_identifier") or "").strip() or None
        use_supertonic = tts_engine == "supertonic"
        use_chatterbox = tts_engine == "chatterbox"
        chatterbox_voice = (prefs.get("chatterbox_voice") or "").strip()
        chatterbox_variant = (prefs.get("chatterbox_variant") or "turbo").strip().lower() or "turbo"
        chatterbox_language = (prefs.get("chatterbox_language_id") or "en").strip() or "en"
        chatterbox_allow_tags = bool(prefs.get("chatterbox_allow_tags", False))
        if use_chatterbox and not chatterbox_voice:
            return JSONResponse(content={"ok": False, "error": "No Chatterbox voice selected"}, status_code=400)
        supertonic_voice = (prefs.get("supertonic_voice") or "M1").strip() or "M1"
        supertonic_speed = float(prefs.get("supertonic_speed") if prefs.get("supertonic_speed") is not None else 1.05)
        supertonic_steps = int(prefs.get("supertonic_total_steps") if prefs.get("supertonic_total_steps") is not None else 5)
        supertonic_lang = (prefs.get("supertonic_lang") or "en").strip() or "en"

        def _do_speak():
            return voice_output.speak(
                text,
                voice_identifier=voice_id,
                use_supertonic_tts=use_supertonic,
                use_chatterbox_tts=use_chatterbox,
                chatterbox_voice=chatterbox_voice,
                chatterbox_variant=chatterbox_variant,
                chatterbox_language_id=chatterbox_language,
                chatterbox_allow_tags=chatterbox_allow_tags,
                supertonic_voice=supertonic_voice,
                supertonic_speed=supertonic_speed,
                supertonic_total_steps=supertonic_steps,
                supertonic_lang=supertonic_lang,
            )

        loop = asyncio.get_event_loop()
        try:
            fallback_reason = await loop.run_in_executor(None, _do_speak)
        except Exception as e:
            return JSONResponse(content={"ok": False, "error": str(e)}, status_code=500)
        if use_chatterbox and fallback_reason == "chatterbox_unavailable":
            return JSONResponse(content={"ok": False, "error": "Chatterbox is not installed"}, status_code=503)
        return JSONResponse(content={"ok": True})

    @fastapi_app.post("/tts/synthesize")
    async def tts_synthesize(body: dict = Body(default=None)):
        """Synthesize text to WAV (Supertonic or Chatterbox, no playback). Returns audio/wav for Swift to play."""
        body = body or {}
        text = (body.get("text") or "").strip()
        if not text:
            return JSONResponse(content={"error": "text required"}, status_code=400)
        try:
            import voice_output
        except Exception as exc:
            return JSONResponse(content={"error": f"voice_output not available: {exc}"}, status_code=500)
        prefs = load_app_prefs()
        engine = str(body.get("tts_engine") or prefs.get("tts_engine") or "").strip().lower()
        if engine not in ("apple", "supertonic", "chatterbox"):
            engine = "supertonic" if bool(prefs.get("use_supertonic_tts", False)) else "apple"
        if engine == "chatterbox":
            voice = (body.get("voice") or body.get("chatterbox_voice") or prefs.get("chatterbox_voice") or "").strip()
            if not voice:
                return JSONResponse(content={"error": "Chatterbox voice is required"}, status_code=400)
            variant = (body.get("variant") or body.get("chatterbox_variant") or prefs.get("chatterbox_variant") or "turbo").strip().lower() or "turbo"
            lang = (body.get("language_id") or body.get("chatterbox_language_id") or prefs.get("chatterbox_language_id") or "en").strip() or "en"
            allow_tags = bool(body.get("allow_tags", body.get("chatterbox_allow_tags", prefs.get("chatterbox_allow_tags", False))))

            def _chatterbox_synth():
                return voice_output.synthesize_chatterbox_to_wav_bytes_with_error(
                    text,
                    voice_profile_id=voice,
                    variant=variant,
                    language_id=lang,
                    allow_tags=allow_tags,
                    worker_timeout_s=850.0,
                )

            loop = asyncio.get_event_loop()
            try:
                wav_bytes, err_detail = await loop.run_in_executor(None, _chatterbox_synth)
            except Exception as exc:
                return JSONResponse(
                    content={"error": f"Chatterbox synthesis error: {exc}"},
                    status_code=500,
                )
            if err_detail:
                err_l = err_detail.lower()
                if "not available" in err_l and "software" in err_l:
                    code = 503
                elif "missing" in err_l and "re-import" in err_l:
                    code = 400
                elif "timed out" in err_l:
                    code = 504
                elif "no text to synthesize" in err_l:
                    code = 400
                else:
                    code = 502
                return JSONResponse(content={"error": err_detail}, status_code=code)
            if not wav_bytes:
                return JSONResponse(
                    content={"error": "Chatterbox produced no audio"},
                    status_code=502,
                )
            return Response(content=wav_bytes, media_type="audio/wav")
        if not voice_output.supertonic_available():
            return JSONResponse(content={"error": "Supertonic not installed"}, status_code=503)
        voice = (body.get("voice") or body.get("supertonic_voice") or "M1").strip() or "M1"
        lang = (body.get("lang") or body.get("supertonic_lang") or "en").strip() or "en"
        speed = float(body.get("speed") if body.get("speed") is not None else body.get("supertonic_speed", 1.05))
        steps = int(body.get("steps") if body.get("steps") is not None else body.get("supertonic_total_steps", 5))
        speed = max(0.7, min(2.0, speed))
        steps = max(2, min(15, steps))
        wav_bytes = await asyncio.get_event_loop().run_in_executor(
            None,
            lambda: voice_output.synthesize_supertonic_to_wav_bytes(text, voice_name=voice, lang=lang, speed=speed, total_steps=steps),
        )
        if not wav_bytes:
            return JSONResponse(content={"error": "synthesis failed"}, status_code=500)
        return Response(content=wav_bytes, media_type="audio/wav")

    @fastapi_app.post("/tts/stream")
    async def tts_stream(body: dict = Body(default=None)):
        body = body or {}
        text = str(body.get("text") or "").strip()
        if not text:
            return JSONResponse(content={"error": "text required"}, status_code=400)
        chunk_size = int(body.get("chunk_size") or 4096)
        chunk_size = max(1024, min(64 * 1024, chunk_size))

        prefs = load_app_prefs()
        engine = str(body.get("tts_engine") or prefs.get("tts_engine") or "").strip().lower()
        if engine not in ("apple", "supertonic", "chatterbox"):
            engine = "supertonic" if bool(prefs.get("use_supertonic_tts", False)) else "apple"
        if engine == "apple":
            return JSONResponse(content={"error": "apple engine does not support streamed synthesis"}, status_code=400)
        try:
            import voice_output
        except Exception as exc:
            return JSONResponse(content={"error": f"voice_output not available: {exc}"}, status_code=500)

        thread_id = str(body.get("thread_id") or "").strip()
        run_id = sanitize_run_id(str(body.get("run_id") or ""))
        if thread_id:
            try:
                delegate._structured_bus.publish(
                    thread_id,
                    json.dumps(
                        {
                            "type": "duplex_agent_state",
                            "state": "speaking",
                            "thread_id": thread_id,
                            "run_id": run_id,
                        }
                    ),
                )
            except Exception:
                pass

        if engine == "chatterbox":
            voice = (body.get("voice") or body.get("chatterbox_voice") or prefs.get("chatterbox_voice") or "").strip()
            if not voice:
                return JSONResponse(content={"error": "Chatterbox voice is required"}, status_code=400)
            variant = (body.get("variant") or body.get("chatterbox_variant") or prefs.get("chatterbox_variant") or "turbo").strip().lower() or "turbo"
            lang = (body.get("language_id") or body.get("chatterbox_language_id") or prefs.get("chatterbox_language_id") or "en").strip() or "en"
            allow_tags = bool(body.get("allow_tags", body.get("chatterbox_allow_tags", prefs.get("chatterbox_allow_tags", False))))
            chunks = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: list(
                    voice_output.stream_chatterbox_wav_chunks(
                        text,
                        voice_profile_id=voice,
                        variant=variant,
                        language_id=lang,
                        allow_tags=allow_tags,
                        chunk_size=chunk_size,
                    )
                ),
            )
        else:
            voice = (body.get("voice") or body.get("supertonic_voice") or "M1").strip() or "M1"
            lang = (body.get("lang") or body.get("supertonic_lang") or "en").strip() or "en"
            speed = float(body.get("speed") if body.get("speed") is not None else body.get("supertonic_speed", 1.05))
            steps = int(body.get("steps") if body.get("steps") is not None else body.get("supertonic_total_steps", 5))
            speed = max(0.7, min(2.0, speed))
            steps = max(2, min(15, steps))
            chunks = await asyncio.get_event_loop().run_in_executor(
                None,
                lambda: list(
                    voice_output.stream_supertonic_wav_chunks(
                        text,
                        voice_name=voice,
                        lang=lang,
                        speed=speed,
                        total_steps=steps,
                        chunk_size=chunk_size,
                    )
                ),
            )

        if not chunks:
            return JSONResponse(content={"error": "synthesis failed"}, status_code=500)

        async def _wav_chunks():
            try:
                for chunk in chunks:
                    yield chunk
            finally:
                if thread_id:
                    try:
                        delegate._structured_bus.publish(
                            thread_id,
                            json.dumps(
                                {
                                    "type": "duplex_agent_state",
                                    "state": "listening",
                                    "thread_id": thread_id,
                                    "run_id": run_id,
                                }
                            ),
                        )
                    except Exception:
                        pass

        return StreamingResponse(_wav_chunks(), media_type="audio/wav")

    @fastapi_app.post("/duplex/cancel")
    async def duplex_cancel(request: FRequest):
        try:
            body = await request.json()
        except Exception:
            body = {}
        thread_id = str(body.get("thread_id") or "").strip() or delegate._current_thread_id
        run_id = sanitize_run_id(str(body.get("run_id") or ""))
        session_id = str(body.get("session_id") or "").strip()
        if session_id:
            sess = duplex_sessions.get(session_id)
            if sess is not None and sess.thread_id != thread_id:
                return JSONResponse(content={"ok": False, "error": "session_thread_mismatch"}, status_code=409)
        workers = getattr(delegate, "_workers", {}) or {}
        worker = workers.get(thread_id) if hasattr(workers, "get") else None
        if worker is None:
            return JSONResponse(content={"ok": False, "error": "no_active_run"}, status_code=404)
        active_run = sanitize_run_id(str(getattr(worker, "run_id", "") or ""))
        if run_id and run_id != active_run:
            return JSONResponse(
                content={
                    "ok": False,
                    "error": "stale_run_id",
                    "active_run_id": active_run,
                },
                status_code=409,
            )
        duplex_metrics["interruptions"] += 1
        if hasattr(delegate, "onStop_"):
            delegate.onStop_(thread_id=thread_id)
        if session_id:
            duplex_sessions.end_session(session_id)
        try:
            delegate._structured_bus.publish(
                thread_id,
                json.dumps(
                    {
                        "type": "duplex_interrupted",
                        "thread_id": thread_id,
                        "run_id": active_run,
                        "session_id": session_id or "",
                    }
                ),
            )
        except Exception:
            pass
        return JSONResponse(content={"ok": True, "thread_id": thread_id, "run_id": active_run, "session_id": session_id or None})

    @fastapi_app.post("/tts/import_voice")
    async def tts_import_voice(request: FRequest):
        """Import a custom Supertonic voice JSON file into ~/.mugi/supertonic_voices/."""
        try:
            import voice_output
        except Exception as exc:
            return JSONResponse(content={"ok": False, "message": f"voice_output not available: {exc}"}, status_code=500)
        try:
            body = await request.json()
        except Exception:
            body = {}
        src_path = (body.get("path") or "").strip()
        if not src_path:
            return JSONResponse(content={"ok": False, "message": "File not found"}, status_code=400)
        src_abs = os.path.abspath(os.path.expanduser(src_path))
        if not os.path.isfile(src_abs):
            return JSONResponse(content={"ok": False, "message": "File not found"}, status_code=400)
        if os.path.getsize(src_abs) > 5 * 1024 * 1024:
            return JSONResponse(content={"ok": False, "message": "Voice file is too large (max 5MB)"}, status_code=400)
        if not src_abs.endswith(".json"):
            return JSONResponse(content={"ok": False, "message": "Only .json voice files are supported"}, status_code=400)
        dest_dir = voice_output.get_supertonic_custom_voices_dir()
        os.makedirs(dest_dir, exist_ok=True)
        filename = os.path.basename(src_abs)
        if ".." in filename or "/" in filename or "\\" in filename:
            return JSONResponse(content={"ok": False, "message": "Invalid file name"}, status_code=400)
        dest_path = os.path.join(dest_dir, filename)
        try:
            shutil.copy2(src_abs, dest_path)
        except Exception as e:
            return JSONResponse(content={"ok": False, "message": str(e)}, status_code=500)
        voice_id = "custom:" + filename
        builtin = list(voice_output.SUPERTONIC_BUILTIN_VOICES)
        custom = [vid for vid, _ in voice_output.list_supertonic_custom_voices()]
        return JSONResponse(content={
            "ok": True,
            "voice_id": voice_id,
            "supertonic_voices": builtin + custom,
        })

    @fastapi_app.post("/tts/chatterbox/import_voice")
    async def tts_import_chatterbox_voice(request: FRequest):
        try:
            import voice_output
        except Exception as exc:
            return JSONResponse(content={"ok": False, "message": f"voice_output not available: {exc}"}, status_code=500)
        try:
            body = await request.json()
        except Exception:
            body = {}
        source_path = str(body.get("path") or "").strip()
        display_name = str(body.get("display_name") or "").strip()
        ok, voice_id, message = await asyncio.get_event_loop().run_in_executor(
            None,
            lambda: voice_output.import_chatterbox_voice_file(source_path, display_name=display_name),
        )
        if not ok:
            return JSONResponse(content={"ok": False, "message": message or "Import failed"}, status_code=400)
        voices = [{"id": vid, "name": name} for vid, name in voice_output.list_chatterbox_voices()]
        payload = {"ok": True, "voice_id": voice_id, "chatterbox_voices": voices}
        if message:
            payload["warning"] = message
        return JSONResponse(content=payload)
