"""Audio capture implementation using sounddevice.

Provide cross-platform audio input capture with device handling.
"""

from __future__ import annotations

import time
from collections.abc import Mapping, Sequence
from typing import TYPE_CHECKING, Protocol, cast

import numpy as np
import sounddevice as sd

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.audio.dto import AudioDeviceInfo, AudioFrameCallback
from noteflow.infrastructure.logging import get_logger

if TYPE_CHECKING:
    from numpy.typing import NDArray


class _InputStream(Protocol):
    active: bool

    def start(self) -> None: ...
    def stop(self) -> None: ...
    def close(self) -> None: ...


class _SoundDeviceDefault(Protocol):
    device: tuple[int | None, int | None]


class _SoundDeviceModule(Protocol):
    default: _SoundDeviceDefault

    def query_devices(
        self, device: int | None = None, kind: str | None = None
    ) -> Sequence[Mapping[str, object]] | Mapping[str, object]: ...

    def InputStream(self, **kwargs: object) -> _InputStream: ...

logger = get_logger(__name__)


def _int_from_object(value: object, default: int = 0) -> int:
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return int(value)
    if isinstance(value, str):
        try:
            return int(value)
        except ValueError:
            return default
    return default


class SoundDeviceCapture:
    """sounddevice-based implementation of AudioCapture.

    Handle device enumeration, stream management, and device change detection.
    Use PortAudio under the hood for cross-platform audio capture.
    """

    def __init__(self) -> None:
        """Initialize the capture instance."""
        self._stream: _InputStream | None = None
        self._callback: AudioFrameCallback | None = None
        self._device_id: int | None = None
        self._sample_rate: int = DEFAULT_SAMPLE_RATE
        self._channels: int = 1

    def list_devices(self) -> list[AudioDeviceInfo]:
        """List available audio input devices.

        Returns:
            List of AudioDeviceInfo for all available input devices.
        """
        devices: list[AudioDeviceInfo] = []
        sd_typed = cast(_SoundDeviceModule, sd)
        device_list_value = sd_typed.query_devices()
        if isinstance(device_list_value, Mapping):
            raw_devices: list[Mapping[str, object]] = [device_list_value]
        else:
            raw_devices = list(device_list_value)

        # Get default input device index
        try:
            default_input = sd_typed.default.device[0]  # Input device index
        except (TypeError, IndexError):
            default_input = -1

        for idx, dev in enumerate(raw_devices):
            max_input_channels = _int_from_object(dev.get("max_input_channels", 0))
            if max_input_channels <= 0:
                continue
            devices.append(
                AudioDeviceInfo(
                    device_id=idx,
                    name=str(dev.get("name", "")),
                    channels=max_input_channels,
                    sample_rate=_int_from_object(dev.get("default_samplerate", 0)),
                    is_default=(idx == default_input),
                )
            )
        return devices

    def get_default_device(self) -> AudioDeviceInfo | None:
        """Get the default input device.

        Returns:
            Default input device info, or None if no input devices available.
        """
        devices = self.list_devices()
        for dev in devices:
            if dev.is_default:
                return dev
        return devices[0] if devices else None

    def start(
        self,
        device_id: int | None,
        on_frames: AudioFrameCallback,
        sample_rate: int = DEFAULT_SAMPLE_RATE,
        channels: int = 1,
        chunk_duration_ms: int = 100,
    ) -> None:
        """Start capturing audio from the specified device.

        Args:
            device_id: Device ID to capture from, or None for default device.
            on_frames: Callback receiving (frames, timestamp) for each chunk.
            sample_rate: Sample rate in Hz (default 16kHz for ASR).
            channels: Number of channels (default 1 for mono).
            chunk_duration_ms: Duration of each audio chunk in milliseconds.

        Raises:
            RuntimeError: If already capturing.
            ValueError: If device_id is invalid.
        """
        if self._stream is not None:
            raise RuntimeError("Already capturing audio")

        self._callback = on_frames
        self._device_id = device_id
        self._sample_rate = sample_rate
        self._channels = channels

        # Calculate block size from chunk duration
        blocksize = int(sample_rate * chunk_duration_ms / 1000)

        def _stream_callback(
            indata: NDArray[np.float32],
            frames: int,
            time_info: object,  # cffi CData from sounddevice, unused
            status: sd.CallbackFlags,
        ) -> None:
            """Internal sounddevice callback."""
            # Suppress unused parameter warnings
            _ = frames, time_info

            if status:
                logger.warning("Audio stream status: %s", status)

            if self._callback is not None:
                # Zero-copy optimization: mono slices column, multi flattens (3x faster)
                audio_data = indata[:, 0].copy() if channels == 1 else indata.flatten()
                timestamp = time.monotonic()
                self._callback(audio_data, timestamp)

        try:
            sd_typed = cast(_SoundDeviceModule, sd)
            stream = sd_typed.InputStream(
                device=device_id,
                channels=channels,
                samplerate=sample_rate,
                blocksize=blocksize,
                dtype=np.float32,
                callback=_stream_callback,
            )
            stream.start()
            self._stream = stream
            logger.info(
                "Started audio capture: device=%s, rate=%d, channels=%d, blocksize=%d",
                device_id,
                sample_rate,
                channels,
                blocksize,
            )
        except sd.PortAudioError as e:
            self._stream = None
            self._callback = None
            raise RuntimeError(f"Failed to start audio capture: {e}") from e

    def stop(self) -> None:
        """Stop audio capture.

        Safe to call even if not capturing.
        """
        stream = self._stream
        if stream is not None:
            try:
                stream.stop()
                stream.close()
            except sd.PortAudioError as e:
                logger.warning("Error stopping audio stream: %s", e)
            finally:
                self._stream = None
                self._callback = None
                logger.info("Stopped audio capture")

    def is_capturing(self) -> bool:
        """Check if currently capturing audio.

        Returns:
            True if capture is active.
        """
        stream = self._stream
        return stream is not None and bool(stream.active)

    @property
    def current_device_id(self) -> int | None:
        """Get the current device ID being used for capture."""
        return self._device_id

    @property
    def sample_rate(self) -> int:
        """Get the current sample rate."""
        return self._sample_rate

    @property
    def channels(self) -> int:
        """Get the current number of channels."""
        return self._channels
