"""Audio capture service — shared audio analysis with ref counting.

Provides real-time FFT spectrum, RMS level, and beat detection from
system audio or microphone/line-in.  Multiple AudioColorStripStreams
sharing the same device reuse a single capture thread via
AudioCaptureManager.

Engine-agnostic: uses AudioEngineRegistry to create the underlying
capture stream (WASAPI, sounddevice, etc.).
"""

import threading
import time
from typing import Any, Dict, List, Optional, Tuple

from wled_controller.core.audio.analysis import (
    AudioAnalysis,
    AudioAnalyzer,
)
from wled_controller.core.audio.base import AudioCaptureStreamBase
from wled_controller.core.audio.factory import AudioEngineRegistry
from wled_controller.utils import get_logger

logger = get_logger(__name__)


# Re-export for backward compatibility
__all__ = [
    "AudioAnalysis",
    "ManagedAudioStream",
    "AudioCaptureManager",
]


# ---------------------------------------------------------------------------
# ManagedAudioStream — wraps engine stream + analyzer in background thread
# ---------------------------------------------------------------------------

class ManagedAudioStream:
    """Wraps an AudioCaptureStreamBase + AudioAnalyzer in a background thread.

    Public API is the same as the old AudioCaptureStream:
    start(), stop(), get_latest_analysis(), get_last_timing().
    """

    def __init__(
        self,
        engine_type: str,
        device_index: int,
        is_loopback: bool,
        engine_config: Optional[Dict[str, Any]] = None,
    ):
        self._engine_type = engine_type
        self._device_index = device_index
        self._is_loopback = is_loopback
        self._engine_config = engine_config or {}

        self._running = False
        self._thread: Optional[threading.Thread] = None
        self._lock = threading.Lock()
        self._latest: Optional[AudioAnalysis] = None
        self._last_timing: dict = {}

    def start(self) -> None:
        if self._running:
            return
        self._running = True
        self._thread = threading.Thread(
            target=self._capture_loop, daemon=True,
            name=f"AudioCapture-{self._engine_type}-{self._device_index}-"
                 f"{'lb' if self._is_loopback else 'in'}",
        )
        self._thread.start()
        logger.info(
            f"ManagedAudioStream started: engine={self._engine_type} "
            f"device={self._device_index} loopback={self._is_loopback}"
        )

    def stop(self) -> None:
        self._running = False
        if self._thread is not None:
            self._thread.join(timeout=5.0)
            self._thread = None
        with self._lock:
            self._latest = None
        logger.info(
            f"ManagedAudioStream stopped: engine={self._engine_type} "
            f"device={self._device_index}"
        )

    def get_latest_analysis(self) -> Optional[AudioAnalysis]:
        with self._lock:
            return self._latest

    def get_last_timing(self) -> dict:
        return dict(self._last_timing)

    def _capture_loop(self) -> None:
        stream: Optional[AudioCaptureStreamBase] = None
        try:
            stream = AudioEngineRegistry.create_stream(
                self._engine_type, self._device_index,
                self._is_loopback, self._engine_config,
            )
            stream.initialize()

            sample_rate = stream.sample_rate
            chunk_size = stream.chunk_size
            channels = stream.channels

            analyzer = AudioAnalyzer(sample_rate=sample_rate, chunk_size=chunk_size)

            logger.info(
                f"Audio stream opened: engine={self._engine_type} "
                f"device={self._device_index} loopback={self._is_loopback} "
                f"channels={channels} sr={sample_rate}"
            )

            while self._running:
                t_read_start = time.perf_counter()
                raw_data = stream.read_chunk()
                if raw_data is None:
                    time.sleep(0.05)
                    continue
                t_read_end = time.perf_counter()

                analysis = analyzer.analyze(raw_data, channels)

                t_fft_end = time.perf_counter()
                self._last_timing = {
                    "read_ms": (t_read_end - t_read_start) * 1000,
                    "fft_ms": (t_fft_end - t_read_end) * 1000,
                }

                with self._lock:
                    self._latest = analysis

        except Exception as e:
            logger.error(f"ManagedAudioStream fatal error: {e}", exc_info=True)
        finally:
            if stream is not None:
                try:
                    stream.cleanup()
                except Exception as e:
                    logger.debug("Audio stream cleanup error: %s", e)
                    pass
            self._running = False
            logger.info(
                f"ManagedAudioStream loop ended: engine={self._engine_type} "
                f"device={self._device_index}"
            )


# ---------------------------------------------------------------------------
# AudioCaptureManager — ref-counted shared capture streams
# ---------------------------------------------------------------------------

class AudioCaptureManager:
    """Manages shared ManagedAudioStream instances with reference counting.

    Multiple AudioColorStripStreams using the same audio device share a
    single capture thread. Key: (engine_type, device_index, is_loopback).
    """

    def __init__(self):
        self._streams: Dict[
            Tuple[str, int, bool],
            Tuple[ManagedAudioStream, int],
        ] = {}
        self._lock = threading.Lock()

    def acquire(
        self,
        device_index: int,
        is_loopback: bool,
        engine_type: Optional[str] = None,
        engine_config: Optional[Dict[str, Any]] = None,
    ) -> ManagedAudioStream:
        """Get or create a ManagedAudioStream for the given device.

        Args:
            device_index: Audio device index
            is_loopback: Whether to capture loopback audio
            engine_type: Engine type (falls back to best available if None)
            engine_config: Engine-specific configuration

        Returns:
            Shared ManagedAudioStream instance.
        """
        if engine_type is None:
            engine_type = AudioEngineRegistry.get_best_available_engine()
            if engine_type is None:
                raise RuntimeError("No audio capture engines available")

        key = (engine_type, device_index, is_loopback)
        with self._lock:
            if key in self._streams:
                stream, ref_count = self._streams[key]
                self._streams[key] = (stream, ref_count + 1)
                logger.info(f"Reusing audio capture {key} (ref_count={ref_count + 1})")
                return stream

            stream = ManagedAudioStream(
                engine_type, device_index, is_loopback, engine_config,
            )
            stream.start()
            self._streams[key] = (stream, 1)
            logger.info(f"Created audio capture {key}")
            return stream

    def release(
        self,
        device_index: int,
        is_loopback: bool,
        engine_type: Optional[str] = None,
    ) -> None:
        """Release a reference to a ManagedAudioStream."""
        if engine_type is None:
            engine_type = AudioEngineRegistry.get_best_available_engine()
            if engine_type is None:
                return

        key = (engine_type, device_index, is_loopback)
        stream_to_stop = None
        with self._lock:
            if key not in self._streams:
                logger.warning(f"Attempted to release unknown audio capture: {key}")
                return

            stream, ref_count = self._streams[key]
            ref_count -= 1
            if ref_count <= 0:
                stream_to_stop = stream
                del self._streams[key]
                logger.info(f"Removed audio capture {key}")
            else:
                self._streams[key] = (stream, ref_count)
                logger.debug(f"Released audio capture {key} (ref_count={ref_count})")
        # Stop outside the lock — stream.stop() joins a thread (up to 5s)
        if stream_to_stop is not None:
            stream_to_stop.stop()

    def release_all(self) -> None:
        """Stop and remove all capture streams. Called on shutdown."""
        with self._lock:
            streams_to_stop = list(self._streams.items())
            self._streams.clear()
        # Stop outside the lock — each stop() joins a thread
        for key, (stream, _) in streams_to_stop:
            try:
                stream.stop()
            except Exception as e:
                logger.error(f"Error stopping audio capture {key}: {e}")
        logger.info("Released all audio capture streams")

    @staticmethod
    def enumerate_devices() -> List[dict]:
        """List available audio devices from all registered engines.

        Returns list of dicts with device info, each tagged with engine_type.
        Deduplicates by (name, is_loopback), keeping the entry from the
        highest-priority engine.
        """
        # Collect from all engines, sorted by descending priority
        engines = [
            (engine_class.ENGINE_PRIORITY, engine_type, engine_class)
            for engine_type, engine_class in AudioEngineRegistry.get_all_engines().items()
        ]
        engines.sort(key=lambda x: x[0], reverse=True)

        seen: set = set()
        result = []
        for _priority, engine_type, engine_class in engines:
            try:
                if not engine_class.is_available():
                    continue
                for dev in engine_class.enumerate_devices():
                    key = (dev.name, dev.is_loopback)
                    if key in seen:
                        continue
                    seen.add(key)
                    result.append({
                        "index": dev.index,
                        "name": dev.name,
                        "is_input": dev.is_input,
                        "is_loopback": dev.is_loopback,
                        "channels": dev.channels,
                        "default_samplerate": dev.default_samplerate,
                        "engine_type": engine_type,
                    })
            except Exception as e:
                logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
        return result

    @staticmethod
    def enumerate_devices_by_engine() -> Dict[str, List[dict]]:
        """List available audio devices grouped by engine type.

        Unlike enumerate_devices(), does NOT deduplicate across engines.
        Each engine's devices are returned with their engine-specific indices.
        """
        result: Dict[str, List[dict]] = {}
        for engine_type, engine_class in AudioEngineRegistry.get_all_engines().items():
            try:
                if not engine_class.is_available():
                    continue
                devices = []
                for dev in engine_class.enumerate_devices():
                    devices.append({
                        "index": dev.index,
                        "name": dev.name,
                        "is_input": dev.is_input,
                        "is_loopback": dev.is_loopback,
                        "channels": dev.channels,
                        "default_samplerate": dev.default_samplerate,
                        "engine_type": engine_type,
                    })
                result[engine_type] = devices
            except Exception as e:
                logger.error(f"Error enumerating devices for engine '{engine_type}': {e}")
        return result
