"""Shed de backlog de audio guiado por energia, compartilhado entre a entrada
(cliente -> LiveKit) e a saida (agente -> cliente) do bridge.

Ideia central: quando o backlog de uma fila de frames PCM16 passa de um limiar,
em vez de descartar cegamente os frames mais antigos -- o que pode cortar fala --
preferimos descartar apenas os frames silenciosos, comprimindo pausas sem remover
amostras de voz. O descarte cego fica reservado para o regime de rajada da
entrada, onde o excesso e grande e a prioridade e recuperar latencia. Na saida
(fala do agente ja gerada, esperando ser tocada) o descarte cego nunca deve ser
usado: so silencio pode sair.
"""

from __future__ import annotations

import audioop
from dataclasses import dataclass
from typing import Any

_FULL_SCALE = 32768.0


def rms_threshold_from_dbfs(dbfs: float) -> int:
    """Converte um limiar em dBFS para amplitude RMS PCM16 (ex.: -50 -> ~103)."""
    try:
        return max(0, int(_FULL_SCALE * (10 ** (float(dbfs) / 20.0))))
    except (ValueError, OverflowError):
        return 0


def frames_from_ms(value_ms: int, frame_ms: int, *, minimum: int = 1) -> int:
    value_ms = max(0, int(value_ms))
    frame_ms = max(1, int(frame_ms))
    return max(minimum, (value_ms + frame_ms - 1) // frame_ms)


def frame_rms(frame: bytes, sample_width: int = 2) -> int:
    try:
        return audioop.rms(frame, sample_width)
    except Exception:
        return 0


def is_silent_frame(frame: bytes, rms_threshold: int, *, sample_width: int = 2) -> bool:
    return frame_rms(frame, sample_width) <= rms_threshold


@dataclass(frozen=True)
class ShedResult:
    dropped: int = 0
    dropped_silent: int = 0
    dropped_voiced: int = 0
    mode: str = "none"


def shed_queue_backlog(
    queue: Any,
    *,
    keep_frames: int,
    rms_threshold: int,
    blind: bool,
    sample_width: int = 2,
) -> ShedResult:
    """Reduz ``queue`` (``asyncio.Queue[bytes]``) ate ``keep_frames``.

    - ``blind=True``: descarta os frames mais antigos ate chegar em
      ``keep_frames`` (regime de rajada -- recupera latencia mesmo cortando
      conteudo).
    - ``blind=False`` (guiado por energia): drena a fila e descarta apenas frames
      silenciosos (rms <= ``rms_threshold``), preservando a fala. Pode terminar
      acima de ``keep_frames`` se nao houver silencio suficiente -- de proposito.

    IMPORTANTE: deve ser chamada sem ``await`` entre os get/put. No event loop
    isso e atomico: nenhum produtor/consumidor da fila roda no meio da drenagem.
    """
    keep_frames = max(0, int(keep_frames))
    qsize = queue.qsize()
    need = qsize - keep_frames
    if need <= 0:
        return ShedResult()

    if blind:
        dropped = 0
        while queue.qsize() > keep_frames:
            try:
                queue.get_nowait()
            except Exception:
                break
            dropped += 1
        return ShedResult(dropped=dropped, dropped_voiced=dropped, mode="blind")

    frames: list[bytes] = []
    while True:
        try:
            frames.append(queue.get_nowait())
        except Exception:
            break

    to_drop = need
    dropped_silent = 0
    kept: list[bytes] = []
    for frame in frames:
        if to_drop > 0 and is_silent_frame(frame, rms_threshold, sample_width=sample_width):
            to_drop -= 1
            dropped_silent += 1
            continue
        kept.append(frame)

    for frame in kept:
        queue.put_nowait(frame)

    return ShedResult(dropped=dropped_silent, dropped_silent=dropped_silent, mode="energy")
