"""
ipc - Single-instance IPC for forwarding "--convert <file>" requests to the
already-running VoxCast instance via WM_COPYDATA.

Why: the running instance has the Whisper model warm; spawning a fresh process
per conversion would reload the model every time. A context-menu/CLI invocation
sends the file path to the running instance, which converts it and shows the
"Save As" dialog + tray notification.

A dedicated message-only window (own WNDPROC) is used rather than Qt's native
event filter, because WM_COPYDATA is delivered via SendMessage (sent, not
posted) and must reach a real window procedure.
"""
from __future__ import annotations

import ctypes
from ctypes import wintypes
from typing import Callable

from loguru import logger

# Window class/title used for FindWindow discovery across processes.
IPC_CLASS_NAME = "VoxCastIPCReceiver"
IPC_WINDOW_TITLE = "VoxCastIPCReceiver"

WM_COPYDATA = 0x004A
HWND_MESSAGE = -3
# Magic dwData tag so we only accept our own messages.
DW_CONVERT = 0x564F5843  # 'VOXC'
_MAX_PATH_CHARS = 4096

# 64-bit safe Win32 type aliases.
LRESULT = ctypes.c_ssize_t
LONG_PTR = ctypes.c_ssize_t
ULONG_PTR = ctypes.c_size_t

_user32 = ctypes.WinDLL("user32", use_last_error=True)
_kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)

WNDPROC = ctypes.WINFUNCTYPE(
    LRESULT, wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
)


class COPYDATASTRUCT(ctypes.Structure):
    _fields_ = [
        ("dwData", ULONG_PTR),
        ("cbData", wintypes.DWORD),
        ("lpData", ctypes.c_void_p),
    ]


class WNDCLASS(ctypes.Structure):
    _fields_ = [
        ("style", wintypes.UINT),
        ("lpfnWndProc", WNDPROC),
        ("cbClsExtra", ctypes.c_int),
        ("cbWndExtra", ctypes.c_int),
        ("hInstance", wintypes.HINSTANCE),
        ("hIcon", wintypes.HICON),
        ("hCursor", wintypes.HANDLE),
        ("hbrBackground", wintypes.HBRUSH),
        ("lpszMenuName", wintypes.LPCWSTR),
        ("lpszClassName", wintypes.LPCWSTR),
    ]


# Explicit prototypes — REQUIRED on Win64 so pointer/handle arguments are not
# truncated to 32 bits by ctypes' default int marshalling.
_user32.FindWindowW.restype = wintypes.HWND
_user32.FindWindowW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR]

_user32.SendMessageTimeoutW.restype = LRESULT
_user32.SendMessageTimeoutW.argtypes = [
    wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
    wintypes.UINT, wintypes.UINT, ctypes.POINTER(ULONG_PTR),
]

_user32.RegisterClassW.restype = wintypes.ATOM
_user32.RegisterClassW.argtypes = [ctypes.POINTER(WNDCLASS)]

_user32.CreateWindowExW.restype = wintypes.HWND
_user32.CreateWindowExW.argtypes = [
    wintypes.DWORD, wintypes.LPCWSTR, wintypes.LPCWSTR, wintypes.DWORD,
    ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int,
    wintypes.HWND, wintypes.HMENU, wintypes.HINSTANCE, wintypes.LPVOID,
]

_user32.DefWindowProcW.restype = LRESULT
_user32.DefWindowProcW.argtypes = [
    wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM,
]

_user32.DestroyWindow.restype = wintypes.BOOL
_user32.DestroyWindow.argtypes = [wintypes.HWND]

_user32.UnregisterClassW.restype = wintypes.BOOL
_user32.UnregisterClassW.argtypes = [wintypes.LPCWSTR, wintypes.HINSTANCE]

_kernel32.GetModuleHandleW.restype = wintypes.HMODULE
_kernel32.GetModuleHandleW.argtypes = [wintypes.LPCWSTR]


def send_convert_request(path: str, *, timeout_ms: int = 5000) -> bool:
    """Send a convert request to a running VoxCast instance.

    Returns True if a receiver window was found and the message was delivered.
    """
    hwnd = _user32.FindWindowW(IPC_CLASS_NAME, IPC_WINDOW_TITLE)
    if not hwnd:
        return False

    payload = (path or "").strip()
    if not payload:
        return False
    buf = ctypes.create_unicode_buffer(payload)  # includes trailing NUL
    cbytes = ctypes.sizeof(ctypes.c_wchar) * (len(payload) + 1)

    cds = COPYDATASTRUCT()
    cds.dwData = DW_CONVERT
    cds.cbData = cbytes
    cds.lpData = ctypes.cast(buf, ctypes.c_void_p)

    result = ULONG_PTR(0)
    SMTO_ABORTIFHUNG = 0x0002
    # LPARAM carries the address of the COPYDATASTRUCT.
    sent = _user32.SendMessageTimeoutW(
        hwnd, WM_COPYDATA, 0, ctypes.addressof(cds),
        SMTO_ABORTIFHUNG, timeout_ms, ctypes.byref(result),
    )
    if not sent:
        logger.warning("IPC SendMessageTimeout failed for path {}", path)
        return False
    logger.info("IPC convert request forwarded to running instance: {}", path)
    return True


class IpcReceiver:
    """Owns a message-only window that receives WM_COPYDATA convert requests."""

    def __init__(self, on_convert: Callable[[str], None]) -> None:
        self._on_convert = on_convert
        self._hwnd: int | None = None
        self._wndproc_ref: WNDPROC | None = None  # keep alive
        self._class_atom: int | None = None

    def start(self) -> None:
        self._wndproc_ref = WNDPROC(self._wndproc)

        hinst = _kernel32.GetModuleHandleW(None)
        wc = WNDCLASS()
        wc.lpfnWndProc = self._wndproc_ref
        wc.hInstance = hinst
        wc.lpszClassName = IPC_CLASS_NAME

        atom = _user32.RegisterClassW(ctypes.byref(wc))
        if not atom:
            # Class may already exist (e.g. re-init); proceed to create window.
            err = ctypes.get_last_error()
            logger.debug("RegisterClassW returned 0 (err={})", err)
        self._class_atom = atom

        self._hwnd = _user32.CreateWindowExW(
            0, IPC_CLASS_NAME, IPC_WINDOW_TITLE, 0, 0, 0, 0, 0,
            wintypes.HWND(HWND_MESSAGE), None, hinst, None,
        )
        if not self._hwnd:
            logger.error("Failed to create IPC receiver window (err={})",
                         ctypes.get_last_error())
        else:
            logger.info("IPC receiver window ready (hwnd={})", self._hwnd)

    def _wndproc(self, hwnd, msg, wparam, lparam):
        if msg == WM_COPYDATA:
            try:
                cds = ctypes.cast(lparam, ctypes.POINTER(COPYDATASTRUCT)).contents
                wchar = ctypes.sizeof(ctypes.c_wchar)
                # UTF-16LE payload must have an even, non-zero byte count.
                if (cds.dwData == DW_CONVERT and cds.lpData
                        and cds.cbData > 0 and cds.cbData % wchar == 0):
                    nchars = min(cds.cbData // wchar, _MAX_PATH_CHARS)
                    text = ctypes.wstring_at(cds.lpData, nchars).rstrip("\x00").strip()
                    if text:
                        self._on_convert(text)
                return 1
            except Exception as e:
                logger.error("IPC WM_COPYDATA handling failed: {}", e)
                return 0
        return _user32.DefWindowProcW(hwnd, msg, wparam, lparam)

    def stop(self) -> None:
        if self._hwnd:
            try:
                _user32.DestroyWindow(self._hwnd)
            except Exception:
                pass
            self._hwnd = None
        if self._class_atom:
            try:
                _user32.UnregisterClassW(IPC_CLASS_NAME, _kernel32.GetModuleHandleW(None))
            except Exception:
                pass
            self._class_atom = None
        self._wndproc_ref = None
