#!/usr/bin/env python3
"""Minimal local HTTP bridge for MisoLabsAI/MisoTTS.

Run this outside the Next.js process and point Madarx at it:

  git clone https://github.com/MisoLabsAI/MisoTTS.git ~/MisoTTS
  cd ~/MisoTTS
  uv sync --python 3.10
  MISO_TTS_REPO_DIR=$PWD uv run python /path/to/madarx-os/scripts/misotts-local-server.py
  MADARX_LOCAL_TTS_URL=http://127.0.0.1:8799/tts MADARX_VOICE_DEFAULT_PROVIDER=local npm run dev

POST /tts with JSON {"text":"...","voice":"0","format":"wav"} returns audio/wav.
The model is loaded lazily on first request and cached in-process.
"""

from __future__ import annotations

import json
import os
import sys
import tempfile
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from typing import Any

# MisoTTS is intentionally not vendored into Madarx. Point at a local clone or
# install it into this Python environment with `pip install -e /path/to/MisoTTS`.
_repo_dir = os.environ.get("MISO_TTS_REPO_DIR")
if _repo_dir:
    sys.path.insert(0, _repo_dir)

os.environ.setdefault("HF_HUB_ETAG_TIMEOUT", "60")
os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", "60")
os.environ.setdefault("NO_TORCH_COMPILE", "1")

_generator: Any | None = None
_torch: Any | None = None
_torchaudio: Any | None = None
_segment_cls: Any | None = None


def _load_generator():
    global _generator, _torch, _torchaudio, _segment_cls
    if _generator is not None:
        return _generator

    import torch  # type: ignore
    import torchaudio  # type: ignore
    from generator import DEFAULT_MISO_TTS_REPO_ID, load_miso_8b, Segment  # type: ignore

    device = os.environ.get("MISO_TTS_DEVICE") or ("cuda" if torch.cuda.is_available() else "cpu")
    model_source = os.environ.get("MISO_TTS_8B_MODEL", DEFAULT_MISO_TTS_REPO_ID)
    _generator = load_miso_8b(device=device, model_path_or_repo_id=model_source)
    _torch = torch
    _torchaudio = torchaudio
    _segment_cls = Segment
    return _generator


def _speaker_id(raw: Any) -> int:
    if raw is None or raw == "":
        return 0
    if isinstance(raw, int):
        return raw
    value = str(raw).strip().lower()
    if value.startswith("speaker-"):
        value = value.split("-", 1)[1]
    try:
        return int(value)
    except ValueError:
        return 0


class Handler(BaseHTTPRequestHandler):
    server_version = "MadarxMisoTTS/0.1"

    def _json(self, status: int, payload: dict[str, Any]) -> None:
        data = json.dumps(payload).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def do_GET(self) -> None:  # noqa: N802 - stdlib handler name
        if self.path.rstrip("/") in ("", "/health"):
            self._json(200, {"ok": True, "provider": "misotts", "loaded": _generator is not None})
            return
        self._json(404, {"error": "not_found"})

    def do_POST(self) -> None:  # noqa: N802 - stdlib handler name
        if self.path.rstrip("/") != "/tts":
            self._json(404, {"error": "not_found"})
            return
        try:
            length = int(self.headers.get("Content-Length", "0"))
            body = json.loads(self.rfile.read(length).decode("utf-8"))
        except Exception as exc:  # pragma: no cover - defensive request parsing
            self._json(400, {"error": f"invalid_json: {exc}"})
            return

        text = str(body.get("text") or "").strip()
        if not text:
            self._json(400, {"error": "text is required"})
            return
        if len(text) > int(os.environ.get("MISO_TTS_MAX_CHARS", "800")):
            self._json(413, {"error": "text too long for local MisoTTS bridge"})
            return

        try:
            generator = _load_generator()
            if generator is None:
                raise RuntimeError("MisoTTS generator did not initialize")
            torchaudio = _torchaudio
            if torchaudio is None:
                raise RuntimeError("torchaudio did not initialize")
            audio = generator.generate(
                text=text,
                speaker=_speaker_id(body.get("voice")),
                context=[],
                max_audio_length_ms=int(body.get("max_audio_length_ms") or os.environ.get("MISO_TTS_MAX_AUDIO_MS", "10000")),
            )
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
                tmp_path = Path(tmp.name)
            try:
                torchaudio.save(str(tmp_path), audio.unsqueeze(0).cpu(), generator.sample_rate)
                data = tmp_path.read_bytes()
            finally:
                tmp_path.unlink(missing_ok=True)
        except Exception as exc:
            self._json(500, {"error": f"misotts_generation_failed: {exc}"})
            return

        self.send_response(200)
        self.send_header("Content-Type", "audio/wav")
        self.send_header("Content-Length", str(len(data)))
        self.send_header("X-Provider", "misotts")
        self.end_headers()
        self.wfile.write(data)


def main() -> None:
    host = os.environ.get("MISO_TTS_HOST", "127.0.0.1")
    port = int(os.environ.get("MISO_TTS_PORT", "8799"))
    print(f"MisoTTS local bridge listening on http://{host}:{port}/tts", flush=True)
    ThreadingHTTPServer((host, port), Handler).serve_forever()


if __name__ == "__main__":
    main()
