"""Chat-facing text-to-speech synthesis.

This is the lightweight audio-output path for the chat UI's "play this reply"
button. Distinct from `agntspace.video.tts` (which targets video voice-over
and is half-built); we share only the pure :func:`sanitize_for_tts` helper.

Provider: edge-tts (Microsoft Edge browser endpoint — free, no API key).
Falls back to a graceful 503 when the lib is missing rather than crashing
server boot or breaking unrelated routes.
"""

from __future__ import annotations

import asyncio
import logging

from agntspace.video.tts import sanitize_for_tts

log = logging.getLogger(__name__)

# Synthesis hard cap. edge-tts can handle far more, but ~4 KB of text is
# already ~30 seconds of audio — anything bigger is almost always a
# pasted document, not a chat reply, and we'd rather refuse fast than
# burn time generating audio nobody listens to all the way through.
MAX_TEXT_CHARS = 4000

# 25 second per-call timeout — typical replies generate in <2s; anything
# beyond 25s indicates the upstream is wedged and we should fail fast so
# the UI can surface the error.
SYNTHESIS_TIMEOUT_SECONDS = 25.0

# Default voice — friendly, neutral US English. Users can override via the
# ``voice`` field on the request; full list at edge-tts's catalog.
DEFAULT_VOICE = "en-US-AriaNeural"


class TTSUnavailable(RuntimeError):
    """Raised when no TTS provider can satisfy the request.

    The route handler maps this to a 503 with a concrete remediation hint
    (e.g. ``pip install edge-tts``).
    """

    def __init__(self, message: str, *, install_hint: str = "") -> None:
        super().__init__(message)
        self.install_hint = install_hint


async def synthesize(text: str, *, voice: str = DEFAULT_VOICE) -> bytes:
    """Synthesize ``text`` to MP3 bytes using the first available provider.

    Returns the full MP3 payload as bytes (callers can stream it). Raises
    :class:`TTSUnavailable` when no provider is installed or the upstream
    refuses the request. Sanitization (em-dash, AGNT, AI → A.I., …) is
    applied before the provider sees the text.
    """
    cleaned = sanitize_for_tts(text or "").strip()
    if not cleaned:
        raise TTSUnavailable("Empty text after sanitization")
    if len(cleaned) > MAX_TEXT_CHARS:
        cleaned = cleaned[:MAX_TEXT_CHARS]
    try:
        return await asyncio.wait_for(
            _try_edge_tts(cleaned, voice=voice),
            timeout=SYNTHESIS_TIMEOUT_SECONDS,
        )
    except TimeoutError as exc:
        raise TTSUnavailable("TTS synthesis timed out") from exc


async def _try_edge_tts(text: str, *, voice: str) -> bytes:
    """Synthesize via the Microsoft Edge browser TTS endpoint.

    Lazy-imports ``edge_tts`` so the module loads cleanly when the lib is
    missing — the actual TTSUnavailable surfaces only when synthesis is
    requested, not at server boot.
    """
    try:
        import edge_tts  # type: ignore[import-not-found]
    except ImportError as exc:
        raise TTSUnavailable(
            "TTS unavailable — edge-tts is not installed",
            install_hint="pip install edge-tts",
        ) from exc

    try:
        communicate = edge_tts.Communicate(text=text, voice=voice)
        chunks: list[bytes] = []
        async for chunk in communicate.stream():
            if chunk.get("type") == "audio":
                data = chunk.get("data")
                if isinstance(data, bytes) and data:
                    chunks.append(data)
        if not chunks:
            raise TTSUnavailable("edge-tts returned no audio data")
        return b"".join(chunks)
    except TTSUnavailable:
        raise
    except Exception as exc:
        # edge-tts surfaces a variety of errors (no internet, voice not
        # found, rate-limited). Map them all to TTSUnavailable so the
        # route handler returns a single shape — "TTS failed: <reason>"
        # — that the UI can render as a single error path.
        raise TTSUnavailable(f"edge-tts synthesis failed: {exc}") from exc
