"""uia.py — BACKGROUND UI Automation for KiCad, via .NET UIAutomation (PowerShell).

Why: opening the Footprint Editor / 3D viewer used to fall back to clicking a
toolbar icon (moves the user's MOUSE) or an Alt+3 keystroke (needs the window
FOREGROUNDED). Both hijack the desktop. UIA's Invoke pattern triggers a control
PROGRAMMATICALLY — no foreground, no focus steal, no cursor movement. KiCad is
wxWidgets but exposes an invokable UIA tree, so its toolbar buttons / menu items
are reachable by accessible name.

Self-contained: shells out to `powershell` (present on every Windows box) which
loads System.Windows.Automation. NOT Bridge's gated `run_script` — this is the bridge's
OWN subprocess (same as how it runs kicad-cli), so no shell-approval is involved.
UIA is a mature, stable API (unlike the thin/undocumented Win32 menu-ID path).
"""

import json
import subprocess
import sys

IS_WINDOWS = sys.platform == "win32"

# PowerShell that: attaches to a window by hwnd, finds a control by exact name or
# name-substring (optionally filtered by control type), and Invokes it (or, for
# SetValue, sets its text). Emits a one-line JSON result. All background.
_PS_TEMPLATE = r'''
$ErrorActionPreference = 'Stop'
try {
  Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
  $AE  = [System.Windows.Automation.AutomationElement]
  $TS  = [System.Windows.Automation.TreeScope]
  $root = $AE::FromHandle([IntPtr]__HWND__)
  if ($root -eq $null) { '{"ok":false,"reason":"window_not_found"}'; exit }

  $name     = __NAME__
  $contains = __CONTAINS__
  $action   = '__ACTION__'
  $text     = __TEXT__

  $target = $null
  if ($name) {
    $cond = New-Object System.Windows.Automation.PropertyCondition($AE::NameProperty, $name)
    $target = $root.FindFirst($TS::Descendants, $cond)
  }
  if ($target -eq $null -and $contains) {
    # bounded FindAll, filter by name substring. For an invoke we must pick a control
    # that ACTUALLY supports InvokePattern — KiCad exposes the label "Footprint Editor"
    # AND the toolbar button with the same name; grabbing the label yields
    # not_invokable. So when action=invoke, prefer an invokable match (fall back to
    # first substring match only if none is invokable).
    $all = $root.FindAll($TS::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
    $firstMatch = $null
    foreach ($el in $all) {
      $n = $el.Current.Name
      if ($n -and $n.ToLower().Contains($contains.ToLower())) {
        if ($firstMatch -eq $null) { $firstMatch = $el }
        if ($action -eq 'invoke') {
          $tmp = $null
          if ($el.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$tmp)) { $target = $el; break }
        } elseif ($action -eq 'setvalue') {
          $tmp = $null
          if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$tmp)) { $target = $el; break }
        } else { $target = $el; break }
      }
    }
    if ($target -eq $null) { $target = $firstMatch }
  }
  if ($target -eq $null) { '{"ok":false,"reason":"control_not_found"}'; exit }

  $foundName = $target.Current.Name
  if ($action -eq 'invoke') {
    $p = $null
    if ($target.TryGetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern, [ref]$p)) {
      $p.Invoke()
      '{"ok":true,"action":"invoke","name":' + ($foundName | ConvertTo-Json) + '}'
    } else {
      # some KiCad menu items expose ExpandCollapse or are only Selectable
      $sp = $null
      if ($target.TryGetCurrentPattern([System.Windows.Automation.SelectionItemPattern]::Pattern, [ref]$sp)) {
        $sp.Select(); '{"ok":true,"action":"select","name":' + ($foundName | ConvertTo-Json) + '}'
      } else { '{"ok":false,"reason":"not_invokable","name":' + ($foundName | ConvertTo-Json) + '}' }
    }
  } elseif ($action -eq 'setvalue') {
    $vp = $null
    if ($target.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$vp)) {
      $vp.SetValue($text)
      '{"ok":true,"action":"setvalue","name":' + ($foundName | ConvertTo-Json) + '}'
    } else { '{"ok":false,"reason":"not_settable","name":' + ($foundName | ConvertTo-Json) + '}' }
  } else {
    '{"ok":true,"action":"find","name":' + ($foundName | ConvertTo-Json) + ',"found":true}'
  }
} catch {
  '{"ok":false,"reason":"exception","error":' + ($_.Exception.Message | ConvertTo-Json) + '}'
}
'''


def _ps_json(value) -> str:
    """Render a Python value as a PowerShell literal (string or $null)."""
    if value is None:
        return "$null"
    return json.dumps(str(value))  # JSON string == a valid PS single/double-quoted string


def _run(hwnd: int, action: str, name=None, contains=None, text=None, timeout: float = 8.0) -> dict:
    if not IS_WINDOWS:
        return {"ok": False, "reason": "not_windows"}
    script = (_PS_TEMPLATE
              .replace("__HWND__", str(int(hwnd)))
              .replace("__NAME__", _ps_json(name))
              .replace("__CONTAINS__", _ps_json(contains))
              .replace("__ACTION__", action)
              .replace("__TEXT__", _ps_json(text)))
    try:
        proc = subprocess.run(
            ["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
            capture_output=True, text=True, timeout=timeout,
        )
        out = (proc.stdout or "").strip().splitlines()
        for line in reversed(out):  # last non-empty line is our JSON
            line = line.strip()
            if line.startswith("{"):
                return json.loads(line)
        return {"ok": False, "reason": "no_output", "stderr": (proc.stderr or "")[:200]}
    except subprocess.TimeoutExpired:
        return {"ok": False, "reason": "timeout"}
    except Exception as e:  # pylint: disable=broad-except
        return {"ok": False, "reason": "run_failed", "error": str(e)[:200]}


def uia_invoke(hwnd: int, name: str = None, contains: str = None, timeout: float = 8.0) -> dict:
    """Background-Invoke a control (toolbar button / menu item) in the given window,
    matched by exact accessible name OR name substring. No foreground, no cursor.
    Returns {ok, action, name} / {ok:False, reason}."""
    return _run(hwnd, "invoke", name=name, contains=contains, timeout=timeout)


def uia_set_value(hwnd: int, text: str, name: str = None, contains: str = None, timeout: float = 8.0) -> dict:
    """Background-SetValue on a text control (e.g. a search box) — no focus/typing."""
    return _run(hwnd, "setvalue", name=name, contains=contains, text=text, timeout=timeout)


def uia_find(hwnd: int, name: str = None, contains: str = None, timeout: float = 6.0) -> dict:
    """Read-only: is the control present in the UIA tree? {ok, found, name}."""
    return _run(hwnd, "find", name=name, contains=contains, timeout=timeout)