"""Windows OS-shutdown handler.

Without this, ``pythonw.exe -m ledgrab`` is force-terminated by Windows at
PC shutdown/restart/logoff: the FastAPI ``lifespan`` shutdown hook never
runs, so the ``stop_targets`` setting is silently ignored and the SQLite
WAL is never checkpointed (the user's most recent edits roll back on the
next boot).

How it works
------------

The fix is a hidden, top-level window in a daemon thread:

* Top-level — message-only windows (``HWND_MESSAGE``) do **not** receive
  shutdown broadcasts. We need a real top-level window. We make it
  invisible by never calling ``ShowWindow`` and by using
  ``WS_EX_TOOLWINDOW`` (no taskbar entry, no Alt-Tab).
* ``WM_QUERYENDSESSION`` arrives first. We immediately call
  ``ShutdownBlockReasonCreate`` so Windows shows our reason on the
  shutdown UI **and** extends its hung-app timeout (default ~5 s) up to
  the GUI ceiling (~20 s). We then trigger the caller's shutdown
  callback (which sets ``uvicorn.Server.should_exit``).
* ``WM_ENDSESSION`` arrives second. We wait on a ``threading.Event``
  that the FastAPI lifespan sets when it is fully torn down (including
  the DB checkpoint), then destroy the block reason and return.
* ``SetProcessShutdownParameters`` raises our shutdown priority so we
  are notified before non-system apps.

This module is a no-op on non-Windows platforms.
"""

from __future__ import annotations

import ctypes
import logging
import threading
from ctypes import wintypes
from typing import Callable

from ledgrab.utils.platform import is_windows

logger = logging.getLogger(__name__)


# -- Win32 constants ---------------------------------------------------------

_WM_QUERYENDSESSION = 0x0011
_WM_ENDSESSION = 0x0016
_WM_CLOSE = 0x0010
_WM_DESTROY = 0x0002

_CS_HREDRAW = 0x0002
_CS_VREDRAW = 0x0001

_WS_OVERLAPPED = 0x00000000
_WS_EX_TOOLWINDOW = 0x00000080
_WS_EX_NOACTIVATE = 0x08000000

# SetProcessShutdownParameters: higher value = notified earlier. The default
# for user apps is 0x280. We use 0x300 so we shut down before normal apps but
# still inside the user-app range (0x100–0x3FF).
_SHUTDOWN_PRIORITY = 0x300

# Bound how long we wait inside WM_ENDSESSION for the lifespan to finish.
# Windows' GUI-app ceiling (with a shutdown block reason set) is 20 s; leave
# a safety margin so we always destroy the block reason and return cleanly.
_SHUTDOWN_WAIT_SECONDS = 18.0


# -- Win32 typedefs ----------------------------------------------------------

# LRESULT / LPARAM are LONG_PTR (signed, pointer-sized: 32-bit on x86, 64-bit
# on x64). wintypes.LPARAM is already c_ssize_t, but there is no LRESULT in
# wintypes — we define it here so the WNDPROC return value is the right size.
LRESULT = ctypes.c_ssize_t


# -- WindowProc signature ----------------------------------------------------

_WNDPROC = ctypes.WINFUNCTYPE(
    LRESULT,
    wintypes.HWND,
    wintypes.UINT,
    wintypes.WPARAM,
    wintypes.LPARAM,
)


class _WNDCLASS(ctypes.Structure):
    _fields_ = [
        ("style", wintypes.UINT),
        ("lpfnWndProc", _WNDPROC),
        ("cbClsExtra", ctypes.c_int),
        ("cbWndExtra", ctypes.c_int),
        ("hInstance", wintypes.HINSTANCE),
        ("hIcon", wintypes.HICON),
        ("hCursor", wintypes.HANDLE),
        ("hbrBackground", wintypes.HBRUSH),
        ("lpszMenuName", wintypes.LPCWSTR),
        ("lpszClassName", wintypes.LPCWSTR),
    ]


class _MSG(ctypes.Structure):
    _fields_ = [
        ("hwnd", wintypes.HWND),
        ("message", wintypes.UINT),
        ("wParam", wintypes.WPARAM),
        ("lParam", wintypes.LPARAM),
        ("time", wintypes.DWORD),
        ("pt_x", wintypes.LONG),
        ("pt_y", wintypes.LONG),
    ]


def _bind_winapi() -> None:
    """Declare argtypes/restype for every Win32 function we call.

    Without these, ctypes treats integer args as ``c_int`` (32-bit) — which
    silently overflows when Windows passes a 64-bit LPARAM into our
    WindowProc and we hand it back to ``DefWindowProcW``. The result is
    ``OverflowError: int too long to convert`` and the message is dropped.
    Binding types once at import is the safe, idempotent fix.
    """
    user32 = ctypes.windll.user32
    kernel32 = ctypes.windll.kernel32

    user32.DefWindowProcW.argtypes = [
        wintypes.HWND,
        wintypes.UINT,
        wintypes.WPARAM,
        wintypes.LPARAM,
    ]
    user32.DefWindowProcW.restype = LRESULT

    user32.GetMessageW.argtypes = [
        ctypes.POINTER(_MSG),
        wintypes.HWND,
        wintypes.UINT,
        wintypes.UINT,
    ]
    user32.GetMessageW.restype = wintypes.BOOL

    user32.TranslateMessage.argtypes = [ctypes.POINTER(_MSG)]
    user32.TranslateMessage.restype = wintypes.BOOL

    user32.DispatchMessageW.argtypes = [ctypes.POINTER(_MSG)]
    user32.DispatchMessageW.restype = LRESULT

    user32.PostMessageW.argtypes = [
        wintypes.HWND,
        wintypes.UINT,
        wintypes.WPARAM,
        wintypes.LPARAM,
    ]
    user32.PostMessageW.restype = wintypes.BOOL

    user32.PostQuitMessage.argtypes = [ctypes.c_int]
    user32.PostQuitMessage.restype = None

    user32.RegisterClassW.argtypes = [ctypes.POINTER(_WNDCLASS)]
    user32.RegisterClassW.restype = wintypes.ATOM

    user32.CreateWindowExW.argtypes = [
        wintypes.DWORD,
        wintypes.LPCWSTR,
        wintypes.LPCWSTR,
        wintypes.DWORD,
        ctypes.c_int,
        ctypes.c_int,
        ctypes.c_int,
        ctypes.c_int,
        wintypes.HWND,
        wintypes.HMENU,
        wintypes.HINSTANCE,
        wintypes.LPVOID,
    ]
    user32.CreateWindowExW.restype = wintypes.HWND

    user32.ShutdownBlockReasonCreate.argtypes = [wintypes.HWND, wintypes.LPCWSTR]
    user32.ShutdownBlockReasonCreate.restype = wintypes.BOOL

    user32.ShutdownBlockReasonDestroy.argtypes = [wintypes.HWND]
    user32.ShutdownBlockReasonDestroy.restype = wintypes.BOOL

    kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]
    kernel32.GetModuleHandleW.restype = wintypes.HMODULE

    kernel32.SetProcessShutdownParameters.argtypes = [wintypes.DWORD, wintypes.DWORD]
    kernel32.SetProcessShutdownParameters.restype = wintypes.BOOL


if is_windows():
    _bind_winapi()


class WindowsShutdownGuard:
    """Catches Windows session-end notifications and runs a cleanup callback.

    Usage::

        complete = threading.Event()
        guard = WindowsShutdownGuard(
            on_shutdown=lambda: server.should_exit_set_true(),
            shutdown_complete=complete,
        )
        guard.start()
        ...  # app runs
        # When your cleanup is finished, signal so Windows can finish ending the session.
        complete.set()
    """

    def __init__(
        self,
        on_shutdown: Callable[[], None],
        shutdown_complete: threading.Event,
        app_name: str = "LED Grab",
        wait_seconds: float = _SHUTDOWN_WAIT_SECONDS,
    ) -> None:
        self._on_shutdown = on_shutdown
        self._shutdown_complete = shutdown_complete
        self._app_name = app_name
        self._wait_seconds = wait_seconds

        self._hwnd: wintypes.HWND | None = None
        self._thread: threading.Thread | None = None
        self._ready = threading.Event()
        self._fired = False  # idempotency: only trigger on_shutdown once

        # We must keep a strong reference to the WindowProc callback for the
        # entire lifetime of the window — Win32 stores a raw pointer and
        # ctypes will free it the moment the Python object is GC'd, which
        # would crash inside the message pump.
        self._wndproc_ref = _WNDPROC(self._wndproc)

    # -- public API ----------------------------------------------------------

    def start(self) -> bool:
        """Start the shutdown-guard thread. Returns False on non-Windows."""
        if not is_windows():
            return False

        try:
            self._raise_shutdown_priority()
        except Exception as e:  # pragma: no cover - best-effort
            logger.warning("SetProcessShutdownParameters failed: %s", e)

        self._thread = threading.Thread(
            target=self._run,
            name="windows-shutdown-guard",
            daemon=True,
        )
        self._thread.start()

        # Wait briefly so callers can rely on the window existing before they
        # return from start(). If the thread fails to create the window we
        # still return — the failure is logged from inside the thread.
        self._ready.wait(timeout=2.0)
        return self._hwnd is not None

    def stop(self) -> None:
        """Tear down the hidden window. Safe to call from any thread."""
        hwnd = self._hwnd
        if hwnd is None:
            return
        try:
            ctypes.windll.user32.PostMessageW(hwnd, _WM_CLOSE, 0, 0)
        except Exception:  # pragma: no cover - shutdown best-effort
            pass

    # -- internals -----------------------------------------------------------

    @staticmethod
    def _raise_shutdown_priority() -> None:
        kernel32 = ctypes.windll.kernel32
        if not kernel32.SetProcessShutdownParameters(_SHUTDOWN_PRIORITY, 0):
            # Failure is benign — the WM_ENDSESSION path still works without
            # an elevated priority, we'd just be notified slightly later in
            # the shutdown ordering. Don't even warn.
            err = ctypes.get_last_error()
            logger.debug("SetProcessShutdownParameters returned 0 (err=%d)", err)

    def _run(self) -> None:
        try:
            self._create_window()
        except Exception:
            logger.exception("Failed to create Windows shutdown-guard window")
            self._ready.set()
            return

        self._ready.set()
        self._pump_messages()

    def _create_window(self) -> None:
        user32 = ctypes.windll.user32
        kernel32 = ctypes.windll.kernel32

        h_instance = kernel32.GetModuleHandleW(None)
        class_name = f"LedGrabShutdownGuard_{id(self)}"

        wc = _WNDCLASS()
        wc.style = _CS_HREDRAW | _CS_VREDRAW
        wc.lpfnWndProc = self._wndproc_ref
        wc.cbClsExtra = 0
        wc.cbWndExtra = 0
        wc.hInstance = h_instance
        wc.hIcon = None
        wc.hCursor = None
        wc.hbrBackground = None
        wc.lpszMenuName = None
        wc.lpszClassName = class_name

        atom = user32.RegisterClassW(ctypes.byref(wc))
        if not atom:
            err = ctypes.get_last_error()
            raise OSError(f"RegisterClassW failed (err={err})")

        # Top-level (parent=NULL), but invisible and off-screen, no taskbar
        # entry, and not focusable. Crucially NOT HWND_MESSAGE — message-only
        # windows do not receive shutdown broadcasts.
        hwnd = user32.CreateWindowExW(
            _WS_EX_TOOLWINDOW | _WS_EX_NOACTIVATE,
            class_name,
            self._app_name,
            _WS_OVERLAPPED,
            -32000,  # off-screen x
            -32000,  # off-screen y
            0,  # width
            0,  # height
            None,  # parent
            None,  # menu
            h_instance,
            None,  # lpParam
        )
        if not hwnd:
            err = ctypes.get_last_error()
            raise OSError(f"CreateWindowExW failed (err={err})")

        self._hwnd = hwnd
        logger.info("Windows shutdown guard ready (hwnd=%s)", hwnd)

    def _pump_messages(self) -> None:
        user32 = ctypes.windll.user32
        msg = _MSG()
        while True:
            ret = user32.GetMessageW(ctypes.byref(msg), None, 0, 0)
            if ret == 0:  # WM_QUIT
                break
            if ret == -1:  # error
                logger.error("GetMessageW returned -1; exiting shutdown guard")
                break
            user32.TranslateMessage(ctypes.byref(msg))
            user32.DispatchMessageW(ctypes.byref(msg))

    # -- WindowProc ----------------------------------------------------------

    def _wndproc(
        self,
        hwnd: wintypes.HWND,
        msg: wintypes.UINT,
        wparam: wintypes.WPARAM,
        lparam: wintypes.LPARAM,
    ) -> int:
        user32 = ctypes.windll.user32

        if msg == _WM_QUERYENDSESSION:
            logger.warning("WM_QUERYENDSESSION received (lParam=0x%x) — beginning shutdown", lparam)
            self._begin_shutdown(hwnd)
            return 1  # TRUE — allow the session to end

        if msg == _WM_ENDSESSION:
            logger.warning("WM_ENDSESSION received (wParam=%s)", wparam)
            # wParam=0 means the session was cancelled; nothing to clean up.
            if wparam:
                self._wait_for_cleanup(hwnd)
            return 0

        if msg == _WM_DESTROY:
            user32.PostQuitMessage(0)
            return 0

        return user32.DefWindowProcW(hwnd, msg, wparam, lparam)

    def _begin_shutdown(self, hwnd: wintypes.HWND) -> None:
        if self._fired:
            return
        self._fired = True

        try:
            ctypes.windll.user32.ShutdownBlockReasonCreate(
                hwnd,
                f"{self._app_name} is stopping LED targets and saving settings…",
            )
        except Exception as e:  # pragma: no cover - best-effort
            logger.warning("ShutdownBlockReasonCreate failed: %s", e)

        try:
            self._on_shutdown()
        except Exception:
            logger.exception("on_shutdown callback raised")

    def _wait_for_cleanup(self, hwnd: wintypes.HWND) -> None:
        try:
            completed = self._shutdown_complete.wait(timeout=self._wait_seconds)
            if not completed:
                logger.error(
                    "Shutdown cleanup did not complete within %.0fs — proceeding anyway",
                    self._wait_seconds,
                )
        finally:
            try:
                ctypes.windll.user32.ShutdownBlockReasonDestroy(hwnd)
            except Exception:  # pragma: no cover - best-effort
                pass


__all__ = ["WindowsShutdownGuard"]
