"""Win32 text insertion helpers for keyboard simulation and focus management."""
import ctypes
import ctypes.wintypes
import time

from loguru import logger

INPUT_KEYBOARD = 1
KEYEVENTF_KEYUP = 0x0002
KEYEVENTF_UNICODE = 0x0004

VK_MENU = 0x12  # Alt key
VK_CONTROL = 0x11
VK_SHIFT = 0x10
VK_V = 0x56
VK_RETURN = 0x0D
VK_TAB = 0x09

# ─── Proper Win32 type declarations ──────────────────────────────────────
# Without these, ctypes defaults to c_int (32-bit) for return values,
# which truncates HWND on 64-bit Windows.

_user32 = ctypes.windll.user32
_kernel32 = ctypes.windll.kernel32

_user32.GetForegroundWindow.restype = ctypes.wintypes.HWND
_user32.GetForegroundWindow.argtypes = []

_user32.SetForegroundWindow.restype = ctypes.wintypes.BOOL
_user32.SetForegroundWindow.argtypes = [ctypes.wintypes.HWND]

_user32.BringWindowToTop.restype = ctypes.wintypes.BOOL
_user32.BringWindowToTop.argtypes = [ctypes.wintypes.HWND]

_user32.IsWindow.restype = ctypes.wintypes.BOOL
_user32.IsWindow.argtypes = [ctypes.wintypes.HWND]

_user32.IsIconic.restype = ctypes.wintypes.BOOL
_user32.IsIconic.argtypes = [ctypes.wintypes.HWND]

_user32.ShowWindow.restype = ctypes.wintypes.BOOL
_user32.ShowWindow.argtypes = [ctypes.wintypes.HWND, ctypes.c_int]

_user32.AttachThreadInput.restype = ctypes.wintypes.BOOL
_user32.AttachThreadInput.argtypes = [
    ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.BOOL,
]

_user32.GetWindowThreadProcessId.restype = ctypes.wintypes.DWORD
_user32.GetWindowThreadProcessId.argtypes = [
    ctypes.wintypes.HWND, ctypes.POINTER(ctypes.wintypes.DWORD),
]

_kernel32.GetCurrentThreadId.restype = ctypes.wintypes.DWORD
_kernel32.GetCurrentThreadId.argtypes = []

_user32.keybd_event.restype = None
_user32.keybd_event.argtypes = [
    ctypes.wintypes.BYTE, ctypes.wintypes.BYTE,
    ctypes.wintypes.DWORD, ctypes.wintypes.WPARAM,  # ULONG_PTR
]

SW_RESTORE = 9

# ─── Clipboard API type declarations (x64-safe) ─────────────────────────
_user32.OpenClipboard.restype = ctypes.wintypes.BOOL
_user32.OpenClipboard.argtypes = [ctypes.wintypes.HWND]
_user32.CloseClipboard.restype = ctypes.wintypes.BOOL
_user32.CloseClipboard.argtypes = []
_user32.EmptyClipboard.restype = ctypes.wintypes.BOOL
_user32.EmptyClipboard.argtypes = []
_user32.GetClipboardData.restype = ctypes.c_void_p  # HANDLE
_user32.GetClipboardData.argtypes = [ctypes.wintypes.UINT]
_user32.SetClipboardData.restype = ctypes.c_void_p  # HANDLE
_user32.SetClipboardData.argtypes = [ctypes.wintypes.UINT, ctypes.c_void_p]

_kernel32.GlobalAlloc.restype = ctypes.c_void_p  # HGLOBAL
_kernel32.GlobalAlloc.argtypes = [ctypes.wintypes.UINT, ctypes.c_size_t]
_kernel32.GlobalLock.restype = ctypes.c_void_p    # LPVOID
_kernel32.GlobalLock.argtypes = [ctypes.c_void_p]
_kernel32.GlobalUnlock.restype = ctypes.wintypes.BOOL
_kernel32.GlobalUnlock.argtypes = [ctypes.c_void_p]


class KEYBDINPUT(ctypes.Structure):
    # dwExtraInfo is ULONG_PTR (8 bytes on x64) — c_void_p is the correct type
    _fields_ = [
        ("wVk", ctypes.wintypes.WORD),
        ("wScan", ctypes.wintypes.WORD),
        ("dwFlags", ctypes.wintypes.DWORD),
        ("time", ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.c_void_p),
    ]


class MOUSEINPUT(ctypes.Structure):
    _fields_ = [
        ("dx", ctypes.wintypes.LONG),
        ("dy", ctypes.wintypes.LONG),
        ("mouseData", ctypes.wintypes.DWORD),
        ("dwFlags", ctypes.wintypes.DWORD),
        ("time", ctypes.wintypes.DWORD),
        ("dwExtraInfo", ctypes.c_void_p),
    ]


class HARDWAREINPUT(ctypes.Structure):
    _fields_ = [
        ("uMsg", ctypes.wintypes.DWORD),
        ("wParamL", ctypes.wintypes.WORD),
        ("wParamH", ctypes.wintypes.WORD),
    ]


class INPUT(ctypes.Structure):
    class _INPUT_UNION(ctypes.Union):
        _fields_ = [("ki", KEYBDINPUT), ("mi", MOUSEINPUT), ("hi", HARDWAREINPUT)]
    _fields_ = [
        ("type", ctypes.wintypes.DWORD),
        ("_input", _INPUT_UNION),
    ]


# Declare SendInput argtypes/restype — critical on x64 to avoid pointer truncation
_user32.SendInput.argtypes = [ctypes.wintypes.UINT, ctypes.c_void_p, ctypes.c_int]
_user32.SendInput.restype = ctypes.wintypes.UINT


def _send_inputs(events: list[INPUT]) -> None:
    """Send a batch of INPUT events via SendInput."""
    if not events:
        return
    n = len(events)
    arr = (INPUT * n)(*events)
    sent = _user32.SendInput(n, ctypes.byref(arr), ctypes.sizeof(INPUT))
    if sent != n:
        logger.warning("SendInput: sent {}/{} events", sent, n)


def _make_key_event(vk: int, up: bool = False) -> INPUT:
    """Create a virtual-key INPUT event."""
    evt = INPUT(type=INPUT_KEYBOARD)
    evt._input.ki.wVk = vk
    evt._input.ki.dwFlags = KEYEVENTF_KEYUP if up else 0
    evt._input.ki.dwExtraInfo = 0
    return evt


def release_modifiers() -> None:
    """Send key-up events for Ctrl, Shift, Alt to clear keyboard state.

    Prevents modifier overlap when the hotkey (e.g. Ctrl+Shift+Space)
    is released shortly before Ctrl+V or typing events are sent.
    """
    events = [
        _make_key_event(VK_CONTROL, up=True),
        _make_key_event(VK_SHIFT, up=True),
        _make_key_event(VK_MENU, up=True),
    ]
    _send_inputs(events)
    time.sleep(0.02)  # brief settle time for OS to process key-ups


def type_text(text: str, inter_char_delay: float = 0.005) -> None:
    """Simulate keyboard typing using KEYEVENTF_UNICODE events.

    Each character is sent as a Unicode scancode — works in terminals,
    PuTTY, SSH sessions, and legacy apps where Ctrl+V doesn't work.
    No clipboard access is needed.
    """
    release_modifiers()
    for char in text:
        if char == "\r":
            continue
        if char == "\n":
            events = [
                _make_key_event(VK_RETURN),
                _make_key_event(VK_RETURN, up=True),
            ]
            _send_inputs(events)
        elif char == "\t":
            events = [
                _make_key_event(VK_TAB),
                _make_key_event(VK_TAB, up=True),
            ]
            _send_inputs(events)
        else:
            code = ord(char)
            down = INPUT(type=INPUT_KEYBOARD)
            down._input.ki.wVk = 0
            down._input.ki.wScan = code
            down._input.ki.dwFlags = KEYEVENTF_UNICODE

            up = INPUT(type=INPUT_KEYBOARD)
            up._input.ki.wVk = 0
            up._input.ki.wScan = code
            up._input.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP

            _send_inputs([down, up])
        time.sleep(inter_char_delay)


def send_ctrl_v() -> None:
    """Send Ctrl+V keystroke to paste from clipboard."""
    # Send Ctrl-down first, then V press/release, then Ctrl-up.
    # Split into separate SendInput calls with delays — some apps
    # need the modifier to be fully registered before the key press.
    _send_inputs([_make_key_event(VK_CONTROL)])       # Ctrl down
    time.sleep(0.02)
    _send_inputs([_make_key_event(VK_V)])              # V down
    time.sleep(0.02)
    _send_inputs([_make_key_event(VK_V, up=True)])     # V up
    time.sleep(0.02)
    _send_inputs([_make_key_event(VK_CONTROL, up=True)])  # Ctrl up


# ─── Focus management ────────────────────────────────────────────────────

def get_foreground_window() -> int:
    """Return the HWND of the current foreground window (64-bit safe)."""
    return _user32.GetForegroundWindow() or 0


def force_foreground_window(hwnd: int) -> bool:
    """Reliably bring a window to the foreground on Windows.

    ``SetForegroundWindow`` alone fails silently when the calling process
    is not the foreground process (Windows focus-stealing prevention).
    This function uses the ``AttachThreadInput`` technique to work around
    that restriction.

    Returns True if the target window is now the foreground window.
    """
    if not hwnd or not _user32.IsWindow(hwnd):
        logger.debug("force_foreground_window: invalid hwnd={}", hwnd)
        return False

    # If it's already the foreground window, nothing to do
    current_fg = _user32.GetForegroundWindow()
    if current_fg == hwnd:
        return True

    # Restore if minimized
    if _user32.IsIconic(hwnd):
        _user32.ShowWindow(hwnd, SW_RESTORE)

    # Strategy 1: Direct SetForegroundWindow (works if we are the fg process)
    _user32.SetForegroundWindow(hwnd)
    if _user32.GetForegroundWindow() == hwnd:
        return True

    # Strategy 2: AttachThreadInput trick
    # By attaching our thread's input to the foreground thread, Windows
    # treats us as part of the foreground and allows SetForegroundWindow.
    current_fg = _user32.GetForegroundWindow()
    fg_tid = _user32.GetWindowThreadProcessId(current_fg, None)
    our_tid = _kernel32.GetCurrentThreadId()
    attached = False

    if fg_tid != our_tid:
        attached = bool(_user32.AttachThreadInput(fg_tid, our_tid, True))

    _user32.SetForegroundWindow(hwnd)
    _user32.BringWindowToTop(hwnd)

    if attached:
        _user32.AttachThreadInput(fg_tid, our_tid, False)

    if _user32.GetForegroundWindow() == hwnd:
        return True

    # Strategy 3: Simulate Alt keypress — Windows allows SetForegroundWindow
    # immediately after the user presses a key.
    _user32.keybd_event(VK_MENU, 0, 0, 0)          # Alt down
    _user32.keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0)  # Alt up
    _user32.SetForegroundWindow(hwnd)

    success = _user32.GetForegroundWindow() == hwnd
    if not success:
        logger.warning(
            "force_foreground_window: all strategies failed for hwnd={}",
            hwnd,
        )
    return success


# ─── Clipboard paste ─────────────────────────────────────────────────────

def paste_text(text: str, clipboard_already_set: bool = False) -> None:
    """Insert text at the current cursor position via clipboard paste (Ctrl+V).

    This is the most reliable method — works in all Windows applications.
    If clipboard_already_set is True, skips setting the clipboard.
    Otherwise, saves/restores the previous clipboard content.
    """
    CF_UNICODETEXT = 13
    GMEM_MOVEABLE = 0x0002

    old_clipboard = None

    if not clipboard_already_set:
        # Save current clipboard content
        try:
            if _user32.OpenClipboard(None):
                handle = _user32.GetClipboardData(CF_UNICODETEXT)
                if handle:
                    ptr = _kernel32.GlobalLock(handle)
                    if ptr:
                        old_clipboard = ctypes.wstring_at(ptr)
                        _kernel32.GlobalUnlock(handle)
                _user32.CloseClipboard()
        except Exception:
            old_clipboard = None

        # Set clipboard to new text
        try:
            if _user32.OpenClipboard(None):
                _user32.EmptyClipboard()
                encoded = text.encode("utf-16-le") + b"\x00\x00"
                h = _kernel32.GlobalAlloc(GMEM_MOVEABLE, len(encoded))
                ptr = _kernel32.GlobalLock(h)
                ctypes.memmove(ptr, encoded, len(encoded))
                _kernel32.GlobalUnlock(h)
                _user32.SetClipboardData(CF_UNICODETEXT, h)
                _user32.CloseClipboard()
        except Exception:
            try:
                _user32.CloseClipboard()
            except Exception:
                pass

    # Small delay to ensure clipboard is committed
    time.sleep(0.05)

    # Release any lingering modifiers before sending Ctrl+V
    release_modifiers()

    # Send Ctrl+V
    send_ctrl_v()

    # Restore previous clipboard (after a delay for paste to complete)
    if old_clipboard is not None and not clipboard_already_set:
        time.sleep(0.15)
        try:
            if _user32.OpenClipboard(None):
                _user32.EmptyClipboard()
                encoded = old_clipboard.encode("utf-16-le") + b"\x00\x00"
                h = _kernel32.GlobalAlloc(GMEM_MOVEABLE, len(encoded))
                ptr = _kernel32.GlobalLock(h)
                ctypes.memmove(ptr, encoded, len(encoded))
                _kernel32.GlobalUnlock(h)
                _user32.SetClipboardData(CF_UNICODETEXT, h)
                _user32.CloseClipboard()
        except Exception:
            try:
                _user32.CloseClipboard()
            except Exception:
                pass


# ─── Global Hotkey API (RegisterHotKey) ──────────────────────────────────
# Modifier flags for RegisterHotKey.
MOD_ALT = 0x0001
MOD_CONTROL = 0x0002
MOD_SHIFT = 0x0004
MOD_WIN = 0x0008
MOD_NOREPEAT = 0x4000

# Window messages for hotkey / power / session.
WM_HOTKEY = 0x0312
WM_POWERBROADCAST = 0x0218
WM_WTSSESSION_CHANGE = 0x02B1

# Power broadcast wParams.
PBT_APMSUSPEND = 0x0004
PBT_APMRESUMESUSPEND = 0x0007
PBT_APMRESUMEAUTOMATIC = 0x0012

# WTS session change wParams.
WTS_SESSION_LOCK = 0x7
WTS_SESSION_UNLOCK = 0x8

# Special parent HWND for message-only windows.
HWND_MESSAGE = -3

# Session notification flags.
NOTIFY_FOR_THIS_SESSION = 0

# RegisterHotKey / UnregisterHotKey
_user32.RegisterHotKey.restype = ctypes.wintypes.BOOL
_user32.RegisterHotKey.argtypes = [
    ctypes.wintypes.HWND, ctypes.c_int,
    ctypes.wintypes.UINT, ctypes.wintypes.UINT,
]
_user32.UnregisterHotKey.restype = ctypes.wintypes.BOOL
_user32.UnregisterHotKey.argtypes = [ctypes.wintypes.HWND, ctypes.c_int]

# CreateWindowExW / DestroyWindow for message-only window.
_user32.CreateWindowExW.restype = ctypes.wintypes.HWND
_user32.CreateWindowExW.argtypes = [
    ctypes.wintypes.DWORD,          # dwExStyle
    ctypes.wintypes.LPCWSTR,        # lpClassName
    ctypes.wintypes.LPCWSTR,        # lpWindowName
    ctypes.wintypes.DWORD,          # dwStyle
    ctypes.c_int,                    # X
    ctypes.c_int,                    # Y
    ctypes.c_int,                    # nWidth
    ctypes.c_int,                    # nHeight
    ctypes.wintypes.HWND,           # hWndParent
    ctypes.wintypes.HMENU,          # hMenu
    ctypes.wintypes.HINSTANCE,      # hInstance
    ctypes.c_void_p,                # lpParam
]
_user32.DestroyWindow.restype = ctypes.wintypes.BOOL
_user32.DestroyWindow.argtypes = [ctypes.wintypes.HWND]

# GetAsyncKeyState for hold-release polling.
_user32.GetAsyncKeyState.restype = ctypes.c_short
_user32.GetAsyncKeyState.argtypes = [ctypes.c_int]

# GetLastError.
_kernel32.GetLastError.restype = ctypes.wintypes.DWORD
_kernel32.GetLastError.argtypes = []

# wtsapi32 - session lock/unlock notifications.
_wtsapi32 = ctypes.windll.wtsapi32
_wtsapi32.WTSRegisterSessionNotification.restype = ctypes.wintypes.BOOL
_wtsapi32.WTSRegisterSessionNotification.argtypes = [
    ctypes.wintypes.HWND, ctypes.wintypes.DWORD,
]
_wtsapi32.WTSUnRegisterSessionNotification.restype = ctypes.wintypes.BOOL
_wtsapi32.WTSUnRegisterSessionNotification.argtypes = [ctypes.wintypes.HWND]


def register_hotkey(hwnd: int, hotkey_id: int, mods: int, vk: int) -> bool:
    """Register a global hotkey routed to ``hwnd`` via WM_HOTKEY."""
    return bool(_user32.RegisterHotKey(hwnd, hotkey_id, mods, vk))


def unregister_hotkey(hwnd: int, hotkey_id: int) -> bool:
    """Unregister a previously-registered hotkey."""
    return bool(_user32.UnregisterHotKey(hwnd, hotkey_id))


def create_message_only_window() -> int:
    """Create a message-only window for receiving WM_HOTKEY and WTS/power events.

    Uses the predefined ``STATIC`` window class so no class registration
    is needed.  HWND_MESSAGE makes the window invisible, excluded from
    enumeration, and incapable of receiving user input.
    """
    hwnd = _user32.CreateWindowExW(
        0,                           # dwExStyle
        "STATIC",                    # lpClassName
        "VoxCastHotkeyWin",          # lpWindowName
        0,                           # dwStyle
        0, 0, 0, 0,                  # X, Y, W, H
        HWND_MESSAGE,                # hWndParent (message-only)
        None,                        # hMenu
        None,                        # hInstance
        None,                        # lpParam
    )
    return hwnd or 0


def destroy_window(hwnd: int) -> bool:
    """Destroy a window created via ``create_message_only_window``."""
    if not hwnd:
        return False
    return bool(_user32.DestroyWindow(hwnd))


def is_key_down(vk: int) -> bool:
    """Return True if the virtual key ``vk`` is currently pressed."""
    return bool(_user32.GetAsyncKeyState(vk) & 0x8000)


def register_session_notification(hwnd: int) -> bool:
    """Register ``hwnd`` to receive WM_WTSSESSION_CHANGE events."""
    if not hwnd:
        return False
    return bool(_wtsapi32.WTSRegisterSessionNotification(
        hwnd, NOTIFY_FOR_THIS_SESSION,
    ))


def unregister_session_notification(hwnd: int) -> bool:
    """Stop receiving session notifications for ``hwnd``."""
    if not hwnd:
        return False
    return bool(_wtsapi32.WTSUnRegisterSessionNotification(hwnd))


def get_last_error() -> int:
    """Return the calling thread's last Win32 error code."""
    return int(_kernel32.GetLastError())
