"""Audio capture: microphone + system audio via ScreenCaptureKit.

CRITICAL: This module must NOT be imported before daemonize().
CoreAudio crashes if Python forks after loading audio libraries.
Import inside _run_active_session() or after the daemon fork.
"""

from __future__ import annotations

import ctypes
import sys

import numpy as np
import sounddevice as sd
from collections import deque
from queue import Queue, Empty
from threading import Event, Thread
from typing import Optional, Union


SAMPLE_RATE = 16000  # 16kHz for Deepgram/Whisper (not 48kHz)
CHANNELS = 1  # Mono for transcription
BLOCK_SIZE = 1024


def find_capture_device() -> tuple[Optional[int], bool]:
    """Find the default microphone for capture."""
    try:
        default_input = sd.default.device[0]
        if default_input is not None and default_input >= 0:
            device_info = sd.query_devices(default_input)
            if device_info["max_input_channels"] > 0:
                return default_input, False
    except Exception:
        pass

    devices = sd.query_devices()
    for i, device in enumerate(devices):
        if device["max_input_channels"] > 0:
            name = device["name"].lower()
            if "blackhole" in name or "virtual" in name or "aggregate" in name:
                continue
            return i, False

    for i, device in enumerate(devices):
        if device["max_input_channels"] > 0:
            return i, False

    return None, False


class AudioCapture:
    """Captures audio from default microphone."""

    def __init__(
        self,
        device: Union[int, str, None] = None,
        sample_rate: int = SAMPLE_RATE,
        channels: Optional[int] = None,
        block_size: int = BLOCK_SIZE,
    ):
        self.sample_rate = sample_rate
        self.block_size = block_size
        self.audio_queue: Queue[np.ndarray] = Queue()
        self.stop_event = Event()
        self.stream: Optional[sd.InputStream] = None

        self.has_system_audio = False
        if device is None:
            self.device_index, _ = find_capture_device()
            if self.device_index is None:
                raise RuntimeError("No audio input device found.")
        elif isinstance(device, str):
            devices = sd.query_devices()
            self.device_index = None
            for i, d in enumerate(devices):
                if device.lower() in d["name"].lower():
                    self.device_index = i
                    break
            if self.device_index is None:
                raise RuntimeError(f"Device '{device}' not found.")
        else:
            self.device_index = device

        device_info = sd.query_devices(self.device_index)
        max_channels = device_info["max_input_channels"]
        self.channels = channels if channels is not None else min(max_channels, CHANNELS)

    def _audio_callback(self, indata, frames, time_info, status):
        if status:
            pass  # ignore status warnings in production
        self.audio_queue.put(indata.copy())

    def start(self):
        self.stop_event.clear()
        self.stream = sd.InputStream(
            device=self.device_index,
            channels=self.channels,
            samplerate=self.sample_rate,
            blocksize=self.block_size,
            dtype=np.float32,
            callback=self._audio_callback,
        )
        self.stream.start()

    def pause(self):
        """Pause: fully release mic so CoreAudio reports device as free."""
        if self.stream:
            self.stream.stop()
            self.stream.close()
            self.stream = None

    def resume(self):
        """Resume after pause by recreating the stream."""
        if self.stream is None:
            self.stream = sd.InputStream(
                device=self.device_index,
                channels=self.channels,
                samplerate=self.sample_rate,
                blocksize=self.block_size,
                dtype=np.float32,
                callback=self._audio_callback,
            )
            self.stream.start()

    def stop(self):
        self.stop_event.set()
        if self.stream:
            self.stream.stop()
            self.stream.close()
            self.stream = None

    def get_audio(self, timeout: float = 1.0) -> Optional[np.ndarray]:
        try:
            return self.audio_queue.get(timeout=timeout)
        except Empty:
            return None


class SilenceDetector:
    """Detects extended silence in audio stream."""

    def __init__(
        self,
        threshold: float = 0.01,
        silence_duration: float = 60.0,
        sample_rate: int = SAMPLE_RATE,
    ):
        self.threshold = threshold
        self.silence_samples = int(silence_duration * sample_rate)
        self.current_silence = 0
        self.sample_rate = sample_rate

    def update(self, audio: np.ndarray) -> bool:
        """Returns True if extended silence detected (meeting likely ended)."""
        rms = np.sqrt(np.mean(audio ** 2))
        if rms < self.threshold:
            self.current_silence += len(audio)
        else:
            self.current_silence = 0
        return self.current_silence >= self.silence_samples

    def reset(self):
        self.current_silence = 0

    def is_audio_present(self, audio: np.ndarray) -> bool:
        rms = np.sqrt(np.mean(audio ** 2))
        return rms >= self.threshold


class ScreenCaptureAudio:
    """Captures system audio using ScreenCaptureKit (macOS 12.3+)."""

    def __init__(self, sample_rate: int = SAMPLE_RATE, channels: int = CHANNELS):
        self.sample_rate = sample_rate
        self.channels = channels
        self.audio_queue: Queue[np.ndarray] = Queue()
        self._is_running = False
        self._stream = None
        self._delegate = None
        self._dispatch_queue = None

    def _check_availability(self) -> bool:
        if sys.platform != "darwin":
            return False
        try:
            import ScreenCaptureKit
            return True
        except ImportError:
            return False

    def start(self) -> bool:
        if not self._check_availability():
            return False

        try:
            import ScreenCaptureKit as SCK
            import CoreMedia as CM
            from Foundation import NSObject
            import threading

            dispatch_queue_create = None
            DISPATCH_QUEUE_SERIAL = None
            try:
                from libdispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL
            except ImportError:
                try:
                    from dispatch import dispatch_queue_create, DISPATCH_QUEUE_SERIAL
                except ImportError:
                    pass

            audio_queue = self.audio_queue
            parent = self

            class StreamOutput(NSObject):
                def stream_didOutputSampleBuffer_ofType_(self, stream, sampleBuffer, outputType):
                    if outputType != 1:
                        return
                    try:
                        blockBuffer = CM.CMSampleBufferGetDataBuffer(sampleBuffer)
                        if blockBuffer is None:
                            return
                        length = CM.CMBlockBufferGetDataLength(blockBuffer)
                        if length == 0:
                            return
                        result = CM.CMBlockBufferCreateContiguous(
                            None, blockBuffer, None, None, 0, length, 0, None
                        )
                        if result[0] == 0:
                            contigBuffer = result[1]
                            dataResult = CM.CMBlockBufferGetDataPointer(
                                contigBuffer, 0, None, None, None
                            )
                            if dataResult[0] == 0:
                                dataPtr = dataResult[3]
                                if isinstance(dataPtr, tuple) and len(dataPtr) > 0:
                                    ptr_addr = dataPtr[0]
                                    num_floats = length // 4
                                    float_ptr = ctypes.cast(
                                        ptr_addr, ctypes.POINTER(ctypes.c_float)
                                    )
                                    audio = np.ctypeslib.as_array(
                                        float_ptr, shape=(num_floats,)
                                    ).copy()
                                    if len(audio) > 0:
                                        audio_queue.put(audio)
                    except Exception:
                        pass

                def stream_didStopWithError_(self, stream, error):
                    parent._is_running = False

            self._delegate = StreamOutput.alloc().init()

            if dispatch_queue_create is not None:
                self._dispatch_queue = dispatch_queue_create(
                    b"com.meetinghelper.screencapture",
                    DISPATCH_QUEUE_SERIAL,
                )
            else:
                self._dispatch_queue = None

            content_ready = threading.Event()
            content_result = [None, None]

            def on_content(content, error):
                content_result[0] = content
                content_result[1] = error
                content_ready.set()

            SCK.SCShareableContent.getShareableContentWithCompletionHandler_(on_content)
            if not content_ready.wait(timeout=5.0):
                return False

            content, error = content_result
            if error or not content:
                return False

            if not content.displays() or len(content.displays()) == 0:
                return False

            display = content.displays()[0]
            contentFilter = SCK.SCContentFilter.alloc().initWithDisplay_excludingWindows_(
                display, []
            )

            config = SCK.SCStreamConfiguration.alloc().init()
            config.setCapturesAudio_(True)
            config.setExcludesCurrentProcessAudio_(False)
            config.setSampleRate_(self.sample_rate)
            config.setChannelCount_(self.channels)
            config.setWidth_(100)
            config.setHeight_(100)
            config.setMinimumFrameInterval_(CM.CMTimeMake(1, 2))

            self._stream = SCK.SCStream.alloc().initWithFilter_configuration_delegate_(
                contentFilter, config, self._delegate
            )
            if self._stream is None:
                return False

            self._stream.addStreamOutput_type_sampleHandlerQueue_error_(
                self._delegate, 0, self._dispatch_queue, None
            )
            self._stream.addStreamOutput_type_sampleHandlerQueue_error_(
                self._delegate, 1, self._dispatch_queue, None
            )

            start_ready = threading.Event()
            start_error = [None]

            def on_start(error):
                start_error[0] = error
                start_ready.set()

            self._stream.startCaptureWithCompletionHandler_(on_start)
            if not start_ready.wait(timeout=5.0):
                return False

            if start_error[0]:
                return False

            self._is_running = True
            return True

        except Exception:
            return False

    def stop(self):
        self._is_running = False
        if self._stream:
            try:
                stop_done = Event()
                self._stream.stopCaptureWithCompletionHandler_(lambda e: stop_done.set())
                stop_done.wait(timeout=2.0)
            except Exception:
                pass
            self._stream = None

    def get_audio(self, timeout: float = 1.0) -> Optional[np.ndarray]:
        try:
            return self.audio_queue.get(timeout=timeout)
        except Empty:
            return None

    @property
    def is_running(self) -> bool:
        return self._is_running


class CombinedAudioCapture:
    """Captures both microphone and system audio.

    Uses default microphone for user's voice and ScreenCaptureKit for system audio.
    """

    def __init__(self, sample_rate: int = SAMPLE_RATE):
        self._sample_rate = sample_rate
        self._channels = CHANNELS
        self.audio_queue: Queue[np.ndarray] = Queue()  # for recording/session
        self.transcriber_queue: Queue[np.ndarray] = Queue()  # dedicated for transcriber
        self.stop_event = Event()
        self._mic_capture: Optional[AudioCapture] = None
        self._system_capture: Optional[ScreenCaptureAudio] = None
        self._has_system_audio = False
        self._output_thread: Optional[Thread] = None

    def start(self):
        self.stop_event.clear()
        self._mic_capture = AudioCapture(sample_rate=self._sample_rate)
        self._mic_capture.start()
        self._channels = self._mic_capture.channels

        self._system_capture = ScreenCaptureAudio(
            sample_rate=self._sample_rate, channels=self._channels
        )
        self._has_system_audio = self._system_capture.start()

        self._output_thread = Thread(target=self._process_audio, daemon=True)
        self._output_thread.start()

    def _process_audio(self):
        mic_buffer: deque = deque(maxlen=self._sample_rate)
        sys_buffer: deque = deque(maxlen=self._sample_rate)
        chunk_size = int(self._sample_rate * 0.02)

        while not self.stop_event.is_set():
            if self._mic_capture:
                mic_audio = self._mic_capture.get_audio(timeout=0.01)
                if mic_audio is not None:
                    mic_buffer.extend(mic_audio.flatten())

            if self._system_capture and self._system_capture.is_running:
                sys_audio = self._system_capture.get_audio(timeout=0.01)
                if sys_audio is not None:
                    sys_buffer.extend(sys_audio.flatten())

            if len(mic_buffer) >= chunk_size:
                mic_chunk = np.array(
                    [mic_buffer.popleft() for _ in range(min(chunk_size, len(mic_buffer)))],
                    dtype=np.float32,
                )

                if self._has_system_audio and len(sys_buffer) >= chunk_size:
                    sys_chunk = np.array(
                        [sys_buffer.popleft() for _ in range(min(chunk_size, len(sys_buffer)))],
                        dtype=np.float32,
                    )
                    min_len = min(len(mic_chunk), len(sys_chunk))
                    if min_len > 0:
                        mixed = np.clip(
                            mic_chunk[:min_len] * 0.7 + sys_chunk[:min_len] * 0.7, -1.0, 1.0
                        )
                        self.audio_queue.put(mixed)
                        self.transcriber_queue.put(mixed.copy())
                    else:
                        self.audio_queue.put(mic_chunk)
                        self.transcriber_queue.put(mic_chunk.copy())
                else:
                    self.audio_queue.put(mic_chunk)
                    self.transcriber_queue.put(mic_chunk.copy())

    def pause_mic(self):
        """Pause microphone stream (for MicProbe). System audio continues."""
        if self._mic_capture:
            self._mic_capture.pause()

    def resume_mic(self):
        """Resume microphone stream after a probe."""
        if self._mic_capture:
            self._mic_capture.resume()

    def stop(self):
        self.stop_event.set()
        if self._mic_capture:
            self._mic_capture.stop()
        if self._system_capture:
            self._system_capture.stop()

    def get_audio(self, timeout: float = 1.0) -> Optional[np.ndarray]:
        """Get audio for recording/session (silence detection, MP3 encoding)."""
        try:
            return self.audio_queue.get(timeout=timeout)
        except Empty:
            return None

    def get_transcriber_audio(self, timeout: float = 0.01) -> Optional[np.ndarray]:
        """Get audio for transcriber (dedicated queue, no contention)."""
        try:
            return self.transcriber_queue.get(timeout=timeout)
        except Empty:
            return None

    @property
    def channels(self) -> int:
        return self._channels

    @property
    def sample_rate(self) -> int:
        return self._sample_rate

    @property
    def has_system_audio(self) -> bool:
        return self._has_system_audio
