#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Find & open a target application by NAME — no exact title or path needed.

Give it a human name ("Data Car"); it:
  1. looks at the currently OPEN windows and fuzzy-matches the name -> if the app
     is already running, it focuses that window and returns.
  2. if not open, locates the program via the Start Menu (.lnk), the uninstall
     registry, and Program Files, fuzzy-matching the name -> launches it, waits
     for its window, focuses it.
  3. only if it genuinely cannot find or launch it does it return an error
     listing the closest candidates it saw (so a human can pick).

Windows-only at runtime; imports cleanly on Linux (guards return errors).
"""

from __future__ import annotations

import difflib
import glob
import os
import sys
import time

IS_WINDOWS = sys.platform.startswith("win")


def _ratio(a, b):
    return difflib.SequenceMatcher(None, (a or "").lower(), (b or "").lower()).ratio()


def _score(name, candidate):
    """High score if name is a substring of candidate, else fuzzy ratio."""
    n, c = (name or "").lower().strip(), (candidate or "").lower().strip()
    if not n or not c:
        return 0.0
    if n in c or c in n:
        return 0.92 + 0.08 * _ratio(n, c)
    # token overlap helps multi-word names ("data car")
    nt, ct = set(n.split()), set(c.split())
    overlap = len(nt & ct) / max(1, len(nt))
    return max(_ratio(n, c), 0.6 * overlap)


# --------------------------------------------------------------------------
# Open-window enumeration + focus (ctypes user32)
# --------------------------------------------------------------------------
def enumerate_windows():
    if not IS_WINDOWS:
        return []
    import ctypes
    from ctypes import wintypes
    u = ctypes.windll.user32
    out = []
    EnumProc = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)

    def cb(hwnd, _l):
        if not u.IsWindowVisible(hwnd):
            return True
        n = u.GetWindowTextLengthW(hwnd)
        if n:
            buf = ctypes.create_unicode_buffer(n + 1)
            u.GetWindowTextW(hwnd, buf, n + 1)
            if buf.value.strip():
                out.append((int(hwnd), buf.value))
        return True

    u.EnumWindows(EnumProc(cb), 0)
    return out


def find_open_window(name, min_score=0.55):
    best, best_s = None, 0.0
    ranked = []
    for hwnd, title in enumerate_windows():
        s = _score(name, title)
        ranked.append((round(s, 2), title))
        if s > best_s:
            best_s, best = s, (hwnd, title)
    ranked.sort(reverse=True)
    if best and best_s >= min_score:
        return best, ranked[:5]
    return None, ranked[:5]


def focus_window(hwnd):
    if not IS_WINDOWS:
        return
    import ctypes
    u = ctypes.windll.user32
    u.ShowWindow(hwnd, 9)          # SW_RESTORE
    u.SetForegroundWindow(hwnd)
    time.sleep(0.4)


# --------------------------------------------------------------------------
# Locate an installed program to LAUNCH (Start Menu / registry / Program Files)
# --------------------------------------------------------------------------
def _start_menu_candidates():
    dirs = []
    for env in ("ProgramData", "APPDATA"):
        base = os.environ.get(env)
        if base:
            dirs.append(os.path.join(base, "Microsoft", "Windows",
                                     "Start Menu", "Programs"))
    out = []
    for d in dirs:
        if os.path.isdir(d):
            for lnk in glob.glob(os.path.join(d, "**", "*.lnk"), recursive=True):
                out.append(lnk)
    return out


def _registry_candidates():
    if not IS_WINDOWS:
        return []
    import winreg  # type: ignore
    out = []
    roots = [(winreg.HKEY_LOCAL_MACHINE,
              r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"),
             (winreg.HKEY_LOCAL_MACHINE,
              r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"),
             (winreg.HKEY_CURRENT_USER,
              r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")]
    for hive, path in roots:
        try:
            k = winreg.OpenKey(hive, path)
        except OSError:
            continue
        i = 0
        while True:
            try:
                sub = winreg.EnumKey(k, i); i += 1
            except OSError:
                break
            try:
                sk = winreg.OpenKey(k, sub)
                name = _reg_val(sk, "DisplayName")
                if not name:
                    continue
                exe = _reg_val(sk, "DisplayIcon") or ""
                exe = exe.split(",")[0].strip('"')
                loc = _reg_val(sk, "InstallLocation") or ""
                out.append({"name": name, "exe": exe, "loc": loc})
            except OSError:
                continue
    return out


def _reg_val(key, val):
    import winreg  # type: ignore
    try:
        v, _ = winreg.QueryValueEx(key, val)
        return v
    except OSError:
        return None


def find_launch_target(name, min_score=0.6):
    """Return a path to launch (a .lnk or .exe) for `name`, or (None, candidates)."""
    scored = []
    # Start menu shortcuts — launch the .lnk directly (Windows resolves it).
    for lnk in _start_menu_candidates():
        base = os.path.splitext(os.path.basename(lnk))[0]
        scored.append((_score(name, base), lnk, base))
    # Registry installed programs
    for entry in _registry_candidates():
        s = _score(name, entry["name"])
        target = entry["exe"] if entry["exe"].lower().endswith(".exe") \
            and os.path.exists(entry["exe"]) else None
        if not target and entry["loc"] and os.path.isdir(entry["loc"]):
            exes = glob.glob(os.path.join(entry["loc"], "*.exe"))
            if exes:
                exes.sort(key=lambda p: _score(name, os.path.basename(p)), reverse=True)
                target = exes[0]
        if target:
            scored.append((s, target, entry["name"]))
    scored.sort(key=lambda t: t[0], reverse=True)
    cands = [(round(s, 2), os.path.basename(str(p)), nm) for s, p, nm in scored[:6]]
    if scored and scored[0][0] >= min_score:
        return scored[0][1], cands
    return None, cands


# --------------------------------------------------------------------------
# The one entry point
# --------------------------------------------------------------------------
def open_target(name, launch_wait=12):
    """Attach to (or find+launch) the app named `name`. Returns a dict:
       {ok, how, hwnd, title, candidates, error}."""
    if not IS_WINDOWS:
        return {"ok": False, "error": "find_app runs on Windows only"}

    # 1) already open?
    win, ranked = find_open_window(name)
    if win:
        hwnd, title = win
        focus_window(hwnd)
        return {"ok": True, "how": "attached", "hwnd": hwnd, "title": title,
                "candidates": ranked}

    # 2) locate + launch
    target, cands = find_launch_target(name)
    if target:
        try:
            os.startfile(target)  # type: ignore[attr-defined]
        except Exception as e:  # noqa: BLE001
            return {"ok": False, "error": "found %r but launch failed: %s"
                    % (target, e), "candidates": cands}
        deadline = time.time() + launch_wait
        while time.time() < deadline:
            time.sleep(1.0)
            win, ranked = find_open_window(name)
            if win:
                hwnd, title = win
                focus_window(hwnd)
                return {"ok": True, "how": "launched", "hwnd": hwnd,
                        "title": title, "launched": target, "candidates": ranked}
        return {"ok": False, "error": "launched %r but no matching window appeared "
                "in %ds" % (target, launch_wait), "candidates": cands}

    # 3) give up — hand back what we saw so a human can pick
    return {"ok": False,
            "error": "could not find an OPEN window or an INSTALLED program "
                     "matching %r" % name,
            "open_windows_top": ranked,
            "install_candidates": cands}
