"""Windows: capture selected text via UI Automation and Win32 messages.

Stealth-compatible alternative to Ctrl+C clipboard simulation.
Uses accessibility APIs that do not generate keyboard events.

Priority chain:
  1. Win32 EM_GETSEL + WM_GETTEXT  (Edit/RichEdit controls, Notepad, etc.)
  2. IAccessible MSAA              (older UI frameworks: WinForms, VB6)
  3. IUIAutomation TextPattern     (CEF/Chrome, Electron, WebView2, WPF)
  4. Fallback: return ""           (caller falls back to clipboard method)
"""

from __future__ import annotations

import ctypes
import ctypes.wintypes
import logging

log = logging.getLogger(__name__)

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

# Win32 message constants
_WM_GETTEXT = 0x000D
_WM_GETTEXTLENGTH = 0x000E
_EM_GETSEL = 0x00B0

# SendMessageTimeout flags
_SMTO_ABORTIFHUNG = 0x0002
_SMTO_TIMEOUT_MS = 500

# IAccessible MSAA constants
_OBJID_CLIENT = 0xFFFFFFFC  # pass to AccessibleObjectFromWindow


class _GUID(ctypes.Structure):
    _fields_ = [
        ("Data1", ctypes.c_ulong),
        ("Data2", ctypes.c_ushort),
        ("Data3", ctypes.c_ushort),
        ("Data4", ctypes.c_ubyte * 8),
    ]


# IID_IAccessible = {618736E0-3C3D-11CF-810C-00AA00389B71}
_IID_IACCESSIBLE = _GUID(
    0x618736E0,
    0x3C3D,
    0x11CF,
    (ctypes.c_ubyte * 8)(0x81, 0x0C, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71),
)

# IUIAutomation constants
_CLSCTX_INPROC_SERVER = 1
_UIA_TextPatternId = 10014

# CLSID_CUIAutomation = {FF48DBA4-60EF-4201-AA87-54103EEF594E}
_CLSID_CUIAUTOMATION = _GUID(
    0xFF48DBA4,
    0x60EF,
    0x4201,
    (ctypes.c_ubyte * 8)(0xAA, 0x87, 0x54, 0x10, 0x3E, 0xEF, 0x59, 0x4E),
)

# IID_IUIAutomation = {30CBE57D-D9D0-452A-B4FF-A337007FEB55}
_IID_IUIAUTOMATION = _GUID(
    0x30CBE57D,
    0xD9D0,
    0x452A,
    (ctypes.c_ubyte * 8)(0xB4, 0xFF, 0xA3, 0x37, 0x00, 0x7F, 0xEB, 0x55),
)


def _get_focused_hwnd_cross_process(foreground_hwnd: int) -> int:
    """Return the focused child window handle using AttachThreadInput.

    GetFocus() only works within the calling thread.
    AttachThreadInput lets us query the focused control in another process.
    """
    tid_target = _user32.GetWindowThreadProcessId(foreground_hwnd, 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_hwnd


def _em_getsel(hwnd: int) -> tuple[int, int]:
    """Return (sel_start, sel_end) from an Edit/RichEdit control."""
    start = ctypes.wintypes.DWORD(0)
    end = ctypes.wintypes.DWORD(0)
    result = ctypes.wintypes.LRESULT(0)
    _user32.SendMessageTimeoutW(
        hwnd,
        _EM_GETSEL,
        ctypes.byref(start),
        ctypes.byref(end),
        _SMTO_ABORTIFHUNG,
        _SMTO_TIMEOUT_MS,
        ctypes.byref(result),
    )
    return int(start.value), int(end.value)


def _wm_gettext(hwnd: int) -> str:
    """Read full text of a Win32 Edit/RichEdit control via WM_GETTEXT."""
    length_result = ctypes.wintypes.LRESULT(0)
    _user32.SendMessageTimeoutW(
        hwnd,
        _WM_GETTEXTLENGTH,
        0,
        0,
        _SMTO_ABORTIFHUNG,
        _SMTO_TIMEOUT_MS,
        ctypes.byref(length_result),
    )
    length = int(length_result.value)
    if length <= 0:
        return ""

    buf = ctypes.create_unicode_buffer(length + 1)
    got_result = ctypes.wintypes.LRESULT(0)
    _user32.SendMessageTimeoutW(
        hwnd,
        _WM_GETTEXT,
        length + 1,
        buf,
        _SMTO_ABORTIFHUNG,
        _SMTO_TIMEOUT_MS,
        ctypes.byref(got_result),
    )
    return buf.value


def _get_via_em_getsel(hwnd: int) -> str:
    """Extract selected text from a Win32 Edit/RichEdit via EM_GETSEL + WM_GETTEXT.

    Works for: Notepad, cmd.exe, standard dialogs, older Qt5 text widgets.
    Does NOT work for: CEF, Electron, WebView2, WPF TextBox.
    Returns "" if hwnd is not an Edit control or nothing is selected.
    """
    try:
        sel_start, sel_end = _em_getsel(hwnd)
        if sel_start >= sel_end:
            return ""
        full_text = _wm_gettext(hwnd)
        if not full_text:
            return ""
        selected = full_text[sel_start:sel_end]
        if selected:
            log.debug("Captured %d chars via EM_GETSEL", len(selected))
        return selected
    except Exception as exc:
        log.debug("EM_GETSEL failed: %s", exc)
        return ""


def _make_variant_i4(value: int) -> ctypes.Array:
    """Return a 16-byte VARIANT buffer with vt=VT_I4 and lVal=value."""
    buf = (ctypes.c_byte * 16)()
    ctypes.c_ushort.from_buffer(buf).value = 3  # VT_I4
    ctypes.c_long.from_buffer(buf, 8).value = value
    return buf


def _get_via_iaccessible(hwnd: int) -> str:
    """Extract selected text via IAccessible MSAA API.

    Works for WinForms, VB6, older Qt, Java Swing (with MSAA bridge).
    Uses AccessibleObjectFromWindow + get_accValue vtable call (vtable[11]).
    Returns "" if MSAA is unavailable or hwnd has no accessible value.
    """
    try:
        oleacc = ctypes.windll.oleacc
    except (OSError, AttributeError):
        return ""
    try:
        iid = _GUID(
            _IID_IACCESSIBLE.Data1,
            _IID_IACCESSIBLE.Data2,
            _IID_IACCESSIBLE.Data3,
            (ctypes.c_ubyte * 8)(*_IID_IACCESSIBLE.Data4),
        )
        ppv = ctypes.c_void_p(0)
        hr = oleacc.AccessibleObjectFromWindow(
            hwnd,
            _OBJID_CLIENT,
            ctypes.byref(iid),
            ctypes.byref(ppv),
        )
        if hr != 0 or not ppv.value:
            return ""

        com_ptr = ppv.value
        # IAccessible vtable layout (indices):
        #   [0-2]  IUnknown: QueryInterface, AddRef, Release
        #   [3-6]  IDispatch: GetTypeInfoCount, GetTypeInfo, GetIDsOfNames, Invoke
        #   [7-10] IAccessible: get_accParent, get_accChildCount, get_accChild, get_accName
        #   [11]   get_accValue <- we use this
        vtable = ctypes.cast(
            ctypes.cast(com_ptr, ctypes.POINTER(ctypes.c_void_p))[0],
            ctypes.POINTER(ctypes.c_void_p),
        )

        # get_accValue(self, varChild: VARIANT, pbstrValue: BSTR*) -> HRESULT
        # Pass CHILDID_SELF = VT_I4(0) to query the control itself
        GetAccValue = ctypes.WINFUNCTYPE(
            ctypes.c_long,  # HRESULT return
            ctypes.c_void_p,  # this (IAccessible*)
            ctypes.c_byte * 16,  # varChild (VARIANT, 16 bytes by value)
            ctypes.POINTER(ctypes.c_void_p),  # pbstrValue (BSTR*)
        )
        get_value_fn = GetAccValue(vtable[11])
        var_child = _make_variant_i4(0)  # CHILDID_SELF
        bstr_ptr = ctypes.c_void_p(0)
        hr2 = get_value_fn(com_ptr, var_child, ctypes.byref(bstr_ptr))

        text = ""
        if hr2 == 0 and bstr_ptr.value:
            # BSTR layout: 4-byte byte-length prefix + wchar_t array (no null counted)
            bstr_addr = bstr_ptr.value
            byte_len = ctypes.c_int.from_address(bstr_addr - 4).value
            if byte_len > 0:
                text = ctypes.wstring_at(bstr_addr, byte_len // 2)
            ctypes.windll.oleaut32.SysFreeString(bstr_ptr)

        # Release the IAccessible COM object (vtable[2])
        Release = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)
        Release(vtable[2])(com_ptr)

        if text:
            log.debug("Captured %d chars via IAccessible MSAA", len(text))
        return text

    except Exception as exc:
        log.debug("IAccessible MSAA failed: %s", exc)
        return ""


def _get_via_uia(hwnd: int) -> str:  # noqa: ARG001
    """Extract selected text via IUIAutomation TextPattern.

    Works for: CEF/Chrome, Electron, WebView2, WPF TextBox, Edge (Chromium).
    Uses GetFocusedElement() — traverses the accessibility tree to the focused
    text control, even when it is nested inside a CEF/browser host window.

    IUIAutomation vtable indices used:
      IUIAutomation:        [8] GetFocusedElement
      IUIAutomationElement: [16] GetCurrentPattern(UIA_TextPatternId=10014)
      IUIAutomationTextPattern:  [5] GetSelection
      IUIAutomationTextRangeArray: [3] get_Length, [4] GetElement
      IUIAutomationTextRange: [12] GetText(-1 = unlimited)

    Returns "" if UIA is unavailable or nothing is selected.
    """
    try:
        ole32 = ctypes.windll.ole32
    except (OSError, AttributeError):
        return ""

    com_initialized = False
    try:
        # Initialize COM for this thread (daemon threads are not COM-initialized)
        hr_init = ole32.CoInitializeEx(None, 0)  # COINIT_MULTITHREADED
        com_initialized = hr_init in (0, 1)  # S_OK or S_FALSE

        try:
            # --- Create IUIAutomation factory via CoCreateInstance ---
            clsid = _GUID(
                _CLSID_CUIAUTOMATION.Data1,
                _CLSID_CUIAUTOMATION.Data2,
                _CLSID_CUIAUTOMATION.Data3,
                (ctypes.c_ubyte * 8)(*_CLSID_CUIAUTOMATION.Data4),
            )
            iid = _GUID(
                _IID_IUIAUTOMATION.Data1,
                _IID_IUIAUTOMATION.Data2,
                _IID_IUIAUTOMATION.Data3,
                (ctypes.c_ubyte * 8)(*_IID_IUIAUTOMATION.Data4),
            )
            uia_ptr = ctypes.c_void_p(0)
            hr = ole32.CoCreateInstance(
                ctypes.byref(clsid),
                None,
                _CLSCTX_INPROC_SERVER,
                ctypes.byref(iid),
                ctypes.byref(uia_ptr),
            )
            if hr != 0 or not uia_ptr.value:
                return ""

            uia = uia_ptr.value
            uia_vt = ctypes.cast(
                ctypes.cast(uia, ctypes.POINTER(ctypes.c_void_p))[0],
                ctypes.POINTER(ctypes.c_void_p),
            )
            Release = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)

            # --- GetFocusedElement(self, element**) -> HRESULT  [vtable[8]] ---
            # Traverses the UIA tree to the actual focused leaf (works for CEF)
            GetFocusedElement = ctypes.WINFUNCTYPE(
                ctypes.c_long,
                ctypes.c_void_p,
                ctypes.POINTER(ctypes.c_void_p),
            )
            elem_ptr = ctypes.c_void_p(0)
            hr2 = GetFocusedElement(uia_vt[8])(uia, ctypes.byref(elem_ptr))
            Release(uia_vt[2])(uia)  # done with factory

            if hr2 != 0 or not elem_ptr.value:
                return ""

            elem = elem_ptr.value
            elem_vt = ctypes.cast(
                ctypes.cast(elem, ctypes.POINTER(ctypes.c_void_p))[0],
                ctypes.POINTER(ctypes.c_void_p),
            )

            # --- GetCurrentPattern(self, patternId, pattern**) [vtable[16]] ---
            GetCurrentPattern = ctypes.WINFUNCTYPE(
                ctypes.c_long,
                ctypes.c_void_p,
                ctypes.c_int,
                ctypes.POINTER(ctypes.c_void_p),
            )
            pattern_ptr = ctypes.c_void_p(0)
            hr3 = GetCurrentPattern(elem_vt[16])(
                elem, _UIA_TextPatternId, ctypes.byref(pattern_ptr)
            )
            Release(elem_vt[2])(elem)  # done with element

            if hr3 != 0 or not pattern_ptr.value:
                return ""

            pattern = pattern_ptr.value
            pattern_vt = ctypes.cast(
                ctypes.cast(pattern, ctypes.POINTER(ctypes.c_void_p))[0],
                ctypes.POINTER(ctypes.c_void_p),
            )

            # --- GetSelection(self, ranges**) -> HRESULT  [vtable[5]] ---
            GetSelection = ctypes.WINFUNCTYPE(
                ctypes.c_long,
                ctypes.c_void_p,
                ctypes.POINTER(ctypes.c_void_p),
            )
            ranges_ptr = ctypes.c_void_p(0)
            hr4 = GetSelection(pattern_vt[5])(pattern, ctypes.byref(ranges_ptr))
            Release(pattern_vt[2])(pattern)  # done with TextPattern

            if hr4 != 0 or not ranges_ptr.value:
                return ""

            ranges = ranges_ptr.value
            ranges_vt = ctypes.cast(
                ctypes.cast(ranges, ctypes.POINTER(ctypes.c_void_p))[0],
                ctypes.POINTER(ctypes.c_void_p),
            )

            # --- get_Length(self, length*) -> HRESULT  [vtable[3]] ---
            GetLength = ctypes.WINFUNCTYPE(
                ctypes.c_long,
                ctypes.c_void_p,
                ctypes.POINTER(ctypes.c_int),
            )
            arr_len = ctypes.c_int(0)
            GetLength(ranges_vt[3])(ranges, ctypes.byref(arr_len))

            text = ""
            if arr_len.value > 0:
                # --- GetElement(self, index, range**) -> HRESULT  [vtable[4]] ---
                GetElement = ctypes.WINFUNCTYPE(
                    ctypes.c_long,
                    ctypes.c_void_p,
                    ctypes.c_int,
                    ctypes.POINTER(ctypes.c_void_p),
                )
                range_ptr = ctypes.c_void_p(0)
                hr5 = GetElement(ranges_vt[4])(ranges, 0, ctypes.byref(range_ptr))

                if hr5 == 0 and range_ptr.value:
                    rng = range_ptr.value
                    rng_vt = ctypes.cast(
                        ctypes.cast(rng, ctypes.POINTER(ctypes.c_void_p))[0],
                        ctypes.POINTER(ctypes.c_void_p),
                    )

                    # --- GetText(self, maxLength, bstr*) [vtable[12]] ---
                    # maxLength = -1 means no limit
                    GetText = ctypes.WINFUNCTYPE(
                        ctypes.c_long,
                        ctypes.c_void_p,
                        ctypes.c_int,
                        ctypes.POINTER(ctypes.c_void_p),
                    )
                    bstr_ptr = ctypes.c_void_p(0)
                    hr6 = GetText(rng_vt[12])(rng, -1, ctypes.byref(bstr_ptr))
                    Release(rng_vt[2])(rng)  # done with text range

                    if hr6 == 0 and bstr_ptr.value:
                        # BSTR: 4-byte byte-length prefix + wchar_t array
                        bstr_addr = bstr_ptr.value
                        byte_len = ctypes.c_int.from_address(bstr_addr - 4).value
                        if byte_len > 0:
                            text = ctypes.wstring_at(bstr_addr, byte_len // 2)
                        ctypes.windll.oleaut32.SysFreeString(bstr_ptr)

            Release(ranges_vt[2])(ranges)  # done with ranges array

            if text:
                log.debug("Captured %d chars via IUIAutomation TextPattern", len(text))
            return text

        finally:
            if com_initialized:
                try:
                    ole32.CoUninitialize()
                except Exception:
                    pass

    except Exception as exc:
        log.debug("IUIAutomation TextPattern failed: %s", exc)
        return ""


def get_selected_text_stealth() -> str:
    """Return selected text without generating keyboard events.

    Tries multiple accessibility APIs. Returns "" if nothing found;
    the caller should fall back to the clipboard method.
    """
    try:
        foreground = _user32.GetForegroundWindow()
        if not foreground:
            return ""

        focused = _get_focused_hwnd_cross_process(foreground)

        log.info("UIA capture: foreground=%s focused=%s", foreground, focused)

        # Attempt 1: Win32 Edit / RichEdit messages
        text = _get_via_em_getsel(focused)
        log.info("UIA EM_GETSEL(focused): %r", text[:30] if text else "")
        if text:
            return text

        # Attempt 2: try the foreground window itself (e.g., single-pane editors)
        if focused != foreground:
            text = _get_via_em_getsel(foreground)
            log.info("UIA EM_GETSEL(foreground): %r", text[:30] if text else "")
            if text:
                return text

        # Attempt 3: IAccessible MSAA (WinForms, VB6, older Qt, Java Swing)
        text = _get_via_iaccessible(focused)
        log.info("UIA MSAA(focused): %r", text[:30] if text else "")
        if text:
            return text

        if focused != foreground:
            text = _get_via_iaccessible(foreground)
            log.info("UIA MSAA(foreground): %r", text[:30] if text else "")
            if text:
                return text

        # Attempt 4: IUIAutomation TextPattern (CEF/Chrome, Electron, WebView2, WPF)
        text = _get_via_uia(focused)
        log.info("UIA TextPattern: %r", text[:30] if text else "")
        if text:
            return text

    except Exception as exc:
        log.info("Stealth text capture failed: %s", exc)

    return ""
