"""
wivexx_winctl.py — desktop window control  (v1.15)
═══════════════════════════════════════════════════════════════════════
Find, move, focus, minimise, maximise and close top-level windows so WIVEXX
can act on commands like:
    "move Discord to the left monitor"
    "minimize everything except VS Code"
    "focus Spotify"  ·  "maximize Chrome"  ·  "close Notepad"

Pure ctypes (Win32) + `mss` for monitor geometry (both already used by WIVEXX);
`psutil` is used opportunistically to match a window by its process name.

Public API (used by wivexx_api.py):
    list_windows() -> [ {hwnd, title, pid, proc} ]
    find_window(name) -> hwnd | None
    monitors() -> [ {left, top, width, height} ]   # left→right order
    move_to_monitor(hwnd, target) -> bool          # target: int | "left"/"right"/"primary"/"second"
    focus(hwnd) / minimize(hwnd) / maximize(hwnd) / restore(hwnd) / close_window(hwnd) -> bool
    minimize_all_except(name) -> (kept_title|None, minimized_count)
"""

import ctypes
from ctypes import wintypes
from typing import Optional

try:
    import mss as _mss
except Exception:                                    # pragma: no cover
    _mss = None

try:
    import psutil as _psutil
except Exception:                                    # pragma: no cover
    _psutil = None

_logger = None


def set_logger(logger) -> None:
    global _logger
    _logger = logger


# Friendly aliases → substrings that appear in the window title or process name
_ALIASES = {
    "vs code": "code", "vscode": "code", "visual studio code": "code",
    "discord": "discord", "chrome": "chrome", "google chrome": "chrome",
    "firefox": "firefox", "edge": "msedge", "spotify": "spotify",
    "lightroom": "lightroom", "photoshop": "photoshop", "premiere": "premiere",
    "explorer": "explorer", "file explorer": "explorer", "notepad": "notepad",
    "terminal": "windowsterminal", "vlc": "vlc", "obs": "obs",
}

_SW = {"minimize": 6, "maximize": 3, "restore": 9, "show": 5}
_GWL_EXSTYLE = -20
_WS_EX_TOOLWINDOW = 0x00000080
_WM_CLOSE = 0x0010

try:
    _u = ctypes.windll.user32
    _u.GetWindowTextLengthW.argtypes = [wintypes.HWND]
    _u.GetWindowTextW.argtypes = [wintypes.HWND, wintypes.LPWSTR, ctypes.c_int]
    _u.IsWindowVisible.argtypes = [wintypes.HWND]
    _u.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
    _u.SetForegroundWindow.argtypes = [wintypes.HWND]
    _u.IsIconic.argtypes = [wintypes.HWND]
    _u.GetWindowRect.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.RECT)]
    _u.SetWindowPos.argtypes = [wintypes.HWND, wintypes.HWND, ctypes.c_int,
                                ctypes.c_int, ctypes.c_int, ctypes.c_int,
                                wintypes.UINT]
    _u.GetWindowThreadProcessId.argtypes = [wintypes.HWND,
                                            ctypes.POINTER(wintypes.DWORD)]
    _AVAILABLE = True
except Exception:                                    # pragma: no cover
    _AVAILABLE = False

_WNDENUMPROC = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND,
                                  wintypes.LPARAM)


def _proc_name(pid: int) -> str:
    if _psutil is None or not pid:
        return ""
    try:
        return _psutil.Process(pid).name().lower()
    except Exception:
        return ""


def list_windows() -> list:
    """Visible, titled, non-tool top-level windows."""
    out: list = []
    if not _AVAILABLE:
        return out

    def _cb(hwnd, _lparam):
        try:
            if not _u.IsWindowVisible(hwnd):
                return True
            ex = _u.GetWindowLongW(hwnd, _GWL_EXSTYLE) if hasattr(
                _u, "GetWindowLongW") else 0
            if ex & _WS_EX_TOOLWINDOW:
                return True
            n = _u.GetWindowTextLengthW(hwnd)
            if n <= 0:
                return True
            buf = ctypes.create_unicode_buffer(n + 1)
            _u.GetWindowTextW(hwnd, buf, n + 1)
            title = buf.value
            if not title or title == "Program Manager":
                return True
            pid = wintypes.DWORD()
            _u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
            out.append({"hwnd": hwnd, "title": title, "pid": pid.value,
                        "proc": _proc_name(pid.value)})
        except Exception:
            pass
        return True

    try:
        _u.EnumWindows(_WNDENUMPROC(_cb), 0)
    except Exception as e:
        if _logger:
            _logger.debug("winctl EnumWindows failed: %s", e)
    return out


def _norm(name: str) -> str:
    name = (name or "").strip().lower()
    return _ALIASES.get(name, name)


def find_window(name: str):
    """hwnd of the best window matching *name* (alias / title / process)."""
    key = _norm(name)
    if not key:
        return None
    wins = list_windows()
    # prefer a title match, then a process-name match
    for w in wins:
        if key in w["title"].lower():
            return w["hwnd"]
    for w in wins:
        if key and key in w["proc"]:
            return w["hwnd"]
    return None


def monitors() -> list:
    """Physical monitors, left→right, as {left, top, width, height}."""
    if _mss is None:
        return []
    try:
        with _mss.mss() as sct:
            mons = sct.monitors[1:]          # [0] is the virtual "all" monitor
        return sorted(
            ({"left": m["left"], "top": m["top"],
              "width": m["width"], "height": m["height"]} for m in mons),
            key=lambda m: m["left"])
    except Exception:
        return []


def _resolve_monitor(target) -> Optional[dict]:
    mons = monitors()
    if not mons:
        return None
    if isinstance(target, int):
        return mons[target] if 0 <= target < len(mons) else None
    t = str(target).lower()
    if t in ("left", "1st", "first", "primary", "main", "one"):
        return mons[0]
    if t in ("right", "second", "2nd", "secondary", "two", "next", "other"):
        return mons[1] if len(mons) > 1 else mons[0]
    if t in ("third", "3rd", "three"):
        return mons[2] if len(mons) > 2 else mons[-1]
    return None


def move_to_monitor(hwnd, target) -> bool:
    """Move *hwnd* onto the target monitor, keeping its size (clamped)."""
    if not _AVAILABLE or not hwnd:
        return False
    mon = _resolve_monitor(target)
    if not mon:
        return False
    try:
        if _u.IsIconic(hwnd):
            _u.ShowWindow(hwnd, _SW["restore"])
        rect = wintypes.RECT()
        _u.GetWindowRect(hwnd, ctypes.byref(rect))
        w = min(rect.right - rect.left, mon["width"])
        h = min(rect.bottom - rect.top, mon["height"])
        x = mon["left"] + max(0, (mon["width"] - w) // 8)
        y = mon["top"] + max(0, (mon["height"] - h) // 8)
        SWP_NOZORDER, SWP_SHOWWINDOW = 0x0004, 0x0040
        _u.SetWindowPos(hwnd, None, x, y, w, h, SWP_NOZORDER | SWP_SHOWWINDOW)
        try:
            _u.SetForegroundWindow(hwnd)
        except Exception:
            pass
        return True
    except Exception as e:
        if _logger:
            _logger.debug("winctl move failed: %s", e)
        return False


def _show(hwnd, mode: str) -> bool:
    if not _AVAILABLE or not hwnd:
        return False
    try:
        _u.ShowWindow(hwnd, _SW[mode])
        return True
    except Exception:
        return False


def minimize(hwnd) -> bool:
    return _show(hwnd, "minimize")


def maximize(hwnd) -> bool:
    return _show(hwnd, "maximize")


def restore(hwnd) -> bool:
    return _show(hwnd, "restore")


def focus(hwnd) -> bool:
    if not _AVAILABLE or not hwnd:
        return False
    try:
        if _u.IsIconic(hwnd):
            _u.ShowWindow(hwnd, _SW["restore"])
        return bool(_u.SetForegroundWindow(hwnd))
    except Exception:
        return False


def close_window(hwnd) -> bool:
    if not _AVAILABLE or not hwnd:
        return False
    try:
        _u.PostMessageW(hwnd, _WM_CLOSE, 0, 0)
        return True
    except Exception:
        return False


def minimize_all_except(name: str):
    """Minimise every visible app window except the one matching *name*.
    Returns (kept_title_or_None, minimized_count)."""
    key = _norm(name)
    kept = None
    count = 0
    for w in list_windows():
        is_keep = key and (key in w["title"].lower() or key in w["proc"])
        if is_keep:
            kept = w["title"]
            continue
        if minimize(w["hwnd"]):
            count += 1
    return kept, count
