from __future__ import annotations

import asyncio
import collections
import math
import threading
from dataclasses import dataclass

def list_audio_devices() -> list[dict[str, object]]:

    try:
        import sounddevice as sd
    except Exception:
        return []

    out: list[dict[str, object]] = []
    try:
        devs = sd.query_devices()
    except Exception:
        return []

    try:
        for i, d in enumerate(devs):
            name = ""
            try:
                name = str(d.get("name") or "")
            except Exception:
                name = ""
            try:
                max_in = int(d.get("max_input_channels") or 0)
            except Exception:
                max_in = 0
            try:
                max_out = int(d.get("max_output_channels") or 0)
            except Exception:
                max_out = 0
            out.append(
                {
                    "index": int(i),
                    "name": name,
                    "max_input_channels": max_in,
                    "max_output_channels": max_out,
                }
            )
    except Exception:
        return []
    return out

@dataclass
class VoiceParams:
    rate: int = 48000
    channels: int = 1
    frame_ms: int = 20

    @property
    def frames_per_buffer(self) -> int:
        return int(self.rate * self.frame_ms / 1000)

class VoiceEngine:
    def __init__(
        self,
        *,
        params: VoiceParams,
        send_pcm_cb=None,
        input_device: int | None = None,
        output_device: int | None = None,
        mic_gain: float = 1.0,
        speaker_gain: float = 1.0,
        monitor_self: bool = False,
        enable_tx: bool = True,
    ):
        self.params = params
        self._send_pcm_cb = send_pcm_cb
        self._enable_tx = bool(enable_tx)

        self._input_device = input_device
        self._output_device = output_device

        self._muted = False
        self._deafened = False
        self._gain = float(speaker_gain)
        self._mic_gain = float(mic_gain)
        self._monitor_self = bool(monitor_self)

        self._loop: asyncio.AbstractEventLoop | None = None
        self._tx_q: asyncio.Queue[bytes] | None = None
        self._tx_task: asyncio.Task | None = None

        self._rx_buf = collections.deque(maxlen=200)
        self._rx_lock = threading.Lock()

        self._mon_buf = collections.deque(maxlen=40)
        self._mon_lock = threading.Lock()

        self._in_stream = None
        self._out_stream = None

    @property
    def muted(self) -> bool:
        return bool(self._muted)

    @property
    def deafened(self) -> bool:
        return bool(self._deafened)

    @property
    def gain(self) -> float:
        return float(self._gain)

    @property
    def mic_gain(self) -> float:
        return float(self._mic_gain)

    @property
    def monitor_self(self) -> bool:
        return bool(self._monitor_self)

    def set_muted(self, v: bool) -> None:
        self._muted = bool(v)

    def set_deafened(self, v: bool) -> None:
        self._deafened = bool(v)

    def set_mic_gain(self, value: float) -> float:
        try:
            g = float(value)
        except Exception:
            g = float(self._mic_gain)
        g = max(0.0, min(g, 3.0))
        self._mic_gain = g
        return g

    def set_monitor_self(self, v: bool) -> None:
        self._monitor_self = bool(v)

    def adjust_gain(self, delta: float) -> float:
        g = float(self._gain) + float(delta)
        g = max(0.0, min(g, 3.0))
        self._gain = g
        return g

    def set_gain(self, value: float) -> float:
        try:
            g = float(value)
        except Exception:
            g = float(self._gain)
        g = max(0.0, min(g, 3.0))
        self._gain = g
        return g

    async def start(self) -> None:
        try:
            import sounddevice as sd
        except Exception as e:
            raise RuntimeError(f"sounddevice unavailable: {e}")

        try:
            import numpy as np
        except Exception as e:
            raise RuntimeError(f"numpy unavailable: {e}")

        self._loop = asyncio.get_running_loop()

        if self._enable_tx and self._send_pcm_cb is not None:
            self._tx_q = asyncio.Queue(maxsize=200)
        else:
            self._tx_q = None

        frames = self.params.frames_per_buffer
        channels = self.params.channels
        bytes_per_frame = int(frames) * int(channels) * 2

        def in_cb(indata, _frames, _time, status):
            if status:
                pass
            if self._loop is None:
                return
            if self._muted:
                return
            try:
                pcm = indata.tobytes()
            except Exception:
                return

            if self._mic_gain != 1.0:
                try:
                    arr = np.frombuffer(pcm, dtype=np.int16).astype(np.float32)
                    arr = np.clip(arr * float(self._mic_gain), -32768.0, 32767.0).astype(np.int16)
                    pcm = arr.tobytes()
                except Exception:
                    pass

            if self._monitor_self:
                try:
                    with self._mon_lock:
                        self._mon_buf.append(bytes(pcm))
                except Exception:
                    pass

            if self._tx_q is None:
                return

            def _put():
                if self._tx_q is None:
                    return
                if self._tx_q.full():
                    try:
                        self._tx_q.get_nowait()
                    except Exception:
                        return
                try:
                    self._tx_q.put_nowait(pcm)
                except Exception:
                    return

            try:
                self._loop.call_soon_threadsafe(_put)
            except Exception:
                return

        def out_cb(outdata, _frames, _time, status):
            if status:
                pass
            if self._deafened:
                outdata.fill(0)
                return

            pcm_remote = None
            with self._rx_lock:
                if self._rx_buf:
                    pcm_remote = self._rx_buf.popleft()

            pcm_mon = None
            with self._mon_lock:
                if self._mon_buf:

                    pcm_mon = self._mon_buf.pop()

            if pcm_remote is None and pcm_mon is None:
                outdata.fill(0)
                return

            pcm = pcm_remote if pcm_remote is not None else pcm_mon

            try:
                if pcm_remote is not None and pcm_mon is not None:
                    a = np.frombuffer(pcm_remote, dtype=np.int16).astype(np.int32)
                    b = np.frombuffer(pcm_mon, dtype=np.int16).astype(np.int32)
                    n = min(len(a), len(b))
                    if n <= 0:
                        outdata.fill(0)
                        return
                    mix = a[:n] + b[:n]
                    mix = np.clip(mix, -32768, 32767).astype(np.int16)
                    arr = mix
                else:
                    arr = np.frombuffer(pcm, dtype=np.int16)

                if channels > 1:
                    arr = arr.reshape(-1, channels)
                if self._gain != 1.0:
                    f = arr.astype(np.float32) * float(self._gain)
                    f = np.clip(f, -32768.0, 32767.0)
                    arr2 = f.astype(np.int16)
                else:
                    arr2 = arr
                outdata[:] = arr2.reshape(outdata.shape)
            except Exception:
                outdata.fill(0)

        self._in_stream = sd.InputStream(
            samplerate=int(self.params.rate),
            channels=int(channels),
            dtype="int16",
            blocksize=int(frames),
            latency="low",
            callback=in_cb,
            device=self._input_device,
        )
        self._out_stream = sd.OutputStream(
            samplerate=int(self.params.rate),
            channels=int(channels),
            dtype="int16",
            blocksize=int(frames),
            latency="low",
            callback=out_cb,
            device=self._output_device,
        )

        self._in_stream.start()
        self._out_stream.start()

        if self._tx_q is not None and self._send_pcm_cb is not None:
            async def _tx_loop():
                assert self._tx_q is not None
                while True:
                    pcm = await self._tx_q.get()

                    frame_ms = max(1, int(self.params.frame_ms))
                    target_ms = 60
                    frames_per_packet = max(1, int(math.ceil(float(target_ms) / float(frame_ms))))

                    buf = bytearray()
                    buf.extend(pcm)

                    if frames_per_packet > 1:
                        deadline = self._loop.time() + (float(frame_ms) / 1000.0) * float(frames_per_packet - 1) + 0.01
                        for _ in range(frames_per_packet - 1):
                            timeout = max(0.0, deadline - self._loop.time())
                            if timeout <= 0.0:
                                break
                            try:
                                pcm2 = await asyncio.wait_for(self._tx_q.get(), timeout=timeout)
                            except asyncio.TimeoutError:
                                break
                            except Exception:
                                break
                            buf.extend(pcm2)

                    await self._send_pcm_cb(bytes(buf))

            self._tx_task = asyncio.create_task(_tx_loop())
        else:
            self._tx_task = None

    async def stop(self) -> None:
        if self._tx_task is not None:
            self._tx_task.cancel()
            try:
                await self._tx_task
            except Exception:
                pass
            self._tx_task = None

        try:
            if self._in_stream is not None:
                self._in_stream.stop()
                self._in_stream.close()
        except Exception:
            pass
        finally:
            self._in_stream = None

        try:
            if self._out_stream is not None:
                self._out_stream.stop()
                self._out_stream.close()
        except Exception:
            pass
        finally:
            self._out_stream = None

        with self._rx_lock:
            self._rx_buf.clear()

        with self._mon_lock:
            self._mon_buf.clear()

    def push_remote_pcm(self, pcm_s16le: bytes) -> None:
        if not isinstance(pcm_s16le, (bytes, bytearray)):
            return
        bpf = int(self.params.frames_per_buffer) * int(self.params.channels) * 2
        with self._rx_lock:
            if bpf > 0:
                data = bytes(pcm_s16le)
                if len(data) > bpf and (len(data) % bpf) == 0:
                    for i in range(0, len(data), bpf):
                        self._rx_buf.append(data[i : i + bpf])
                    return
            self._rx_buf.append(bytes(pcm_s16le))
