"""OpenAI TTS provider."""

from __future__ import annotations

import logging
import os
import re
import tempfile
from pathlib import Path
from typing import Any

import openai

from langlearn_tts.types import HealthCheck, SynthesisRequest, SynthesisResult

logger = logging.getLogger(__name__)

__all__ = ["OpenAIProvider"]

# Maximum characters per OpenAI TTS API request.
_MAX_CHARS = 4096

# Static voice list for tts-1 / tts-1-hd models.
VOICES: dict[str, str] = {
    "alloy": "alloy",
    "ash": "ash",
    "coral": "coral",
    "echo": "echo",
    "fable": "fable",
    "onyx": "onyx",
    "nova": "nova",
    "sage": "sage",
    "shimmer": "shimmer",
}

# Sentence-ending punctuation followed by a space.
_SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")


def _split_text(text: str, max_chars: int = _MAX_CHARS) -> list[str]:
    """Split text into chunks that fit within the character limit.

    Every returned chunk is guaranteed ``<= max_chars``.

    Strategy:
    1. If the text fits, return it as-is.
    2. Split at sentence boundaries (after . ! ? followed by whitespace).
    3. Accumulate sentences into chunks up to max_chars.
    4. If a single sentence exceeds max_chars, split at word boundaries.

    Note: whitespace between sentences is normalized to a single space
    when two sentences are accumulated into the same chunk. This is
    intentional — TTS engines treat all whitespace identically.
    """
    if len(text) <= max_chars:
        return [text]

    sentences = _SENTENCE_SPLIT_RE.split(text)
    chunks: list[str] = []
    current = ""

    for sentence in sentences:
        if not sentence:
            continue

        # If a single sentence exceeds the limit, split at word boundaries.
        if len(sentence) > max_chars:
            # Flush current buffer first.
            if current:
                chunks.append(current)
                current = ""
            chunks.extend(_split_at_words(sentence, max_chars))
            continue

        candidate = f"{current} {sentence}" if current else sentence
        if len(candidate) <= max_chars:
            current = candidate
        else:
            chunks.append(current)
            current = sentence

    if current:
        chunks.append(current)

    return chunks


def _split_at_words(text: str, max_chars: int) -> list[str]:
    """Split text at word boundaries to fit within max_chars.

    Words exceeding max_chars are split into fixed-size character chunks
    so every returned piece is guaranteed ``<= max_chars``.
    """
    words = text.split()
    chunks: list[str] = []
    current = ""

    for word in words:
        if len(word) > max_chars:
            if current:
                chunks.append(current)
                current = ""
            for i in range(0, len(word), max_chars):
                chunks.append(word[i : i + max_chars])
            continue

        candidate = f"{current} {word}" if current else word
        if len(candidate) <= max_chars:
            current = candidate
        else:
            if current:
                chunks.append(current)
            current = word

    if current:
        chunks.append(current)

    return chunks


class OpenAIProvider:
    """OpenAI TTS provider.

    Implements the TTSProvider protocol using the OpenAI audio API.
    Supports tts-1 and tts-1-hd models with 9 built-in voices.
    """

    def __init__(
        self,
        *,
        model: str | None = None,
        client: openai.OpenAI | None = None,
    ) -> None:
        self._model = model or os.environ.get("LANGLEARN_TTS_MODEL") or "tts-1"
        self._client = client or openai.OpenAI()

    @property
    def name(self) -> str:
        return "openai"

    def synthesize(
        self, request: SynthesisRequest, output_path: Path
    ) -> SynthesisResult:
        """Synthesize text to an MP3 file using OpenAI TTS."""
        voice = self._resolve_voice_name(request.voice)
        speed = self._rate_to_speed(request.rate)

        if len(request.text) > _MAX_CHARS:
            self._chunked_synthesize(request, output_path, voice, speed)
        else:
            self._single_synthesize(request.text, output_path, voice, speed)

        logger.info("Wrote %s", output_path)
        return SynthesisResult(
            file_path=output_path,
            text=request.text,
            voice_name=voice,
        )

    def resolve_voice(self, name: str) -> str:
        """Validate and resolve a voice name to its canonical form."""
        return self._resolve_voice_name(name)

    def check_health(self) -> list[HealthCheck]:
        """Check OpenAI API key and model access."""
        checks: list[HealthCheck] = []

        # Check OPENAI_API_KEY env var.
        api_key = os.environ.get("OPENAI_API_KEY")
        if not api_key:
            checks.append(
                HealthCheck(
                    passed=False,
                    message="OpenAI API key: not set (export OPENAI_API_KEY)",
                )
            )
            return checks

        checks.append(HealthCheck(passed=True, message="OpenAI API key: set"))

        # Verify model access.
        try:
            self._client.models.retrieve(self._model)
            checks.append(
                HealthCheck(
                    passed=True,
                    message=f"OpenAI model access: {self._model}",
                )
            )
        except openai.AuthenticationError:
            checks.append(
                HealthCheck(
                    passed=False,
                    message="OpenAI model access: invalid API key",
                )
            )
        except openai.NotFoundError:
            checks.append(
                HealthCheck(
                    passed=False,
                    message=f"OpenAI model access: model '{self._model}' not found",
                )
            )
        except openai.APIConnectionError:
            checks.append(
                HealthCheck(
                    passed=False,
                    message="OpenAI model access: cannot reach API (check network)",
                )
            )

        return checks

    # -- Private helpers --------------------------------------------------

    def _resolve_voice_name(self, name: str) -> str:
        """Resolve a voice name, case-insensitive."""
        key = name.lower()
        if key in VOICES:
            return VOICES[key]
        available = ", ".join(sorted(VOICES))
        raise ValueError(f"Unknown voice '{name}'. Available: {available}")

    @staticmethod
    def _rate_to_speed(rate: int) -> float:
        """Convert percentage rate to OpenAI speed parameter.

        rate=100 → speed=1.0, rate=90 → speed=0.9.
        Clamped to OpenAI's [0.25, 4.0] range.
        """
        return max(0.25, min(4.0, rate / 100))

    def _single_synthesize(
        self,
        text: str,
        output_path: Path,
        voice: str,
        speed: float,
    ) -> None:
        """Synthesize a single chunk to a file."""
        output_path.parent.mkdir(parents=True, exist_ok=True)
        response: Any = self._client.audio.speech.create(  # pyright: ignore[reportUnknownMemberType]
            model=self._model,
            voice=voice,  # pyright: ignore[reportArgumentType]
            input=text,
            speed=speed,
            response_format="mp3",
        )
        output_path.write_bytes(response.content)  # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]

    def _chunked_synthesize(
        self,
        request: SynthesisRequest,
        output_path: Path,
        voice: str,
        speed: float,
    ) -> None:
        """Split text into chunks, synthesize each, then stitch."""
        from langlearn_tts.core import stitch_audio

        chunks = _split_text(request.text)
        logger.debug("Chunked %d chars into %d parts", len(request.text), len(chunks))

        with tempfile.TemporaryDirectory() as tmp:
            tmp_dir = Path(tmp)
            paths: list[Path] = []

            for i, chunk in enumerate(chunks):
                chunk_path = tmp_dir / f"chunk_{i:04d}.mp3"
                self._single_synthesize(chunk, chunk_path, voice, speed)
                paths.append(chunk_path)

            stitch_audio(paths, output_path, pause_ms=0)
