"""Cross-platform typing engine — dispatches to platform implementation.

Two modes:
  auto_paste=False  → type character by character (KEYEVENTF_UNICODE / CGEvent)
  auto_paste=True   → clipboard paste (pyperclip.copy + Ctrl+V / Cmd+V)
                      faster, but replaces clipboard contents
"""

from __future__ import annotations

import logging
import sys
import threading

log = logging.getLogger(__name__)


def _paste_via_clipboard(text: str) -> None:
    """Copy *text* to clipboard and simulate Ctrl+V / Cmd+V."""
    try:
        import pyperclip

        pyperclip.copy(text)
    except Exception as exc:
        log.error("pyperclip copy failed: %s", exc)
        return

    import time

    time.sleep(0.05)  # allow clipboard to propagate

    if sys.platform == "win32":  # pragma: no cover
        import ctypes

        # Use PostMessage(WM_PASTE) instead of keybd_event(Ctrl+V).
        # PostMessage goes directly to the window message queue — invisible to
        # WH_KEYBOARD_LL hooks (ksHook.exe in CSCA/lockdown exam software).
        # Works with Win32 Edit controls, CEF/Chrome, Electron, WebView2.
        _WM_PASTE = 0x0302
        _VK_CONTROL = 0x11
        _VK_V = 0x56
        _KEYEVENTF_KEYUP = 0x0002
        u32 = ctypes.windll.user32  # type: ignore[attr-defined]
        foreground = u32.GetForegroundWindow()
        if foreground:
            from .impl_win import _get_focused_hwnd

            focused = _get_focused_hwnd(foreground)
            ok = u32.PostMessageW(focused, _WM_PASTE, 0, 0)
            if not ok:
                # WM_PASTE rejected (target does not handle it) — fall back to
                # hardware-queue Ctrl+V via keybd_event.
                log.debug("WM_PASTE returned 0 on hwnd=%d — falling back to keybd_event Ctrl+V", focused)
                u32.keybd_event(_VK_CONTROL, 0, 0, 0)
                u32.keybd_event(_VK_V, 0, 0, 0)
                u32.keybd_event(_VK_V, 0, _KEYEVENTF_KEYUP, 0)
                u32.keybd_event(_VK_CONTROL, 0, _KEYEVENTF_KEYUP, 0)

    elif sys.platform == "darwin":
        try:
            import Quartz

            CMD = 0x37
            V = 0x09
            for key_code, down in [(CMD, True), (V, True), (V, False), (CMD, False)]:
                src = Quartz.CGEventSourceCreate(0)
                ev = Quartz.CGEventCreateKeyboardEvent(src, key_code, down)
                if down and key_code == V:
                    Quartz.CGEventSetFlags(ev, Quartz.kCGEventFlagMaskCommand)
                Quartz.CGEventPost(0, ev)
                time.sleep(0.02)
        except Exception as exc:
            log.error("macOS paste via CGEvent failed: %s", exc)


def _type_via_impl(text: str, delay_ms: int, cancel: threading.Event | None) -> None:
    """Type text using the platform-specific character injection."""
    if sys.platform == "win32":  # pragma: no cover
        from .impl_win import type_char
    elif sys.platform == "darwin":
        from .impl_mac import type_char
    else:
        raise RuntimeError(f"Unsupported platform: {sys.platform}")

    import time

    delay = delay_ms / 1000.0
    for i, char in enumerate(text):
        if cancel is not None and cancel.is_set():
            log.debug("Typing cancelled after %d chars", i)
            return
        type_char(char)
        if delay > 0:
            time.sleep(delay)


def _type_via_impl_stealth(text: str, delay_ms: int, cancel: threading.Event | None) -> None:
    """Type text preferring window messages over SendInput (Windows stealth mode)."""
    if sys.platform == "win32":  # pragma: no cover
        from .impl_win import type_text_stealth

        # type_text_stealth tries EM_REPLACESEL first, then falls back to SendInput.
        # We pass the full text at once — no per-character cancel support when
        # EM_REPLACESEL succeeds (it's instantaneous), but cancel is checked before.
        if cancel is not None and cancel.is_set():
            return
        type_text_stealth(text, delay_ms=delay_ms)
    elif sys.platform == "darwin":
        from .impl_mac import type_text_stealth as _mac_stealth

        if cancel is not None and cancel.is_set():
            return
        _mac_stealth(text)
    else:
        _type_via_impl(text, delay_ms, cancel)


class TypingEngine:
    """Manages text output — either by typing or pasting."""

    def __init__(
        self,
        auto_paste: bool = False,
        delay_ms: int = 30,
        stealth: bool = False,
        biometric: bool = False,
        typo_rate: float = 0.01,
        variation_ms: int = 20,
    ) -> None:
        self.auto_paste = auto_paste
        self.delay_ms = delay_ms
        self.stealth = stealth
        self.biometric = biometric
        self.typo_rate = typo_rate
        self._variation_ms = variation_ms
        self._cancel = threading.Event()

    def type_text(self, text: str) -> None:
        """Output *text* using the configured mode.

        Runs synchronously; call from a background thread if needed.
        """
        if not text:
            return

        self._cancel.clear()

        if self.auto_paste:
            log.debug("Pasting %d chars via clipboard", len(text))
            _paste_via_clipboard(text)
        elif self.biometric and self.stealth:
            log.debug("Typing %d chars (biometric stealth mode)", len(text))
            self._type_biometric(text)
        elif self.stealth:
            log.debug("Typing %d chars (stealth mode)", len(text))
            _type_via_impl_stealth(text, self.delay_ms, self._cancel)
        else:
            log.debug("Typing %d chars (delay=%dms)", len(text), self.delay_ms)
            _type_via_impl(text, self.delay_ms, self._cancel)

    def _type_biometric(self, text: str) -> None:
        """Type text with biometric timing — human-like delays and optional typos."""
        import time

        from .biometrics import TypoActionKind, maybe_introduce_typo

        if sys.platform == "win32":  # pragma: no cover
            from .impl_win import type_char

            def do_backspace() -> None:
                import ctypes

                VK_BACK = 0x08
                KEYEVENTF_KEYUP = 0x0002
                u32 = ctypes.windll.user32  # type: ignore[attr-defined]
                u32.keybd_event(VK_BACK, 0, 0, 0)
                u32.keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0)
        elif sys.platform == "darwin":
            from .impl_mac import type_char

            def do_backspace() -> None:
                try:
                    import Quartz

                    src = Quartz.CGEventSourceCreate(0)
                    ev = Quartz.CGEventCreateKeyboardEvent(src, 0x33, True)
                    Quartz.CGEventPost(0, ev)
                    ev2 = Quartz.CGEventCreateKeyboardEvent(src, 0x33, False)
                    Quartz.CGEventPost(0, ev2)
                except (ImportError, AttributeError, OSError):
                    pass
        else:
            raise RuntimeError(f"Unsupported platform: {sys.platform}")

        actions = maybe_introduce_typo(
            text,
            typo_probability=self.typo_rate,
            base_ms=float(self.delay_ms),
            variation_ms=float(self._variation_ms),
        )

        for action in actions:
            if self._cancel.is_set():
                log.debug("Biometric typing cancelled")
                return
            time.sleep(action.delay)
            if action.kind == TypoActionKind.TYPE:
                type_char(action.char)
            elif action.kind == TypoActionKind.BACKSPACE:
                do_backspace()
            # PAUSE: only sleep (already done above)

    def cancel(self) -> None:
        """Signal ongoing typing to stop at the next character."""
        self._cancel.set()
