"""Win11 kiosk helpers: keep the pygame/SDL window above Explorer and the taskbar."""
from __future__ import annotations

import ctypes
import re
import time
from ctypes import wintypes

HWND_TOPMOST = -1
SW_SHOW = 5
SWP_SHOWWINDOW = 0x0040
SWP_FRAMECHANGED = 0x0020
MONITOR_DEFAULTTOPRIMARY = 1
MONITORINFOF_PRIMARY = 0x00000001
ASFW_ANY = 0xFFFFFFFF
ABM_SETSTATE = 0x0000000A
ABS_AUTOHIDE = 0x00000001
ABS_ALWAYSONTOP = 0x00000002

_last_refresh = 0.0
_boot_apply_until = 0.0
_taskbar_auto_hidden = False

# Selected target for fullscreen + SetWindowPos (defaults = primary).
_selected_display_index = 0
_selected_bounds: tuple[int, int, int, int] | None = None


class _RECT(ctypes.Structure):
    _fields_ = [
        ("left", ctypes.c_long),
        ("top", ctypes.c_long),
        ("right", ctypes.c_long),
        ("bottom", ctypes.c_long),
    ]


class _MONITORINFO(ctypes.Structure):
    _fields_ = [
        ("cbSize", wintypes.DWORD),
        ("rcMonitor", _RECT),
        ("rcWork", _RECT),
        ("dwFlags", wintypes.DWORD),
    ]


class _APPBARDATA(ctypes.Structure):
    _fields_ = [
        ("cbSize", wintypes.DWORD),
        ("hWnd", wintypes.HWND),
        ("uCallbackMessage", wintypes.UINT),
        ("uEdge", wintypes.UINT),
        ("rc", wintypes.RECT),
        ("lParam", wintypes.LPARAM),
    ]


def list_monitors() -> list[dict]:
    """Return Win32 monitors in EnumDisplayMonitors order (usually matches pygame display index)."""
    user32 = ctypes.windll.user32
    monitors: list[dict] = []

    def _cb(hmon, _hdc, lprect, _lparam):
        info = _MONITORINFO()
        info.cbSize = ctypes.sizeof(_MONITORINFO)
        if not user32.GetMonitorInfoW(hmon, ctypes.byref(info)):
            return 1
        r = info.rcMonitor
        monitors.append(
            {
                "index": len(monitors),
                "hmonitor": int(hmon),
                "left": int(r.left),
                "top": int(r.top),
                "right": int(r.right),
                "bottom": int(r.bottom),
                "width": int(r.right - r.left),
                "height": int(r.bottom - r.top),
                "primary": bool(info.dwFlags & MONITORINFOF_PRIMARY),
            }
        )
        return 1

    MonitorEnumProc = ctypes.WINFUNCTYPE(
        wintypes.BOOL,
        wintypes.HMONITOR,
        wintypes.HDC,
        ctypes.POINTER(_RECT),
        wintypes.LPARAM,
    )
    user32.EnumDisplayMonitors(0, 0, MonitorEnumProc(_cb), 0)
    return monitors


def _primary_monitor_bounds() -> tuple[int, int, int, int]:
    """Return left, top, right, bottom for the primary monitor (includes taskbar area)."""
    for mon in list_monitors():
        if mon["primary"]:
            return mon["left"], mon["top"], mon["right"], mon["bottom"]
    user32 = ctypes.windll.user32
    hmon = user32.MonitorFromPoint(wintypes.POINT(0, 0), MONITOR_DEFAULTTOPRIMARY)
    info = _MONITORINFO()
    info.cbSize = ctypes.sizeof(_MONITORINFO)
    if not user32.GetMonitorInfoW(hmon, ctypes.byref(info)):
        width = user32.GetSystemMetrics(0)
        height = user32.GetSystemMetrics(1)
        return 0, 0, width, height
    rect = info.rcMonitor
    return rect.left, rect.top, rect.right, rect.bottom


def resolve_win32_monitor(
    selector: str | None,
    *,
    prefer_width: int | None = None,
    prefer_height: int | None = None,
) -> tuple[int, tuple[int, int, int, int]]:
    """
    Pick a Win32/pygame display index and bounds.

    selector:
      empty / auto  — if prefer WxH matches exactly one (or any) monitor, use it;
                      else primary
      primary       — primary monitor
      secondary     — first non-primary
      N             — 0-based display index
      1920x360      — first monitor with that pixel size
    """
    monitors = list_monitors()
    if not monitors:
        b = _primary_monitor_bounds()
        return 0, b

    raw = (selector or "").strip().lower()
    if raw in ("", "auto"):
        if prefer_width and prefer_height:
            matches = [
                m
                for m in monitors
                if m["width"] == prefer_width and m["height"] == prefer_height
            ]
            if matches:
                # Prefer non-primary when primary is also the same size (rare).
                pick = next((m for m in matches if not m["primary"]), matches[0])
                return pick["index"], (pick["left"], pick["top"], pick["right"], pick["bottom"])
        primary = next((m for m in monitors if m["primary"]), monitors[0])
        return primary["index"], (
            primary["left"],
            primary["top"],
            primary["right"],
            primary["bottom"],
        )

    if raw == "primary":
        primary = next((m for m in monitors if m["primary"]), monitors[0])
        return primary["index"], (
            primary["left"],
            primary["top"],
            primary["right"],
            primary["bottom"],
        )

    if raw == "secondary":
        pick = next((m for m in monitors if not m["primary"]), None)
        if pick is None:
            pick = monitors[0]
        return pick["index"], (pick["left"], pick["top"], pick["right"], pick["bottom"])

    if raw.isdigit():
        idx = int(raw)
        if 0 <= idx < len(monitors):
            m = monitors[idx]
            return m["index"], (m["left"], m["top"], m["right"], m["bottom"])
        primary = next((m for m in monitors if m["primary"]), monitors[0])
        return primary["index"], (
            primary["left"],
            primary["top"],
            primary["right"],
            primary["bottom"],
        )

    mobj = re.fullmatch(r"(?:match:)?(\d+)x(\d+)", raw)
    if mobj:
        w, h = int(mobj.group(1)), int(mobj.group(2))
        matches = [m for m in monitors if m["width"] == w and m["height"] == h]
        if matches:
            pick = next((m for m in matches if not m["primary"]), matches[0])
            return pick["index"], (pick["left"], pick["top"], pick["right"], pick["bottom"])

    primary = next((m for m in monitors if m["primary"]), monitors[0])
    return primary["index"], (
        primary["left"],
        primary["top"],
        primary["right"],
        primary["bottom"],
    )


def configure_win32_monitor(
    selector: str | None,
    *,
    prefer_width: int | None = None,
    prefer_height: int | None = None,
) -> int:
    """Resolve and remember target monitor; return pygame display index."""
    global _selected_display_index, _selected_bounds
    idx, bounds = resolve_win32_monitor(
        selector, prefer_width=prefer_width, prefer_height=prefer_height
    )
    _selected_display_index = idx
    _selected_bounds = bounds
    mons = list_monitors()
    summary = ", ".join(
        f"{m['index']}:{'P' if m['primary'] else 's'}={m['width']}x{m['height']}+{m['left']}+{m['top']}"
        for m in mons
    )
    left, top, right, bottom = bounds
    print(
        f"win32 monitor: selector={selector!r} -> display={idx} "
        f"{right - left}x{bottom - top}+{left}+{top}  [{summary}]",
        flush=True,
    )
    return idx


def selected_display_index() -> int:
    return _selected_display_index


def _target_monitor_bounds() -> tuple[int, int, int, int]:
    if _selected_bounds is not None:
        return _selected_bounds
    return _primary_monitor_bounds()


def _set_taskbar_auto_hide(enabled: bool) -> None:
    global _taskbar_auto_hidden
    shell32 = ctypes.windll.shell32
    appbar = _APPBARDATA()
    appbar.cbSize = ctypes.sizeof(_APPBARDATA)
    appbar.lParam = ABS_AUTOHIDE if enabled else ABS_ALWAYSONTOP
    shell32.SHAppBarMessage(ABM_SETSTATE, ctypes.byref(appbar))
    _taskbar_auto_hidden = enabled


def _pygame_hwnd():
    try:
        import pygame
    except ImportError:
        return None
    return pygame.display.get_wm_info().get("window")


def _try_set_foreground(hwnd) -> None:
    user32 = ctypes.windll.user32
    kernel32 = ctypes.windll.kernel32
    user32.AllowSetForegroundWindow(ASFW_ANY)
    if user32.SetForegroundWindow(hwnd):
        return
    foreground = user32.GetForegroundWindow()
    if not foreground or foreground == hwnd:
        return
    foreground_thread = user32.GetWindowThreadProcessId(foreground, None)
    current_thread = kernel32.GetCurrentThreadId()
    if foreground_thread and foreground_thread != current_thread:
        user32.AttachThreadInput(foreground_thread, current_thread, True)
        user32.SetForegroundWindow(hwnd)
        user32.AttachThreadInput(foreground_thread, current_thread, False)
    user32.BringWindowToTop(hwnd)


def apply_win32_kiosk_window() -> bool:
    """Cover the selected (or primary) monitor and sit above Explorer + taskbar."""
    hwnd = _pygame_hwnd()
    if not hwnd:
        return False

    user32 = ctypes.windll.user32
    left, top, right, bottom = _target_monitor_bounds()
    width = max(1, right - left)
    height = max(1, bottom - top)
    flags = SWP_SHOWWINDOW | SWP_FRAMECHANGED

    if not _taskbar_auto_hidden:
        _set_taskbar_auto_hide(True)

    user32.ShowWindow(hwnd, SW_SHOW)
    # Toggle topmost so Win11 re-evaluates z-order over the taskbar shell band.
    user32.SetWindowPos(hwnd, HWND_TOPMOST, left, top, width, height, flags)
    user32.SetWindowPos(hwnd, HWND_TOPMOST, left, top, width, height, flags)
    user32.BringWindowToTop(hwnd)
    _try_set_foreground(hwnd)
    return True


def start_win32_kiosk_bootstrap(*, duration_s: float = 8.0) -> None:
    """Re-apply topmost frequently right after startup (HWND may not be ready on first call)."""
    global _boot_apply_until
    _boot_apply_until = time.monotonic() + max(1.0, float(duration_s))
    apply_win32_kiosk_window()


def maybe_refresh_win32_kiosk_window(*, interval_s: float = 2.0) -> None:
    """Re-apply topmost during boot bootstrap and periodically after."""
    global _last_refresh
    now = time.monotonic()
    if now < _boot_apply_until:
        apply_win32_kiosk_window()
        return
    if now - _last_refresh < interval_s:
        return
    _last_refresh = now
    apply_win32_kiosk_window()
