"""Windows text input emulation via SendInput with KEYEVENTF_UNICODE.

Supports ALL Unicode characters: Latin, Cyrillic, CJK, Arabic, etc.
Each character is injected as a Unicode key event — no layout dependency.
"""

from __future__ import annotations

import ctypes
import ctypes.wintypes
import logging
import time

log = logging.getLogger(__name__)

# SendInput constants
_INPUT_KEYBOARD = 1
_KEYEVENTF_UNICODE = 0x0004
_KEYEVENTF_KEYUP = 0x0002


# Win32 structures
class _KEYBDINPUT(ctypes.Structure):
    _fields_ = [
        ("wVk", ctypes.c_ushort),
        ("wScan", ctypes.c_ushort),
        ("dwFlags", ctypes.c_ulong),
        ("time", ctypes.c_ulong),
        ("dwExtraInfo", ctypes.POINTER(ctypes.c_ulong)),
    ]


class _INPUT_UNION(ctypes.Union):
    _fields_ = [
        ("ki", _KEYBDINPUT),
        ("_pad", ctypes.c_byte * 32),
    ]


class _INPUT(ctypes.Structure):
    _fields_ = [
        ("type", ctypes.c_ulong),
        ("_input", _INPUT_UNION),
    ]


_user32 = ctypes.windll.user32  # type: ignore[attr-defined]
_INPUT_SIZE = ctypes.sizeof(_INPUT)


def _make_unicode_input(char_code: int, keyup: bool = False) -> _INPUT:
    """Create an INPUT structure for a Unicode character event."""
    flags = _KEYEVENTF_UNICODE | (_KEYEVENTF_KEYUP if keyup else 0)
    ki = _KEYBDINPUT(wVk=0, wScan=char_code, dwFlags=flags, time=0)
    ui = _INPUT_UNION(ki=ki)
    return _INPUT(type=_INPUT_KEYBOARD, _input=ui)


def type_char(char: str) -> None:
    """Inject a single Unicode character into the active window.

    Works for all characters regardless of keyboard layout.
    For surrogate pairs (emoji, CJK Extension B), injects both surrogates.
    """
    code = ord(char)

    if code > 0xFFFF:
        # Encode as UTF-16 surrogate pair
        code -= 0x10000
        high = 0xD800 + (code >> 10)
        low = 0xDC00 + (code & 0x3FF)
        inputs = (
            _make_unicode_input(high),
            _make_unicode_input(low),
            _make_unicode_input(low, keyup=True),
            _make_unicode_input(high, keyup=True),
        )
        arr = (_INPUT * 4)(*inputs)
        _user32.SendInput(4, arr, _INPUT_SIZE)
    else:
        inputs2 = (
            _make_unicode_input(code),
            _make_unicode_input(code, keyup=True),
        )
        arr = (_INPUT * 2)(*inputs2)
        _user32.SendInput(2, arr, _INPUT_SIZE)


def type_text(text: str, delay_ms: int = 30) -> None:
    """Type *text* character by character with *delay_ms* between each.

    Use delay_ms=0 for maximum speed (may drop characters in some apps).
    Recommended: 20-50ms for most applications.
    """
    delay = delay_ms / 1000.0
    for char in text:
        type_char(char)
        if delay > 0:
            time.sleep(delay)


# ------------------------------------------------------------------ #
# Stealth injection via Win32 window messages (no keyboard events)
# ------------------------------------------------------------------ #

_EM_SETSEL = 0x00B1
_EM_REPLACESEL = 0x00C2
_SMTO_ABORTIFHUNG = 0x0002
_SMTO_TIMEOUT_MS = 500

_kernel32 = ctypes.windll.kernel32  # type: ignore[attr-defined]


def _get_focused_hwnd(foreground: int) -> int:
    """Return focused child control using AttachThreadInput cross-process."""
    tid_target = _user32.GetWindowThreadProcessId(foreground, None)
    tid_self = _kernel32.GetCurrentThreadId()
    focused: int = 0
    attached = False
    try:
        if tid_target and tid_target != tid_self:
            attached = bool(_user32.AttachThreadInput(tid_self, tid_target, True))
        focused = _user32.GetFocus() or 0
    except Exception:
        pass
    finally:
        if attached:
            _user32.AttachThreadInput(tid_self, tid_target, False)
    return focused or foreground


_EDIT_CLASSES = frozenset({"Edit", "RichEdit", "RichEdit20W", "RichEdit20A",
                            "RICHEDIT50W", "RichEditD2DPT"})


def _is_edit_control(hwnd: int) -> bool:
    """Return True if *hwnd* is a Win32 Edit or RichEdit control."""
    try:
        buf = ctypes.create_unicode_buffer(64)
        _user32.GetClassNameW(hwnd, buf, 64)
        cls = buf.value.strip()
        return cls in _EDIT_CLASSES
    except Exception:
        return False


def _inject_via_em_replacesel(hwnd: int, text: str) -> bool:
    """Insert *text* at cursor using EM_SETSEL + EM_REPLACESEL window messages.

    Does not generate keyboard events — invisible to WH_KEYBOARD_LL hooks.
    Works for Win32 Edit, RichEdit controls (Notepad, older Qt, etc.).
    Returns False if hwnd is not an Edit/RichEdit control so that the caller
    can fall through to the WM_CHAR path (CEF, Chrome, Electron, WebView2).
    """
    if not _is_edit_control(hwnd):
        log.debug("EM_REPLACESEL skipped — hwnd=%d is not an Edit control", hwnd)
        return False
    try:
        # Move caret to end of current selection (sel_start = sel_end = -1)
        result = ctypes.c_ssize_t(0)
        _user32.SendMessageTimeoutW(
            hwnd,
            _EM_SETSEL,
            ctypes.c_ulonglong(-1),
            ctypes.c_longlong(-1),
            _SMTO_ABORTIFHUNG,
            _SMTO_TIMEOUT_MS,
            ctypes.byref(result),
        )
        # EM_REPLACESEL: wParam=1 → undo-able; lParam → text pointer
        buf = ctypes.create_unicode_buffer(text)
        sent = ctypes.c_ssize_t(0)
        _user32.SendMessageTimeoutW(
            hwnd,
            _EM_REPLACESEL,
            1,
            buf,
            _SMTO_ABORTIFHUNG,
            _SMTO_TIMEOUT_MS,
            ctypes.byref(sent),
        )
        return True
    except Exception as exc:
        log.debug("EM_REPLACESEL failed on hwnd=%d: %s", hwnd, exc)
        return False


_WM_CHAR = 0x0102


def _inject_via_wm_char(hwnd: int, text: str, delay_ms: int = 30) -> bool:
    """Inject text via WM_CHAR PostMessage — bypasses WH_KEYBOARD_LL hooks.

    WM_CHAR goes directly into the window message queue (not the hardware
    input queue), so it is invisible to SetWindowsHookEx WH_KEYBOARD_LL hooks
    and does NOT set the LLKHF_INJECTED flag.

    Works with: CEF/Chrome, Electron, WebView2, modern browsers.
    Does NOT work with: applications that ignore WM_CHAR (games, etc.).
    """
    try:
        delay = delay_ms / 1000.0
        for char in text:
            code = ord(char)
            if code > 0xFFFF:
                # UTF-16 surrogate pair
                code -= 0x10000
                high = 0xD800 + (code >> 10)
                low = 0xDC00 + (code & 0x3FF)
                _user32.PostMessageW(hwnd, _WM_CHAR, high, 1)
                _user32.PostMessageW(hwnd, _WM_CHAR, low, 1)
            else:
                _user32.PostMessageW(hwnd, _WM_CHAR, code, 1)
            if delay > 0:
                import time

                time.sleep(delay)
        return True
    except Exception as exc:
        log.debug("WM_CHAR injection failed on hwnd=%d: %s", hwnd, exc)
        return False


def type_text_stealth(text: str, delay_ms: int = 30) -> None:
    """Insert *text* preferring window messages over SendInput.

    Priority:
      1. EM_REPLACESEL via SendMessage   (Edit/RichEdit: Notepad, older Qt)
      2. WM_CHAR via PostMessage         (CEF/Chrome/Electron — no LLKHF_INJECTED)
      3. SendInput KEYEVENTF_UNICODE     (universal fallback)

    Use this when keyboard hooks may swallow or flag synthetic key events.
    """
    if not text:
        return

    try:
        foreground = _user32.GetForegroundWindow()
        if foreground:
            focused = _get_focused_hwnd(foreground)
            if _inject_via_em_replacesel(focused, text):
                log.debug("Injected %d chars via EM_REPLACESEL", len(text))
                return
            if _inject_via_wm_char(focused, text, delay_ms=delay_ms):
                log.debug("Injected %d chars via WM_CHAR", len(text))
                return
    except Exception as exc:
        log.debug("Stealth inject pre-check failed: %s", exc)

    # Fallback: original SendInput method
    log.debug("Falling back to SendInput for %d chars", len(text))
    type_text(text, delay_ms=delay_ms)
