"""Auto-detect meetings via CoreAudio microphone state + window identification.

Primary trigger: CoreAudio kAudioDevicePropertyDeviceIsRunningSomewhere
detects when another app activates the mic (works for ANY meeting app).
Window scanning is used only to identify the app name for labeling.

Adapted from meeting-noter's mic_monitor.py.
"""

from __future__ import annotations

import ctypes
from ctypes import POINTER, Structure, byref, c_int32, c_uint32
import re
import time
from dataclasses import dataclass
from typing import Optional


@dataclass
class MeetingInfo:
    app_name: str
    meeting_name: Optional[str]
    window_title: str
    is_active: bool = True


# ---------------------------------------------------------------------------
# CoreAudio mic-in-use detection
# ---------------------------------------------------------------------------

_kAudioObjectSystemObject = 1
_kAudioHardwarePropertyDefaultInputDevice = 1682533920  # 'dIn '
_kAudioDevicePropertyDeviceIsRunningSomewhere = 1735356005  # 'gone'
_kAudioObjectPropertyScopeGlobal = 1735159650  # 'glob'
_kAudioObjectPropertyElementMain = 0


class _AudioObjectPropertyAddress(Structure):
    _fields_ = [
        ("mSelector", c_uint32),
        ("mScope", c_uint32),
        ("mElement", c_uint32),
    ]


_core_audio = None
_AudioObjectGetPropertyDataSize = None
_AudioObjectGetPropertyData = None
_coreaudio_init_time = 0
_COREAUDIO_REFRESH_INTERVAL = 30 * 60


def _reset_coreaudio():
    global _core_audio, _AudioObjectGetPropertyDataSize, _AudioObjectGetPropertyData, _coreaudio_init_time
    _core_audio = None
    _AudioObjectGetPropertyDataSize = None
    _AudioObjectGetPropertyData = None
    _coreaudio_init_time = 0


def _init_coreaudio():
    global _core_audio, _AudioObjectGetPropertyDataSize, _AudioObjectGetPropertyData, _coreaudio_init_time
    if _core_audio is not None:
        return True
    try:
        _core_audio = ctypes.CDLL("/System/Library/Frameworks/CoreAudio.framework/CoreAudio")

        _AudioObjectGetPropertyDataSize = _core_audio.AudioObjectGetPropertyDataSize
        _AudioObjectGetPropertyDataSize.argtypes = [
            c_uint32, POINTER(_AudioObjectPropertyAddress), c_uint32, ctypes.c_void_p, POINTER(c_uint32),
        ]
        _AudioObjectGetPropertyDataSize.restype = c_int32

        _AudioObjectGetPropertyData = _core_audio.AudioObjectGetPropertyData
        _AudioObjectGetPropertyData.argtypes = [
            c_uint32, POINTER(_AudioObjectPropertyAddress), c_uint32, ctypes.c_void_p, POINTER(c_uint32), ctypes.c_void_p,
        ]
        _AudioObjectGetPropertyData.restype = c_int32

        _coreaudio_init_time = time.time()
        return True
    except Exception:
        return False


def _maybe_refresh_coreaudio():
    if _core_audio is not None and time.time() - _coreaudio_init_time > _COREAUDIO_REFRESH_INTERVAL:
        _reset_coreaudio()


def is_mic_in_use_by_another_app() -> bool:
    """Check if the microphone is being used by another application."""
    _maybe_refresh_coreaudio()
    if not _init_coreaudio():
        return False
    try:
        addr = _AudioObjectPropertyAddress(
            _kAudioHardwarePropertyDefaultInputDevice,
            _kAudioObjectPropertyScopeGlobal,
            _kAudioObjectPropertyElementMain,
        )
        size = c_uint32(4)
        device_id = c_uint32(0)
        err = _AudioObjectGetPropertyData(
            _kAudioObjectSystemObject, byref(addr), 0, None, byref(size), byref(device_id),
        )
        if err != 0 or device_id.value == 0:
            return False

        addr_running = _AudioObjectPropertyAddress(
            _kAudioDevicePropertyDeviceIsRunningSomewhere,
            _kAudioObjectPropertyScopeGlobal,
            _kAudioObjectPropertyElementMain,
        )
        is_running = c_uint32(0)
        size = c_uint32(4)
        err = _AudioObjectGetPropertyData(
            device_id.value, byref(addr_running), 0, None, byref(size), byref(is_running),
        )
        return err == 0 and is_running.value != 0
    except Exception:
        return False


# ---------------------------------------------------------------------------
# Window-based app identification (for labeling, not for triggering)
# ---------------------------------------------------------------------------

_SYSTEM_APPS = {
    "finder", "system preferences", "system settings",
    "terminal", "iterm", "iterm2", "activity monitor", "console",
    "xcode", "visual studio code", "code", "cursor", "pycharm",
    "warp", "alacritty", "kitty",
}

_BROWSERS = {"chrome", "safari", "firefox", "edge", "brave", "arc", "google chrome"}

_AUXILIARY_PATTERNS = [
    "select ", "choose ", "settings", "preferences",
    "menu window", "notification", "healthcheck",
    "floating video", "media stream", "waiting room",
    "breakout room", " - not started",
]

_AUXILIARY_SINGLE_WORDS = {
    "menu", "login", "settings", "preferences", "options",
    "notifications", "chat", "participants", "transcript",
}


def _is_auxiliary_title(title: str) -> bool:
    if not title or not title.strip():
        return True
    t = title.lower().strip()
    if t in _AUXILIARY_SINGLE_WORDS:
        return True
    for p in _AUXILIARY_PATTERNS:
        if p in t:
            return True
    if title.startswith(("ZM_", "zm_", "_")):
        return True
    return False


def _identify_meeting_app() -> Optional[MeetingInfo]:
    """Scan visible windows to identify which meeting app is active.

    Returns MeetingInfo with app name and window title, or None.
    """
    try:
        import Quartz
        from AppKit import NSWorkspace
    except ImportError:
        return None

    windows = Quartz.CGWindowListCopyWindowInfo(
        Quartz.kCGWindowListOptionOnScreenOnly | Quartz.kCGWindowListExcludeDesktopElements,
        Quartz.kCGNullWindowID,
    )

    # Collect visible windows per app
    app_windows: dict[str, list[str]] = {}
    for w in windows:
        owner = w.get(Quartz.kCGWindowOwnerName, "")
        title = w.get(Quartz.kCGWindowName, "")
        layer = w.get(Quartz.kCGWindowLayer, 0)
        if not owner or not title or layer != 0:
            continue
        if _is_auxiliary_title(title):
            continue
        app_windows.setdefault(owner, []).append(title)

    # First pass: non-browser, non-system apps (Zoom, Teams, FaceTime, Webex, etc.)
    for app_name, titles in app_windows.items():
        app_lower = app_name.lower()
        if app_lower in _SYSTEM_APPS:
            continue
        if any(b in app_lower for b in _BROWSERS):
            continue
        title = titles[0]
        meeting_name = _clean_meeting_title(title)
        return MeetingInfo(
            app_name=app_name,
            meeting_name=meeting_name,
            window_title=title,
        )

    # Second pass: browser-based meetings (Meet, Teams web, etc.)
    for app_name, titles in app_windows.items():
        app_lower = app_name.lower()
        if not any(b in app_lower for b in _BROWSERS):
            continue
        for title in titles:
            t = title.lower()
            if any(kw in t for kw in ["meet", "meeting", "call", "conference", "huddle"]):
                meeting_name = _clean_meeting_title(title)
                return MeetingInfo(
                    app_name=app_name,
                    meeting_name=meeting_name,
                    window_title=title,
                )

    return None


def _clean_meeting_title(title: str) -> str:
    title = title.strip()
    if title.lower().startswith("meet - "):
        title = "Meet_" + title[7:]
        title = re.sub(r"[🔊🔇📹]", "", title).strip()
    if title.lower().startswith("zoom meeting - "):
        title = title[15:]
    if title.lower().endswith(" - zoom"):
        title = title[:-7]
    if " | Microsoft Teams" in title:
        title = title.split(" | Microsoft Teams")[0]
    title = re.sub(r'[<>:"/\\|?*]', "_", title)
    title = re.sub(r"[\s_]+", "_", title)
    if len(title) > 50:
        title = title[:50]
    return title.strip("_")


# ---------------------------------------------------------------------------
# MeetingMonitor — the main detection class used by MeetingSession
# ---------------------------------------------------------------------------

class MeetingMonitor:
    """Monitor for active meetings using CoreAudio mic state + window scanning.

    Start detection: CoreAudio reports another app is using the mic
                     (debounced: 2 consecutive readings, polled every 2s).
    App identification: window scan to identify which app and meeting name.
    """

    def __init__(self):
        self.last_meeting: Optional[MeetingInfo] = None
        self._was_in_meeting = False
        self._on_count = 0
        self._on_threshold = 2  # require 2 consecutive "mic on" readings

    def check(self) -> tuple[bool, bool, Optional[MeetingInfo]]:
        """Check for meeting status change.

        Returns (meeting_started, meeting_ended, meeting_info).
        """
        if self._was_in_meeting:
            # While in a meeting, end detection is handled by MicProbe + silence
            # in session.py, not here. We just return the current state.
            return False, False, self.last_meeting

        # Not in a meeting — check if mic was activated by another app
        mic_active = is_mic_in_use_by_another_app()

        if mic_active:
            self._on_count += 1
            if self._on_count >= self._on_threshold:
                # Mic confirmed active — identify the app
                info = _identify_meeting_app()
                if info is None:
                    # Mic is active but can't identify app — use generic name
                    info = MeetingInfo(
                        app_name="Unknown",
                        meeting_name=None,
                        window_title="",
                    )
                self.last_meeting = info
                self._was_in_meeting = True
                self._on_count = 0
                return True, False, info
        else:
            self._on_count = 0

        return False, False, None

    def set_meeting_ended(self):
        """Called by MeetingSession when the meeting ends."""
        self._was_in_meeting = False
        self.last_meeting = None
        self._on_count = 0

    def is_in_meeting(self) -> bool:
        return self._was_in_meeting
