"""Ambiente de oficina (loop) mezclado en el RTP hacia el llamante."""

from __future__ import annotations

import audioop
import logging
import subprocess
from pathlib import Path

from media.rtp_session import SAMPLES_PER_PACKET

logger = logging.getLogger(__name__)

# Silencio µ-law (0xff) de un frame 20 ms
_SILENCE_FRAME = b"\xff" * SAMPLES_PER_PACKET


class AmbienceLoop:
    """Audio µ-law 8 kHz en loop, con ganancia y mezcla con TTS."""

    def __init__(self, ulaw: bytes, *, gain: float = 0.18) -> None:
        if not ulaw:
            raise ValueError("Ambience vacío")
        # Asegurar múltiplo de frame
        rem = len(ulaw) % SAMPLES_PER_PACKET
        if rem:
            ulaw = ulaw + b"\xff" * (SAMPLES_PER_PACKET - rem)
        self._ulaw = ulaw
        self._gain = max(0.0, min(1.0, float(gain)))
        self._offset = 0
        self._n = len(ulaw)

    @property
    def duration_s(self) -> float:
        return self._n / 8000.0

    def next_frame(self) -> bytes:
        """Siguiente frame de ambiente atenuado (20 ms)."""
        end = self._offset + SAMPLES_PER_PACKET
        if end <= self._n:
            chunk = self._ulaw[self._offset:end]
            self._offset = end % self._n
        else:
            # Cruce de fin de loop
            first = self._ulaw[self._offset :]
            need = SAMPLES_PER_PACKET - len(first)
            second = self._ulaw[:need]
            chunk = first + second
            self._offset = need
        if self._gain >= 0.999:
            return chunk
        if self._gain <= 0.001:
            return _SILENCE_FRAME
        return attenuate_ulaw(chunk, self._gain)

    def mix_with_tts(self, tts_ulaw: bytes) -> bytes:
        """Mezcla un frame TTS con el siguiente frame de ambiente."""
        if len(tts_ulaw) < SAMPLES_PER_PACKET:
            tts_ulaw = tts_ulaw + b"\xff" * (SAMPLES_PER_PACKET - len(tts_ulaw))
        elif len(tts_ulaw) > SAMPLES_PER_PACKET:
            tts_ulaw = tts_ulaw[:SAMPLES_PER_PACKET]
        bg = self.next_frame()
        return mix_ulaw(tts_ulaw, bg)


def attenuate_ulaw(ulaw: bytes, gain: float) -> bytes:
    pcm = audioop.ulaw2lin(ulaw, 2)
    pcm = audioop.mul(pcm, 2, gain)
    return audioop.lin2ulaw(pcm, 2)


def mix_ulaw(a: bytes, b: bytes) -> bytes:
    """Suma PCM con recorte suave vía audioop.add."""
    n = max(len(a), len(b))
    if len(a) < n:
        a = a + b"\xff" * (n - len(a))
    if len(b) < n:
        b = b + b"\xff" * (n - len(b))
    pa = audioop.ulaw2lin(a, 2)
    pb = audioop.ulaw2lin(b, 2)
    try:
        mixed = audioop.add(pa, pb, 2)
    except audioop.error:
        # Overflow raro: atenuar ambos y reintentar
        pa = audioop.mul(pa, 2, 0.7)
        pb = audioop.mul(pb, 2, 0.7)
        mixed = audioop.add(pa, pb, 2)
    return audioop.lin2ulaw(mixed, 2)


def load_ambience_ulaw(path: Path) -> bytes:
    """Carga µ-law 8 kHz; convierte con ffmpeg si hace falta y cachea .ulaw."""
    path = path.resolve()
    if not path.is_file():
        raise FileNotFoundError(f"Ambience no encontrado: {path}")

    if path.suffix.lower() == ".ulaw":
        data = path.read_bytes()
        if not data:
            raise ValueError(f"Archivo ulaw vacío: {path}")
        return data

    cache = path.with_suffix(path.suffix + ".ulaw")
    if (
        cache.is_file()
        and cache.stat().st_mtime >= path.stat().st_mtime
        and cache.stat().st_size > 0
    ):
        logger.info("Ambience cache ulaw: %s (%d bytes)", cache, cache.stat().st_size)
        return cache.read_bytes()

    logger.info("Convirtiendo ambience a µ-law 8 kHz: %s", path)
    result = subprocess.run(
        [
            "ffmpeg",
            "-y",
            "-i",
            str(path),
            "-ar",
            "8000",
            "-ac",
            "1",
            "-f",
            "mulaw",
            str(cache),
        ],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0 or not cache.is_file() or cache.stat().st_size == 0:
        err = (result.stderr or result.stdout or "")[-500:]
        raise RuntimeError(f"ffmpeg falló al convertir {path}: {err}")

    data = cache.read_bytes()
    logger.info(
        "Ambience listo: %s → %s (%.1f s)",
        path.name,
        cache.name,
        len(data) / 8000.0,
    )
    return data


_cached: dict[str, bytes] = {}


def get_ambience_loop(path: Path, *, gain: float) -> AmbienceLoop:
    key = str(path.resolve())
    if key not in _cached:
        _cached[key] = load_ambience_ulaw(path)
    return AmbienceLoop(_cached[key], gain=gain)
