"""
Audio Engine Module.
====================

This module handles the low-level audio recording capabilities using `sounddevice`.
It manages the input stream, buffers audio data in memory, and provides a callback
mechanism for real-time visualization of audio amplitude.

Classes:
    AudioEngine: The main controller for recording streams.
"""

import sounddevice as sd
import numpy as np
import threading
import queue
import logging
from typing import Optional, Callable
from src.core.config import ConfigManager

class AudioEngine:
    """
    Manages audio recording from the default input device.
    Uses ConfigManager for settings (device, silence detection).
    """

    def __init__(self, sample_rate: int = 16000, channels: int = 1):
        """
        Initialize the AudioEngine.
        """
        self.config = ConfigManager()
        self.sample_rate = sample_rate
        self.channels = channels
        self.recording = False
        self.stream: Optional[sd.InputStream] = None
        self.visualizer_callback: Optional[Callable[[float], None]] = None
        self.silence_callback: Optional[Callable[[], None]] = None
        
        # Audio buffer to store the current session's frames
        self.frames = []
        self.last_noise_time = 0.0

    def list_devices(self):
        """
        Query available audio devices.
        
        Returns:
            DeviceList: A list of all available input/output devices seen by PortAudio.
        """
        return sd.query_devices()

    def set_visualizer_callback(self, callback: Callable[[float], None]):
        """
        Register a callback function for visualizer updates.

        Args:
            callback (function): A function that accepts a single float argument (amplitude).
                                 This will be called roughly every audio block.
        """
        self.visualizer_callback = callback

    def set_silence_callback(self, callback: Callable[[], None]):
        """
        Register a callback function for silence detection (Auto-Stop).
        """
        self.silence_callback = callback

    def _audio_callback(self, indata: np.ndarray, frames: int, time, status: sd.CallbackFlags):
        """
        Internal callback used by sounddevice to process incoming audio chunks.

        Args:
            indata (numpy.ndarray): The recorded audio data chunk.
            frames (int): Number of frames.
            time: Timestamp info.
            status: Callback status flags (e.g., overflow warnings).
        """
        if status:
            logging.warning(f"Audio callback status: {status}")
        
        if self.recording:
            # Copy data to avoid buffer race conditions
            data = indata.copy()
            self.frames.append(data)
            
            # Calculate amplitude for visualizer (Root Mean Square)
            if self.visualizer_callback:
                # Calculate RMS of the current chunk to determine loudness
                rms = np.sqrt(np.mean(data**2))
                
                # Apply logarithmic scaling for better sensitivity to quiet sounds
                if rms > 0:
                    # Convert to dB scale, normalize, and apply compression
                    db = 20 * np.log10(rms + 1e-10)  # Add small value to avoid log(0)
                    # Map from typical range (-60 to 0 dB) to (0 to 1)
                    amp = float(np.clip((db + 60) / 60, 0.0, 1.0))
                    # Apply power curve for better sensitivity at low levels
                    amp = np.power(amp, 0.5)  # Square root gives good response to quiet sounds
                else:
                    amp = 0.0
                
                # Apply exponential smoothing to prevent jumpy waveform
                if not hasattr(self, '_smoothed_amp'):
                    self._smoothed_amp = amp
                else:
                    # Exponential moving average with smoothing factor 0.3
                    self._smoothed_amp = 0.3 * amp + 0.7 * self._smoothed_amp
                
                self.visualizer_callback(self._smoothed_amp)



            # --- Silence Detection Logic ---
            # We calculate this even if visualizer is off
            # Calculate linear RMS for VAD comparison
            raw_rms = np.sqrt(np.mean(data**2))
            # Heuristic mapping: 0.1 RMS = 100% threshold
            vad_level = float(np.clip(raw_rms * 10, 0.0, 1.0))

            import time
            current_time = time.time()
            
            # Fetch params dynamically
            threshold = float(self.config.get("silence_threshold"))
            duration = float(self.config.get("silence_duration"))
            
            if vad_level > threshold:
                self.last_noise_time = current_time
            else:
                # If we have been silent for > silence_duration, trigger auto-stop
                if (current_time - self.last_noise_time) > duration:
                    if self.silence_callback:
                        logging.info(f"Silence detected ({duration}s). Triggering auto-stop.")
                        # Reset last_noise_time to prevent spamming
                        self.last_noise_time = current_time 
                        self.silence_callback()

    def start_recording(self, device: Optional[int] = None):
        """
        Start the recording stream.

        Args:
            device (int, optional): The device ID to use. Defaults to system default.
        """
        if self.recording:
            return

        self.frames = [] # Reset buffer
        self.recording = True
        import time
        self.last_noise_time = time.time() # Reset silence timer
        
        # Determine Device
        # If passed arg is None, check Config. If Config is None, use Default.
        if device is None:
            device = self.config.get("input_device")
            
        try:
            self.stream = sd.InputStream(
                samplerate=self.sample_rate,
                channels=self.channels,
                device=device,
                callback=self._audio_callback
            )
            self.stream.start()
            logging.info("Audio recording started.")
        except Exception as e:
            logging.error(f"Failed to start recording: {e}")
            self.recording = False

    def stop_recording(self) -> np.ndarray:
        """
        Stop the current recording session and return the captured audio.

        Returns:
            np.ndarray: The complete audio recording flattened into a single numpy array.
                        Returns an empty array if nothing was recorded.
        """
        if not self.recording:
            return np.array([], dtype=np.float32)

        self.recording = False
        if self.stream:
            self.stream.stop()
            self.stream.close()
            self.stream = None
            logging.info("Audio recording stopped.")

        if not self.frames:
            return np.array([], dtype=np.float32)

        # Concatenate all buffered chunks into one continuous array
        # sounddevice returns (frames, channels), so we get (N, 1).
        # Whisper expects flattened 1D array (N,).
        audio = np.concatenate(self.frames, axis=0)
        return audio.flatten()
