"""Excel COM helpers — locate the Excel Application object."""

import ctypes
from ctypes import wintypes

from shortcut_xl._log import xl_log

# AccessibleObjectFromWindow native object ID
_OBJID_NATIVEOM = -16

# Excel window class names (hierarchy: XLMAIN → XLDESK → EXCEL7)
_WC_XLMAIN = 'XLMAIN'
_WC_XLDESK = 'XLDESK'
_WC_EXCEL7 = 'EXCEL7'


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


# IID_IDispatch = {00020400-0000-0000-C000-000000000046}
_IID_IDISPATCH = _GUID(0x00020400, 0x0000, 0x0000, (ctypes.c_byte * 8)(0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46))


_user32 = ctypes.windll.user32


def _find_own_xlmain():
    """Find the XLMAIN window belonging to our own process.

    The XLL runs in-process, so our PID == Excel's PID.
    EnumWindows + GetWindowThreadProcessId ensures we find the right
    instance even when multiple Excels are open.

    Uses local variables captured by a closure instead of module-level
    mutable globals, making this safe for concurrent calls."""
    target_pid = ctypes.windll.kernel32.GetCurrentProcessId()
    result = ctypes.c_void_p(0)

    @ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
    def callback(hwnd, _lparam):
        buf = ctypes.create_unicode_buffer(32)
        _user32.GetClassNameW(hwnd, buf, 32)
        if buf.value != _WC_XLMAIN:
            return True
        pid = wintypes.DWORD()
        _user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
        if pid.value == target_pid:
            result.value = hwnd
            return False
        return True

    _user32.EnumWindows(callback, 0)
    return result.value


def _ensure_com_init():
    """Initialize COM on the current thread (idempotent)."""
    import pythoncom

    pythoncom.CoInitialize()


def xl_app():
    """Return the Excel Application COM object.

    Performs a fresh HWND lookup every call. The cost is negligible
    (microseconds) and avoids stale-reference bugs from caching.

    Uses EnumWindows + PID matching to find this process's Excel,
    not just the first XLMAIN window on the desktop."""
    _ensure_com_init()
    import pythoncom
    import win32com.client

    # Use windll (not oledll) so we get the HRESULT as a return value
    # instead of an auto-raised OSError — lets us fall through to the
    # GetActiveObject fallback on failure.
    oleacc = ctypes.windll.oleacc

    xlmain = _find_own_xlmain()
    if xlmain:
        xldesk = _user32.FindWindowExW(xlmain, 0, _WC_XLDESK, None)
        excel7 = _user32.FindWindowExW(xldesk, 0, _WC_EXCEL7, None) if xldesk else 0
        if excel7:
            punk = ctypes.c_void_p()
            hr = oleacc.AccessibleObjectFromWindow(
                excel7, _OBJID_NATIVEOM, ctypes.byref(_IID_IDISPATCH), ctypes.byref(punk)
            )
            if hr == 0 and punk.value:
                iunk = pythoncom.ObjectFromAddress(punk.value)
                disp = iunk.QueryInterface(pythoncom.IID_IDispatch)
                window = win32com.client.Dispatch(disp)
                return window.Application

    # Fallback — try ROT, but NEVER create a new instance.
    xl_log('xl_app: HWND approach failed, trying GetActiveObject')
    try:
        return win32com.client.GetActiveObject('Excel.Application')
    except Exception as e:
        raise RuntimeError(
            'Could not find a running Excel instance. Make sure Excel is open before loading the XLL.'
        ) from e
