"""Handler for open_symbol_editor command.

Opens the KiCad Symbol Editor and optionally loads a specific symbol.

Windows: two-phase Win32 approach —
  1. Send WM_COMMAND 20012 to the KiCad project manager to open the Symbol Editor
  2. If a symbolName is specified, call navigate_symbol.ps1 which clicks the
     search box, types the symbol name via SendKeys, and sends Ctrl+Shift+E.

macOS: launches the Symbol Editor app directly via `open -a`. Symbol-name
navigation requires Accessibility-framework permission for UI scripting,
which we don't request — handlers report this limitation in the response.
"""

import ctypes
import ctypes.wintypes
import subprocess
import sys
import time
from pathlib import Path

IS_WINDOWS = sys.platform == "win32"
IS_MACOS = sys.platform == "darwin"

# Win32 constants — defined cross-platform so module-level references don't
# error on macOS. The Win32 helpers below only use them through user32/kernel32,
# which are None on macOS; handlers branch on platform before calling helpers.
WM_COMMAND = 0x0111
# v1.7.6 fix (KiCad v2 Phase 4 menu dump audit): KiCad 10's Project
# Manager Tools menu IDs are: 20010=Schematic Editor, 20011=Symbol Editor,
# 20012=PCB Editor, 20013=Footprint Editor, 20014=Gerber Viewer.
# Previous value of 20012 sent the wrong command (PCB Editor) so this
# handler silently failed for KiCad 10 users — Symbol Editor never opened,
# PCB Editor sometimes flashed instead. Caught when Phase 3 tier-1 routing
# also sent 20012 to the bridge and got identical "did not appear" failure.
KICAD_SYMBOL_EDITOR_CMD_ID = 20011  # Project Manager > Tools > Symbol Editor
SW_RESTORE = 9
GW_OWNER = 4

if IS_WINDOWS:
    user32 = ctypes.windll.user32
    kernel32 = ctypes.windll.kernel32
else:
    user32 = None
    kernel32 = None

# Keep callback references alive to prevent GC
_callbacks = []

# Path to the navigate_symbol.ps1 script (same directory as the plugin root)
_SCRIPT_DIR = Path(__file__).resolve().parent.parent
_NAVIGATE_SCRIPT = _SCRIPT_DIR / "navigate_symbol.ps1"


def _find_windows_by_title(substring: str) -> list:
    """Find all visible windows whose title contains the given substring."""
    results = []
    WNDENUMPROC = ctypes.WINFUNCTYPE(
        ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM
    )

    def callback(hwnd, _lparam):
        if user32.IsWindowVisible(hwnd):
            length = user32.GetWindowTextLengthW(hwnd)
            if length > 0:
                buf = ctypes.create_unicode_buffer(length + 1)
                user32.GetWindowTextW(hwnd, buf, length + 1)
                if substring in buf.value:
                    owner = user32.GetWindow(hwnd, GW_OWNER)
                    results.append((hwnd, buf.value, owner))
        return True

    cb = WNDENUMPROC(callback)
    _callbacks.append(cb)
    user32.EnumWindows(cb, 0)
    _callbacks.remove(cb)
    return results


def _find_kicad_manager() -> int | None:
    """Find the KiCad project manager window handle."""
    windows = _find_windows_by_title("KiCad")
    for hwnd, title, owner in windows:
        lower = title.lower()
        if ("editor" not in lower
                and "viewer" not in lower
                and "calculator" not in lower
                and owner == 0):
            return hwnd
    return None


def _find_symbol_editor() -> int | None:
    """Find an open Symbol Editor window."""
    windows = _find_windows_by_title("Symbol Editor")
    for hwnd, title, _owner in windows:
        if "symbol editor" in title.lower():
            return hwnd
    return None


def _find_child_by_class_and_parent(parent_hwnd: int, child_class: str, parent_text: str) -> int | None:
    """Find a child window by class name and parent window text."""
    result = [None]
    WNDENUMPROC = ctypes.WINFUNCTYPE(
        ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM
    )

    def callback(hwnd, _lparam):
        cls_buf = ctypes.create_unicode_buffer(256)
        user32.GetClassNameW(hwnd, cls_buf, 256)
        if cls_buf.value == child_class:
            parent = user32.GetParent(hwnd)
            p_buf = ctypes.create_unicode_buffer(256)
            user32.GetWindowTextW(parent, p_buf, 256)
            if p_buf.value == parent_text:
                result[0] = hwnd
                return False
        return True

    cb = WNDENUMPROC(callback)
    _callbacks.append(cb)
    user32.EnumChildWindows(parent_hwnd, cb, 0)
    _callbacks.remove(cb)
    return result[0]


def _force_foreground(hwnd: int):
    """Bring a window to the foreground."""
    user32.ShowWindow(hwnd, SW_RESTORE)
    user32.SetForegroundWindow(hwnd)
    user32.BringWindowToTop(hwnd)


def _navigate_to_symbol(sym_hwnd: int, symbol_name: str) -> dict:
    """Call the PowerShell script to navigate to a specific symbol.

    Finds the search Edit control HWND from Python (reliable cross-process),
    then passes it to the PowerShell script which handles focus, typing, and shortcuts.

    Returns a dict with 'status' ('ok', 'partial', 'noload', 'error') and 'title' or 'error'.
    """
    if not _NAVIGATE_SCRIPT.exists():
        return {"status": "error", "error": f"Script not found: {_NAVIGATE_SCRIPT}"}

    # Find the search Edit control from Python (more reliable than C# cross-process)
    search_edit = _find_child_by_class_and_parent(sym_hwnd, "Edit", "searchCtrl")
    if not search_edit:
        return {"status": "error", "error": "Search edit control not found in Symbol Editor"}

    try:
        subprocess.run(
            [
                "powershell", "-ExecutionPolicy", "Bypass",
                "-File", str(_NAVIGATE_SCRIPT),
                "-SymEditorHwnd", str(sym_hwnd),
                "-SearchEditHwnd", str(search_edit),
                "-SymbolName", symbol_name,
            ],
            capture_output=True, text=True, timeout=20,
        )
    except subprocess.TimeoutExpired:
        pass  # Script timed out, but actions may have been performed
    except Exception:
        pass  # Script may crash during .NET cleanup but actions still succeed

    # Verify result from Python by checking the window title directly.
    # The PowerShell script often crashes during .NET cleanup (0xC0000409)
    # but the actual Win32 actions (focus, click, type, shortcut) succeed.
    time.sleep(0.5)

    # Re-find the Symbol Editor (HWND may have changed if editor was reopened)
    sym_hwnd_check = _find_symbol_editor()
    if not sym_hwnd_check:
        return {"status": "error", "error": "Symbol Editor closed unexpectedly"}

    windows = _find_windows_by_title("Symbol Editor")
    for _, title, _ in windows:
        if symbol_name in title:
            return {"status": "ok", "title": title}
        if "symbol editor" in title.lower() and "[no symbol loaded]" not in title:
            return {"status": "partial", "title": title}

    return {"status": "noload", "title": "[no symbol loaded]"}


def _mac_open_symbol_editor(kicad_info: dict, args: dict) -> dict:
    """macOS: launch the Symbol Editor app directly. Symbol/library navigation
    after launch is not supported (would require Accessibility permission)."""
    parent = Path(kicad_info.get("base_dir", "")).parent
    sym_app = parent / "Symbol Editor.app"
    if not sym_app.exists():
        return {
            "success": False,
            "error": f"Symbol Editor.app not found at {sym_app}",
            "_hint": "Reinstall KiCad — the Symbol Editor app bundle is missing.",
        }

    try:
        subprocess.Popen(
            ["open", "-a", str(sym_app)],
            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
    except Exception as e:
        return {"success": False, "error": f"Failed to launch Symbol Editor: {e}"}

    symbol_name = args.get("symbolName", "")
    library_name = args.get("libraryName", "")
    if symbol_name or library_name:
        return {
            "success": True,
            "output": "Symbol Editor opened. Navigation to a specific symbol is not implemented on macOS.",
            "_hint": ("On macOS, ask the user to type the symbol name in the editor's "
                      "search box manually. UI automation requires Accessibility permission."),
        }
    return {"success": True, "output": "Symbol Editor opened."}


def handle_open_symbol_editor(kicad_info: dict, args: dict) -> dict:
    """Open the KiCad Symbol Editor and optionally load a specific symbol.

    Args:
        kicad_info: KiCad detection info dict.
        args: Optional dict with:
            - symbolName: Name of symbol to load (e.g. "ADOM_MCU").
            - libraryName: Name of the library containing the symbol.
    """
    if not kicad_info.get("installed"):
        return {
            "success": False,
            "error": "KiCad not installed",
            "_hint": "Report to the user and ask them to install KiCad from https://www.kicad.org/download/, then retry.",
        }

    if IS_MACOS:
        return _mac_open_symbol_editor(kicad_info, args or {})

    if not IS_WINDOWS:
        return {"success": False, "error": f"open_symbol_editor not implemented on {sys.platform!r}"}

    symbol_name = args.get("symbolName", "")
    library_name = args.get("libraryName", "")

    # Check if the symbol is already loaded in the Symbol Editor
    if symbol_name:
        sym_hwnd = _find_symbol_editor()
        if sym_hwnd:
            windows = _find_windows_by_title("Symbol Editor")
            for hwnd, title, _ in windows:
                if symbol_name in title:
                    _force_foreground(hwnd)
                    return {
                        "success": True,
                        "output": f"Symbol '{symbol_name}' is already loaded in the Symbol Editor.",
                    }

    # --- Phase 1: Ensure Symbol Editor is open ---

    sym_hwnd = _find_symbol_editor()
    pathways_tried: list[str] = []

    if not sym_hwnd:
        # Phase 3 Tier 1: try the reverse bridge first. Two sub-paths
        # because Symbol Editor is hosted INSIDE eeschema.exe (not as a
        # separate process):
        #   1a) If eeschema is already running, send menu_id 20390 to
        #       its Schematic Editor frame — proven path from Phase 2
        #       MVP testing. Opens Symbol Editor as a sub-window in the
        #       existing eeschema process.
        #   1b) Else, send menu_id 20011 to kicad.exe Project Manager's
        #       Tools menu — this spawns a NEW eeschema.exe with Symbol
        #       Editor as its top-level window. Slower (new process) but
        #       works from a cold start.
        try:
            from handlers.bridge_client import try_wm_command_via_bridge

            # 1a: prefer eeschema if alive
            tier1 = try_wm_command_via_bridge(
                exe_name="eeschema", menu_id=20390, frame_index=0,
            )
            tier1_source = "eeschema-20390"
            if not tier1.get("success"):
                # 1b: fall through to kicad project manager
                tier1_pm = try_wm_command_via_bridge(
                    exe_name="kicad", menu_id=KICAD_SYMBOL_EDITOR_CMD_ID, frame_index=0,
                )
                if tier1_pm.get("success"):
                    tier1 = tier1_pm
                    tier1_source = "kicad-20011"
            pathways_tried.append(
                f"plugin-{tier1_source}" if tier1.get("success")
                else f"plugin-failed:{tier1.get('errorCode','')}"
            )
            if tier1.get("success"):
                for _ in range(20):
                    time.sleep(0.5)
                    sym_hwnd = _find_symbol_editor()
                    if sym_hwnd:
                        break
                if sym_hwnd:
                    _force_foreground(sym_hwnd)
                    if not symbol_name and not library_name:
                        return {
                            "success": True,
                            "output": f"Symbol Editor opened successfully via bridge ({tier1_source}).",
                            "pathway": "plugin",
                            "pathwaysTried": pathways_tried,
                            "bridgePid": tier1.get("pid"),
                        }
                    # If symbol_name/library_name was requested, fall
                    # through to the navigate step below (uses legacy
                    # PowerShell SendKeys path).
        except Exception as e:  # pylint: disable=broad-except
            # Bridge import failed (unlikely) — record and fall through.
            pathways_tried.append(f"plugin-import-error:{type(e).__name__}")

    if not sym_hwnd:
        # Tier 2: legacy Win32 PostMessage from outside the KiCad process.
        # Find or launch KiCad project manager
        kicad_hwnd = _find_kicad_manager()

        if not kicad_hwnd:
            kicad_exe = kicad_info.get("kicad_exe")
            if not kicad_exe or not Path(kicad_exe).exists():
                return {
                    "success": False,
                    "error": "KiCad executable not found",
                    "_hint": "Report to the user and ask them to reinstall KiCad — the installation appears corrupted.",
                }

            try:
                subprocess.Popen(
                    [kicad_exe],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
            except Exception as e:
                return {
                    "success": False,
                    "error": f"Failed to launch KiCad: {e}",
                    "_hint": "Call close_kicad with {\"force\": true} to clear stuck processes, then retry.",
                }

            for _ in range(20):
                time.sleep(0.5)
                kicad_hwnd = _find_kicad_manager()
                if kicad_hwnd:
                    break

            if not kicad_hwnd:
                return {
                    "success": False,
                    "error": "KiCad launched but project manager window not found after 10s",
                    "_hint": "Call screenshot_all to see KiCad's current state, or call close_kicad with {\"force\": true} and retry.",
                }
            time.sleep(2.0)

        # Open Symbol Editor via WM_COMMAND
        user32.PostMessageW(kicad_hwnd, WM_COMMAND, KICAD_SYMBOL_EDITOR_CMD_ID, 0)
        pathways_tried.append("wm_command")

        for _ in range(20):
            time.sleep(0.5)
            sym_hwnd = _find_symbol_editor()
            if sym_hwnd:
                break

        if not sym_hwnd:
            return {
                "success": False,
                "error": "WM_COMMAND sent to KiCad but Symbol Editor did not appear within 10s.",
                "pathwaysTried": pathways_tried,
                "_hint": "Call screenshot_all to check for a modal dialog blocking KiCad, dismiss with send_key escape, then retry.",
            }

        # Wait for Symbol Editor to fully initialize
        time.sleep(1.5)

    # --- Phase 2: Navigate to specific symbol (if requested) ---

    if not symbol_name and not library_name:
        _force_foreground(sym_hwnd)
        return {"success": True, "output": "Symbol Editor opened successfully.",
                "pathwaysTried": pathways_tried}

    search_text = symbol_name or library_name

    # NOTE: Phase 2.aa shipped a `select_and_load_symbol` bridge RPC that
    # would normally replace the legacy PowerShell SendKeys path here.
    # However, the v0.5.0 implementation segfaults eeschema when walking
    # the wx.dataview.DataViewModel — needs a different approach
    # (likely keystroke simulation: WXK_DOWN + WXK_RETURN to the tree).
    # Until v2.ab fixes that, we deliberately DO NOT auto-route through
    # the bridge here. The bridge RPC remains callable manually for
    # users who want to experiment, but the default kicad_open_symbol_editor
    # flow keeps using the proven PowerShell path.

    nav_result = _navigate_to_symbol(sym_hwnd, search_text)

    if nav_result["status"] == "ok":
        return {
            "success": True,
            "output": f"Symbol '{search_text}' loaded in the Symbol Editor: {nav_result['title']}",
        }
    elif nav_result["status"] == "partial":
        return {
            "success": True,
            "output": f"Symbol Editor loaded: {nav_result['title']}",
        }
    elif nav_result["status"] == "noload":
        # The search typed but Ctrl+Shift+E didn't load anything.
        # This can happen if the tree didn't filter in time.
        # Try once more with a longer wait.
        time.sleep(0.5)
        nav_result2 = _navigate_to_symbol(sym_hwnd, search_text)
        if nav_result2["status"] == "ok":
            return {
                "success": True,
                "output": f"Symbol '{search_text}' loaded in the Symbol Editor: {nav_result2['title']}",
            }
        elif nav_result2["status"] == "partial":
            return {
                "success": True,
                "output": f"Symbol Editor loaded: {nav_result2['title']}",
            }
        else:
            return {
                "success": True,
                "output": f"Symbol Editor opened and searched for '{search_text}'. "
                          f"Use Ctrl+Shift+E to load the symbol if it didn't auto-load.",
            }
    else:
        # Error from the script
        _force_foreground(sym_hwnd)
        return {
            "success": True,
            "output": f"Symbol Editor opened but navigation failed: {nav_result.get('error', 'unknown')}. "
                      f"Search for '{search_text}' manually.",
        }