"""Combined audio capture: microphone (sounddevice) + system audio (ScreenCaptureKit).

IMPORTANT: This module must only be imported AFTER daemonize() to prevent
CoreAudio IPC crashes that occur when Python forks after loading audio libs.

Adapted from meeting-noter's CombinedAudioCapture. Key differences:
- Native 16kHz (Whisper-native, no resampling needed)
- Feeds a Queue for the transcriber thread instead of encoding to MP3
"""

from __future__ import annotations

import ctypes
import sys
from collections import deque
from queue import Empty, Queue
from threading import Event, Thread
from typing import Optional

import numpy as np
import sounddevice as sd

# ---------------------------------------------------------------------------
# CoreMedia via ctypes — avoids PyObjC's unsafe CMSampleBuffer marshaling
# which causes SIGSEGV (KERN_INVALID_ADDRESS) when CMBlockBufferGetDataPointer
# return value is converted via PyObjC_CArrayToPython2.
# ---------------------------------------------------------------------------
try:
    _CM = ctypes.CDLL('/System/Library/Frameworks/CoreMedia.framework/CoreMedia')
    _CM.CMSampleBufferGetDataBuffer.restype = ctypes.c_void_p
    _CM.CMSampleBufferGetDataBuffer.argtypes = [ctypes.c_void_p]
    _CM.CMBlockBufferGetDataLength.restype = ctypes.c_size_t
    _CM.CMBlockBufferGetDataLength.argtypes = [ctypes.c_void_p]
    _CM.CMBlockBufferCopyDataBytes.restype = ctypes.c_int32
    _CM.CMBlockBufferCopyDataBytes.argtypes = [
        ctypes.c_void_p,   # CMBlockBufferRef
        ctypes.c_size_t,   # offsetToData
        ctypes.c_size_t,   # dataLength
        ctypes.c_void_p,   # destination
    ]
    _CM_LOADED = True
except OSError:
    _CM_LOADED = False

SAMPLE_RATE = 16000  # Whisper-native sample rate
CHANNELS = 1         # Mono for transcription


# ---------------------------------------------------------------------------
# Microphone capture
# ---------------------------------------------------------------------------

class MicrophoneCapture:
    """Captures audio from the default microphone."""

    def __init__(self, sample_rate: int = SAMPLE_RATE) -> None:
        self.sample_rate = sample_rate
        self.audio_queue: Queue[np.ndarray] = Queue()
        self._stream: Optional[sd.InputStream] = None

    def _find_device(self) -> Optional[int]:
        try:
            default = sd.default.device[0]
            if default is not None and default >= 0:
                info = sd.query_devices(default)
                if info["max_input_channels"] > 0:
                    return default
        except Exception:
            pass
        for i, dev in enumerate(sd.query_devices()):
            if dev["max_input_channels"] > 0:
                name = dev["name"].lower()
                if not any(x in name for x in ("blackhole", "virtual", "aggregate")):
                    return i
        return None

    def start(self) -> None:
        device_idx = self._find_device()
        if device_idx is None:
            raise RuntimeError("No microphone found")

        def _callback(indata, frames, time_info, status):
            self.audio_queue.put(indata[:, 0].copy())  # mono

        self._stream = sd.InputStream(
            device=device_idx,
            channels=1,
            samplerate=self.sample_rate,
            dtype=np.float32,
            blocksize=160,   # 10ms at 16kHz — smaller blocks reach Deepgram faster
            callback=_callback,
        )
        self._stream.start()

    def stop(self) -> None:
        if self._stream:
            self._stream.stop()
            self._stream.close()
            self._stream = None

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


# ---------------------------------------------------------------------------
# System audio via ScreenCaptureKit
# ---------------------------------------------------------------------------

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

    Requires Screen Recording permission in System Settings > Privacy.
    """

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

    def start(self) -> bool:
        if sys.platform != "darwin":
            return False
        try:
            import ScreenCaptureKit as SCK
            import CoreMedia as CM  # still needed for CMTimeMake, CMSampleBufferRef metadata
            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:  # 1 = audio
                        return
                    if not _CM_LOADED:
                        return
                    try:
                        import objc as _objc
                        # Get raw CMSampleBufferRef pointer — bypasses PyObjC type
                        # marshaling which caused SIGSEGV via CArrayToPython2
                        sb_ptr = _objc.pyobjc_id(sampleBuffer)
                        if not sb_ptr:
                            return

                        block_buf = _CM.CMSampleBufferGetDataBuffer(sb_ptr)
                        if not block_buf:
                            return

                        length = _CM.CMBlockBufferGetDataLength(block_buf)
                        if not length or length > 10_000_000:
                            return

                        raw_buf = (ctypes.c_uint8 * length)()
                        if _CM.CMBlockBufferCopyDataBytes(block_buf, 0, length, raw_buf) != 0:
                            return

                        audio = np.frombuffer(raw_buf, dtype=np.float32).copy()
                        if audio.size > 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.ai-interview.screencapture", DISPATCH_QUEUE_SERIAL
                )

            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 or not content.displays():
                return False

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

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

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

            # Register for audio only (type 1) — video frames (type 0) are
            # not needed and would double callback frequency for no benefit
            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]:
                code = start_error[0].code()
                if code == 1003:
                    import logging
                    logging.getLogger(__name__).warning(
                        "Screen Recording permission required for system audio. "
                        "Go to System Settings > Privacy > Screen Recording and enable for Terminal."
                    )
                return False

            self._is_running = True
            return True

        except Exception as exc:
            import logging
            logging.getLogger(__name__).warning("ScreenCaptureKit unavailable: %s", exc)
            return False

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

    def get_audio(self, timeout: float = 0.05) -> 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


# ---------------------------------------------------------------------------
# Combined capture: mic + system audio
# ---------------------------------------------------------------------------

class CombinedAudioCapture:
    """Mixes microphone + system audio and feeds a Queue for the transcriber."""

    def __init__(self, sample_rate: int = SAMPLE_RATE) -> None:
        self.sample_rate = sample_rate
        self.audio_queue: Queue[np.ndarray] = Queue()
        self._stop_event = Event()
        self._mic: Optional[MicrophoneCapture] = None
        self._sys: Optional[ScreenCaptureAudio] = None
        self._has_system_audio = False
        self._mix_thread: Optional[Thread] = None

    def start(self) -> None:
        self._stop_event.clear()

        self._mic = MicrophoneCapture(sample_rate=self.sample_rate)
        self._mic.start()

        self._sys = ScreenCaptureAudio(sample_rate=self.sample_rate)
        self._has_system_audio = self._sys.start()

        import logging
        log = logging.getLogger(__name__)
        if self._has_system_audio:
            log.info("Audio capture: microphone + system audio (ScreenCaptureKit)")
        else:
            log.info("Audio capture: microphone only (grant Screen Recording for system audio)")

        self._mix_thread = Thread(target=self._mix_loop, daemon=True)
        self._mix_thread.start()

    def _mix_loop(self) -> None:
        """Feed system audio (speaker only) to the transcriber queue.

        When system audio is available, mic is intentionally excluded —
        only the interviewer's voice should be transcribed automatically.
        The microphone is only used for manual recordings (2x ESC hold).
        Falls back to mic-only if system audio is unavailable.
        """
        chunk_size = int(self.sample_rate * 0.01)  # 10ms — forward audio to Deepgram faster
        mic_buf: deque = deque(maxlen=self.sample_rate)
        sys_buf: deque = deque(maxlen=self.sample_rate)

        while not self._stop_event.is_set():
            # Always drain mic queue to prevent buffer buildup, but discard when system audio is active
            mic_chunk = self._mic.get_audio(timeout=0.02) if self._mic else None
            if mic_chunk is not None and not self._has_system_audio:
                mic_buf.extend(mic_chunk.flatten())

            if self._sys and self._sys.is_running:
                sys_chunk = self._sys.get_audio(timeout=0.02)
                if sys_chunk is not None:
                    sys_buf.extend(sys_chunk.flatten())

            if self._has_system_audio:
                # System audio only — speaker voice, no mic bleed
                if len(sys_buf) >= chunk_size:
                    sys_data = np.array(
                        [sys_buf.popleft() for _ in range(min(chunk_size, len(sys_buf)))],
                        dtype=np.float32,
                    )
                    self.audio_queue.put(sys_data)
            else:
                # Fallback: mic only (no system audio available)
                if len(mic_buf) >= chunk_size:
                    mic_data = np.array(
                        [mic_buf.popleft() for _ in range(min(chunk_size, len(mic_buf)))],
                        dtype=np.float32,
                    )
                    self.audio_queue.put(mic_data)

    def stop(self) -> None:
        self._stop_event.set()
        if self._mic:
            self._mic.stop()
        if self._sys:
            self._sys.stop()

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