import threading
import time
from typing import Callable, Iterable, Optional

from ws_package.log.writer import LogWriter

logger = LogWriter.file("MacCGEventListener")

try:
    import Quartz
except Exception:  # pragma: no cover - environment specific
    Quartz = None


class MacCGEventListener:
    """Minimal CGEventTap-backed listener with pynput-like lifecycle."""

    def __init__(self, event_types: Iterable[int], callback: Callable, name: str):
        self._event_types = list(event_types)
        self._callback = callback
        self._name = name
        self._thread: Optional[threading.Thread] = None
        self._stop_event = threading.Event()
        self._run_loop = None
        self._tap = None
        self._source = None
        self._callback_ref = None
        self.running = False

    def start(self):
        if Quartz is None:
            raise RuntimeError("Quartz module is not available for CGevent listener")
        if self._thread and self._thread.is_alive():
            return
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._run, daemon=True, name=self._name)
        self._thread.start()

        # Give event tap thread a moment to initialize.
        deadline = time.time() + 2.0
        while time.time() < deadline and not self.running and self._thread.is_alive():
            time.sleep(0.05)
        if not self.running:
            raise RuntimeError(f"{self._name} failed to start")

    def stop(self):
        self._stop_event.set()
        self.running = False
        if self._run_loop:
            try:
                Quartz.CFRunLoopStop(self._run_loop)
            except Exception:
                pass
        if self._thread and self._thread.is_alive():
            self._thread.join(timeout=2)

    def _build_event_mask(self) -> int:
        event_mask = 0
        for event_type in self._event_types:
            event_mask |= Quartz.CGEventMaskBit(event_type)
        return event_mask

    def _run(self):
        try:
            event_mask = self._build_event_mask()

            def _event_tap_callback(proxy, event_type, event, refcon):
                if self._stop_event.is_set():
                    return event
                try:
                    self._callback(event_type, event)
                except Exception as callback_error:
                    logger.debug("%s callback error: %s", self._name, callback_error)
                return event

            self._callback_ref = _event_tap_callback
            self._tap = Quartz.CGEventTapCreate(
                Quartz.kCGSessionEventTap,
                Quartz.kCGHeadInsertEventTap,
                Quartz.kCGEventTapOptionListenOnly,
                event_mask,
                self._callback_ref,
                None,
            )
            if not self._tap:
                raise RuntimeError("CGEventTapCreate returned null")

            self._source = Quartz.CFMachPortCreateRunLoopSource(None, self._tap, 0)
            self._run_loop = Quartz.CFRunLoopGetCurrent()
            Quartz.CFRunLoopAddSource(
                self._run_loop,
                self._source,
                Quartz.kCFRunLoopCommonModes,
            )
            Quartz.CGEventTapEnable(self._tap, True)
            self.running = True

            while not self._stop_event.is_set():
                Quartz.CFRunLoopRunInMode(Quartz.kCFRunLoopDefaultMode, 0.2, False)
        except Exception as e:
            logger.error("%s failed: %s", self._name, e, exc_info=True)
            self.running = False
        finally:
            self.running = False
            try:
                if self._tap:
                    Quartz.CGEventTapEnable(self._tap, False)
            except Exception:
                pass
            try:
                if self._source and self._run_loop:
                    Quartz.CFRunLoopRemoveSource(
                        self._run_loop,
                        self._source,
                        Quartz.kCFRunLoopCommonModes,
                    )
            except Exception:
                pass
