"""Platform-specific process and window detection.

Windows: uses ctypes for process listing and foreground window detection.
Non-Windows: graceful degradation (returns empty results).
"""

import asyncio
import ctypes
import ctypes.wintypes
import os
import sys
import threading
from typing import Optional, Set

from wled_controller.utils import get_logger

logger = get_logger(__name__)

_IS_WINDOWS = sys.platform == "win32"


class PlatformDetector:
    """Detect running processes and the foreground window's process."""

    def __init__(self) -> None:
        self._display_on: bool = True
        self._display_listener_started = False
        if _IS_WINDOWS:
            t = threading.Thread(target=self._display_power_listener, daemon=True)
            t.start()

    # ---- Display power state (event-driven) ----

    def _display_power_listener(self) -> None:
        """Background thread: hidden window that receives display power events."""
        try:
            user32 = ctypes.windll.user32

            WNDPROC = ctypes.WINFUNCTYPE(
                ctypes.c_ssize_t,  # LRESULT (64-bit on x64)
                ctypes.wintypes.HWND,
                ctypes.c_uint,
                ctypes.wintypes.WPARAM,
                ctypes.wintypes.LPARAM,
            )

            WM_POWERBROADCAST = 0x0218
            PBT_POWERSETTINGCHANGE = 0x8013

            class POWERBROADCAST_SETTING(ctypes.Structure):
                _fields_ = [
                    ("PowerSetting", ctypes.c_ubyte * 16),  # GUID
                    ("DataLength", ctypes.wintypes.DWORD),
                    ("Data", ctypes.c_ubyte * 1),
                ]

            # GUID_CONSOLE_DISPLAY_STATE = {6FE69556-704A-47A0-8F24-C28D936FDA47}
            GUID_CONSOLE_DISPLAY_STATE = (ctypes.c_ubyte * 16)(
                0x56, 0x95, 0xE6, 0x6F, 0x4A, 0x70, 0xA0, 0x47,
                0x8F, 0x24, 0xC2, 0x8D, 0x93, 0x6F, 0xDA, 0x47,
            )

            user32.DefWindowProcW.argtypes = [
                ctypes.wintypes.HWND, ctypes.c_uint,
                ctypes.wintypes.WPARAM, ctypes.wintypes.LPARAM,
            ]
            user32.DefWindowProcW.restype = ctypes.c_ssize_t

            def wnd_proc(hwnd, msg, wparam, lparam):
                if msg == WM_POWERBROADCAST and wparam == PBT_POWERSETTINGCHANGE:
                    try:
                        setting = ctypes.cast(
                            lparam, ctypes.POINTER(POWERBROADCAST_SETTING)
                        ).contents
                        # Data: 0=off, 1=on, 2=dimmed (treat dimmed as on)
                        value = setting.Data[0]
                        self._display_on = value != 0
                    except Exception as e:
                        logger.debug("Failed to parse display power setting: %s", e)
                        pass
                    return 0
                return user32.DefWindowProcW(hwnd, msg, wparam, lparam)

            wnd_proc_cb = WNDPROC(wnd_proc)

            # Register window class
            class WNDCLASSEXW(ctypes.Structure):
                _fields_ = [
                    ("cbSize", ctypes.c_uint),
                    ("style", ctypes.c_uint),
                    ("lpfnWndProc", WNDPROC),
                    ("cbClsExtra", ctypes.c_int),
                    ("cbWndExtra", ctypes.c_int),
                    ("hInstance", ctypes.wintypes.HINSTANCE),
                    ("hIcon", ctypes.wintypes.HICON),
                    ("hCursor", ctypes.wintypes.HANDLE),
                    ("hbrBackground", ctypes.wintypes.HBRUSH),
                    ("lpszMenuName", ctypes.wintypes.LPCWSTR),
                    ("lpszClassName", ctypes.wintypes.LPCWSTR),
                    ("hIconSm", ctypes.wintypes.HICON),
                ]

            wc = WNDCLASSEXW()
            wc.cbSize = ctypes.sizeof(WNDCLASSEXW)
            wc.lpfnWndProc = wnd_proc_cb
            wc.lpszClassName = "LedGrabDisplayMonitor"
            wc.hInstance = ctypes.windll.kernel32.GetModuleHandleW(None)

            atom = user32.RegisterClassExW(ctypes.byref(wc))
            if not atom:
                logger.warning("Failed to register display monitor window class")
                return

            HWND_MESSAGE = ctypes.wintypes.HWND(-3)
            hwnd = user32.CreateWindowExW(
                0, wc.lpszClassName, "LedGrab Display Monitor",
                0, 0, 0, 0, 0, HWND_MESSAGE, None, wc.hInstance, None,
            )
            if not hwnd:
                logger.warning("Failed to create display monitor hidden window")
                return

            # Register for display power notifications
            user32.RegisterPowerSettingNotification(
                hwnd, ctypes.byref(GUID_CONSOLE_DISPLAY_STATE), 0
            )

            self._display_listener_started = True
            logger.debug("Display power listener started")

            # Message pump
            msg = ctypes.wintypes.MSG()
            while user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
                user32.TranslateMessage(ctypes.byref(msg))
                user32.DispatchMessageW(ctypes.byref(msg))
        except Exception as e:
            logger.error(f"Display power listener failed: {e}")

    def _get_display_power_state_sync(self) -> Optional[str]:
        """Get display power state: 'on' or 'off'. Returns None if unavailable."""
        if not _IS_WINDOWS:
            return None
        return "on" if self._display_on else "off"

    # ---- System idle detection ----

    def _get_idle_seconds_sync(self) -> Optional[float]:
        """Get system idle time in seconds (keyboard/mouse inactivity).

        Returns None if detection is unavailable.
        """
        if not _IS_WINDOWS:
            return None

        try:
            class LASTINPUTINFO(ctypes.Structure):
                _fields_ = [
                    ("cbSize", ctypes.c_uint),
                    ("dwTime", ctypes.c_uint),
                ]

            lii = LASTINPUTINFO()
            lii.cbSize = ctypes.sizeof(LASTINPUTINFO)
            if not ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lii)):
                return None
            millis = ctypes.windll.kernel32.GetTickCount() - lii.dwTime
            return millis / 1000.0
        except Exception as e:
            logger.error(f"Failed to get idle time: {e}")
            return None

    # ---- Process detection ----

    def _get_running_processes_sync(self) -> Set[str]:
        """Get set of lowercase process names via Win32 EnumProcesses.

        Uses PROCESS_QUERY_LIMITED_INFORMATION + QueryFullProcessImageNameW
        which is ~300x faster than WMI (~8ms vs ~3s).  System services
        running under protected accounts are not visible, but all
        user-facing applications are covered.
        """
        if not _IS_WINDOWS:
            return set()

        try:
            psapi = ctypes.windll.psapi
            kernel32 = ctypes.windll.kernel32

            PROCESS_QUERY_LIMITED_INFORMATION = 0x1000

            # Enumerate all PIDs
            pid_array = (ctypes.wintypes.DWORD * 2048)()
            cb_needed = ctypes.wintypes.DWORD()
            psapi.EnumProcesses(
                ctypes.byref(pid_array), ctypes.sizeof(pid_array),
                ctypes.byref(cb_needed),
            )
            n_pids = cb_needed.value // ctypes.sizeof(ctypes.wintypes.DWORD)

            procs: Set[str] = set()
            name_buf = ctypes.create_unicode_buffer(512)

            for i in range(n_pids):
                pid = pid_array[i]
                if pid == 0:
                    continue
                handle = kernel32.OpenProcess(
                    PROCESS_QUERY_LIMITED_INFORMATION, False, pid,
                )
                if not handle:
                    continue
                try:
                    buf_size = ctypes.wintypes.DWORD(512)
                    if kernel32.QueryFullProcessImageNameW(
                        handle, 0, name_buf, ctypes.byref(buf_size),
                    ):
                        procs.add(os.path.basename(name_buf.value).lower())
                finally:
                    kernel32.CloseHandle(handle)

            return procs
        except Exception as e:
            logger.error(f"Failed to enumerate processes: {e}")
            return set()

    def _get_topmost_process_sync(self) -> tuple:
        """Get (process_name, is_fullscreen) of the foreground window.

        Returns (None, False) when detection fails.
        Blocking — call via executor.
        """
        if not _IS_WINDOWS:
            return (None, False)

        try:
            user32 = ctypes.windll.user32
            kernel32 = ctypes.windll.kernel32
            psapi = ctypes.windll.psapi

            hwnd = user32.GetForegroundWindow()
            if not hwnd:
                return (None, False)

            pid = ctypes.wintypes.DWORD()
            user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
            if not pid.value:
                return (None, False)

            PROCESS_QUERY_INFORMATION = 0x0400
            PROCESS_VM_READ = 0x0010
            handle = kernel32.OpenProcess(
                PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid.value
            )
            if not handle:
                return (None, False)

            proc_name = None
            try:
                buf = ctypes.create_unicode_buffer(512)
                psapi.GetModuleFileNameExW(handle, None, buf, 512)
                full_path = buf.value
                if full_path:
                    proc_name = os.path.basename(full_path).lower()
            finally:
                kernel32.CloseHandle(handle)

            if proc_name is None:
                return (None, False)

            # Check if the foreground window covers its entire monitor
            is_fullscreen = self._is_window_fullscreen(user32, hwnd)

            return (proc_name, is_fullscreen)
        except Exception as e:
            logger.error(f"Failed to get foreground process: {e}")
            return (None, False)

    @staticmethod
    def _is_window_fullscreen(user32, hwnd) -> bool:
        """Check whether *hwnd* covers its monitor completely."""
        try:
            # Get window rectangle
            win_rect = ctypes.wintypes.RECT()
            user32.GetWindowRect(hwnd, ctypes.byref(win_rect))

            # Get the monitor this window is on
            MONITOR_DEFAULTTONEAREST = 2
            hmon = user32.MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)
            if not hmon:
                return False

            # MONITORINFO struct
            class MONITORINFO(ctypes.Structure):
                _fields_ = [
                    ("cbSize", ctypes.wintypes.DWORD),
                    ("rcMonitor", ctypes.wintypes.RECT),
                    ("rcWork", ctypes.wintypes.RECT),
                    ("dwFlags", ctypes.wintypes.DWORD),
                ]

            mi = MONITORINFO()
            mi.cbSize = ctypes.sizeof(MONITORINFO)
            if not user32.GetMonitorInfoW(hmon, ctypes.byref(mi)):
                return False

            mr = mi.rcMonitor
            return (
                win_rect.left <= mr.left
                and win_rect.top <= mr.top
                and win_rect.right >= mr.right
                and win_rect.bottom >= mr.bottom
            )
        except Exception as e:
            logger.debug("Fullscreen check failed for hwnd: %s", e)
            return False

    def _get_fullscreen_processes_sync(self) -> Set[str]:
        """Get set of lowercase process names that have a fullscreen window.

        Enumerates all top-level windows and checks each for fullscreen.
        Returns process names (lowercase) whose window covers an entire monitor.
        """
        if not _IS_WINDOWS:
            return set()

        try:
            user32 = ctypes.windll.user32
            kernel32 = ctypes.windll.kernel32
            psapi = ctypes.windll.psapi

            PROCESS_QUERY_INFORMATION = 0x0400
            PROCESS_VM_READ = 0x0010

            fullscreen_procs: Set[str] = set()

            # Callback receives (hwnd, lparam); return True to continue enumeration
            WNDENUMPROC = ctypes.WINFUNCTYPE(
                ctypes.wintypes.BOOL,
                ctypes.wintypes.HWND,
                ctypes.wintypes.LPARAM,
            )

            # Skip the desktop and shell windows (always cover the full monitor)
            desktop_hwnd = user32.GetDesktopWindow()
            shell_hwnd = user32.GetShellWindow()

            # Get shell process PID to filter all explorer desktop windows
            shell_pid = 0
            if shell_hwnd:
                _spid = ctypes.wintypes.DWORD()
                user32.GetWindowThreadProcessId(shell_hwnd, ctypes.byref(_spid))
                shell_pid = _spid.value

            GWL_EXSTYLE = -20
            WS_EX_TOOLWINDOW = 0x00000080
            WS_EX_NOACTIVATE = 0x08000000

            def _enum_callback(hwnd, _lparam):
                # Skip invisible windows
                if not user32.IsWindowVisible(hwnd):
                    return True
                if hwnd == desktop_hwnd:
                    return True

                # Skip tool/overlay/non-activatable windows (system UI, input hosts)
                ex_style = user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
                if ex_style & (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE):
                    return True

                # Quick fullscreen check before expensive process name lookup
                if not self._is_window_fullscreen(user32, hwnd):
                    return True

                # Get PID; skip shell process windows (desktop, taskbar)
                pid = ctypes.wintypes.DWORD()
                user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
                if not pid.value or pid.value == shell_pid:
                    return True

                handle = kernel32.OpenProcess(
                    PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid.value
                )
                if not handle:
                    return True

                try:
                    buf = ctypes.create_unicode_buffer(512)
                    psapi.GetModuleFileNameExW(handle, None, buf, 512)
                    full_path = buf.value
                    if full_path:
                        fullscreen_procs.add(os.path.basename(full_path).lower())
                finally:
                    kernel32.CloseHandle(handle)

                return True

            callback = WNDENUMPROC(_enum_callback)
            user32.EnumWindows(callback, 0)

            return fullscreen_procs
        except Exception as e:
            logger.error(f"Failed to enumerate fullscreen windows: {e}")
            return set()

    async def get_running_processes(self) -> Set[str]:
        """Get set of lowercase process names (async-safe)."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._get_running_processes_sync)

    async def get_topmost_process(self) -> tuple:
        """Get (process_name, is_fullscreen) of the foreground window (async-safe)."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._get_topmost_process_sync)

    async def get_fullscreen_processes(self) -> Set[str]:
        """Get set of process names that have a fullscreen window (async-safe)."""
        loop = asyncio.get_event_loop()
        return await loop.run_in_executor(None, self._get_fullscreen_processes_sync)
