"""tts_utils.py — Pure TTS helpers for streaming synthesis.

No ROS2 dependencies. Provides streaming Piper synthesis and audio chunking.

Tested by test/test_tts_utils.py.
"""

from __future__ import annotations

import re
import struct
from typing import Generator, List, Tuple


# ── SSML helpers ──────────────────────────────────────────────────────────────

_SSML_STRIP_RE = re.compile(r"<[^>]+>")


def strip_ssml(text: str) -> str:
    """Remove SSML tags, returning plain text."""
    return _SSML_STRIP_RE.sub("", text).strip()


def split_sentences(text: str) -> List[str]:
    """Split text into sentences for streaming TTS synthesis.

    Returns list of non-empty sentence strings.
    """
    # Split on sentence-ending punctuation followed by whitespace or end
    parts = re.split(r"(?<=[.!?])\s+", text.strip())
    return [p.strip() for p in parts if p.strip()]


def split_first_sentence(text: str) -> Tuple[str, str]:
    """Return (first_sentence, remainder).

    Used by TTS node to begin synthesis of first sentence immediately
    while the LLM continues generating the rest.
    """
    sentences = split_sentences(text)
    if not sentences:
        return "", ""
    return sentences[0], " ".join(sentences[1:])


# ── PCM audio helpers ─────────────────────────────────────────────────────────

def pcm16_to_wav_bytes(pcm_data: bytes, sample_rate: int = 22050) -> bytes:
    """Wrap raw PCM16 LE mono data with a WAV header."""
    num_samples = len(pcm_data) // 2
    num_channels = 1
    bits_per_sample = 16
    byte_rate = sample_rate * num_channels * bits_per_sample // 8
    block_align = num_channels * bits_per_sample // 8
    data_size = len(pcm_data)
    header_size = 44

    header = struct.pack(
        "<4sI4s4sIHHIIHH4sI",
        b"RIFF",
        header_size - 8 + data_size,
        b"WAVE",
        b"fmt ",
        16,                 # chunk size
        1,                  # PCM format
        num_channels,
        sample_rate,
        byte_rate,
        block_align,
        bits_per_sample,
        b"data",
        data_size,
    )
    return header + pcm_data


def chunk_pcm(pcm_data: bytes, chunk_ms: int = 200, sample_rate: int = 22050) -> Generator[bytes, None, None]:
    """Yield PCM data in fixed-size chunks for streaming playback."""
    chunk_bytes = (sample_rate * 2 * chunk_ms) // 1000  # int16 = 2 bytes/sample
    chunk_bytes = max(chunk_bytes, 2) & ~1  # ensure even (int16 aligned)
    for i in range(0, len(pcm_data), chunk_bytes):
        yield pcm_data[i:i + chunk_bytes]


def estimate_duration_ms(pcm_data: bytes, sample_rate: int = 22050) -> float:
    """Estimate playback duration in ms from PCM byte length."""
    return len(pcm_data) / (sample_rate * 2) * 1000.0


# ── Volume control ────────────────────────────────────────────────────────────

def apply_volume(pcm_data: bytes, gain: float) -> bytes:
    """Scale PCM16 LE samples by gain factor (0.0–2.0). Clips to ±32767."""
    if abs(gain - 1.0) < 0.01:
        return pcm_data
    n = len(pcm_data) // 2
    samples = struct.unpack(f"<{n}h", pcm_data[:n * 2])
    scaled = [max(-32767, min(32767, int(s * gain))) for s in samples]
    return struct.pack(f"<{n}h", *scaled)
