"""Electron and CEF application detection and automation support.

Provides utilities to detect Electron/CEF-based applications, discover
existing DevTools ports, and launch Electron apps with remote debugging
enabled for CDP-based DOM automation.

Phase 5C.4 — Electron/CEF App Support.

Usage::

    from naturo.electron import detect_electron_app, get_debug_port
    from naturo.cdp import CDPClient

    info = detect_electron_app("Slack")
    if info["is_electron"]:
        port = info.get("debug_port")
        if port:
            client = CDPClient(port=port)
            tabs = client.list_tabs()
"""

from __future__ import annotations

import os
import platform
import re
import subprocess
from typing import Any, Dict, List, Optional

from naturo.errors import NaturoError


# Known Electron-based applications and their typical executable names
_KNOWN_ELECTRON_APPS: Dict[str, Dict[str, Any]] = {
    "slack": {"exe": "Slack.exe", "display": "Slack"},
    "discord": {"exe": "Discord.exe", "display": "Discord"},
    "vscode": {"exe": "Code.exe", "display": "Visual Studio Code"},
    "code": {"exe": "Code.exe", "display": "Visual Studio Code"},
    "teams": {"exe": "ms-teams.exe", "display": "Microsoft Teams"},
    "notion": {"exe": "Notion.exe", "display": "Notion"},
    "figma": {"exe": "Figma.exe", "display": "Figma"},
    "obsidian": {"exe": "Obsidian.exe", "display": "Obsidian"},
    "postman": {"exe": "Postman.exe", "display": "Postman"},
    "spotify": {"exe": "Spotify.exe", "display": "Spotify"},
    "whatsapp": {"exe": "WhatsApp.exe", "display": "WhatsApp"},
    "telegram": {"exe": "Telegram.exe", "display": "Telegram Desktop"},
    "signal": {"exe": "Signal.exe", "display": "Signal"},
    "1password": {"exe": "1Password.exe", "display": "1Password"},
    "bitwarden": {"exe": "Bitwarden.exe", "display": "Bitwarden"},
    "cursor": {"exe": "Cursor.exe", "display": "Cursor"},
    "windsurf": {"exe": "Windsurf.exe", "display": "Windsurf"},
}


def _require_windows() -> None:
    """Raise NaturoError if not running on Windows.

    Raises:
        NaturoError: If the current platform is not Windows.
    """
    if platform.system() != "Windows":
        raise NaturoError(
            code="PLATFORM_ERROR",
            message="Electron detection requires Windows.",
        )


def _get_process_command_line(pid: int) -> Optional[str]:
    """Get the full command line of a process by PID.

    Args:
        pid: Process ID.

    Returns:
        Full command line string, or None if inaccessible.
    """
    try:
        result = subprocess.run(
            [
                "wmic",
                "process",
                "where",
                f"ProcessId={pid}",
                "get",
                "CommandLine",
                "/format:list",
            ],
            capture_output=True,
            text=True,
            timeout=5,
        )
        for line in result.stdout.splitlines():
            if line.startswith("CommandLine="):
                return line[len("CommandLine=") :]
    except (subprocess.TimeoutExpired, OSError):
        pass
    return None


def _get_process_exe_path(pid: int) -> Optional[str]:
    """Get the executable path of a process by PID.

    Args:
        pid: Process ID.

    Returns:
        Executable path string, or None if inaccessible.
    """
    try:
        result = subprocess.run(
            [
                "wmic",
                "process",
                "where",
                f"ProcessId={pid}",
                "get",
                "ExecutablePath",
                "/format:list",
            ],
            capture_output=True,
            text=True,
            timeout=5,
        )
        for line in result.stdout.splitlines():
            if line.startswith("ExecutablePath="):
                return line[len("ExecutablePath=") :]
    except (subprocess.TimeoutExpired, OSError):
        pass
    return None


def _find_processes_by_name(name: str) -> List[Dict[str, Any]]:
    """Find running processes matching a name pattern.

    Args:
        name: Process name or partial match (case-insensitive).

    Returns:
        List of dicts with pid, name, exe_path, command_line.
    """
    processes: List[Dict[str, Any]] = []
    try:
        result = subprocess.run(
            ["tasklist", "/FO", "CSV", "/NH"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        name_lower = name.lower()
        for line in result.stdout.splitlines():
            line = line.strip()
            if not line:
                continue
            # CSV format: "Image Name","PID","Session Name","Session#","Mem Usage"
            parts = line.split('","')
            if len(parts) < 2:
                continue
            proc_name = parts[0].strip('"')
            try:
                pid = int(parts[1].strip('"'))
            except (ValueError, IndexError):
                continue
            if name_lower in proc_name.lower():
                processes.append({
                    "pid": pid,
                    "name": proc_name,
                })
    except (subprocess.TimeoutExpired, OSError):
        pass
    return processes


def _is_electron_process(pid: int) -> bool:
    """Check if a process is an Electron/CEF application.

    Checks for Electron-specific files adjacent to the executable
    or Electron-specific command line arguments.

    Args:
        pid: Process ID to check.

    Returns:
        True if the process appears to be Electron-based.
    """
    # Check command line for Electron indicators
    cmdline = _get_process_command_line(pid)
    if cmdline:
        electron_indicators = [
            "--type=renderer",
            "--type=gpu-process",
            "--type=utility",
            "electron",
            "resources/app.asar",
            "resources\\app.asar",
        ]
        cmdline_lower = cmdline.lower()
        if any(ind in cmdline_lower for ind in electron_indicators):
            return True

    # Check for Electron files next to the executable
    exe_path = _get_process_exe_path(pid)
    if exe_path:
        exe_dir = os.path.dirname(exe_path)
        electron_files = [
            "resources/app.asar",
            "resources\\app.asar",
            "electron.exe",
            "chrome_100_percent.pak",
            "icudtl.dat",
            "v8_context_snapshot.bin",
        ]
        for ef in electron_files:
            if os.path.exists(os.path.join(exe_dir, ef)):
                return True

    return False


def _find_debug_port_from_cmdline(pid: int) -> Optional[int]:
    """Extract --remote-debugging-port from a process command line.

    Args:
        pid: Process ID.

    Returns:
        Port number if found, None otherwise.
    """
    cmdline = _get_process_command_line(pid)
    if cmdline:
        match = re.search(r"--remote-debugging-port=(\d+)", cmdline)
        if match:
            return int(match.group(1))
    return None


def detect_electron_app(app_name: str) -> Dict[str, Any]:
    """Detect if a running application is Electron-based.

    Searches for processes matching the app name and checks for
    Electron/CEF characteristics.

    Args:
        app_name: Application name or process name to search for.

    Returns:
        Dict with keys:
            - is_electron: bool
            - app_name: str (display name)
            - processes: list of matching process dicts
            - debug_port: int or None (if already running with CDP)
            - main_pid: int or None (main process PID)

    Raises:
        NaturoError: If not on Windows or app not found.
    """
    _require_windows()

    # Normalize name for lookup
    name_lower = app_name.lower().replace(".exe", "")

    # Check known apps for the exe name
    known = _KNOWN_ELECTRON_APPS.get(name_lower)
    search_name = known["exe"] if known else app_name

    processes = _find_processes_by_name(search_name)
    if not processes:
        # Try the raw name too
        processes = _find_processes_by_name(app_name)

    if not processes:
        return {
            "is_electron": False,
            "app_name": app_name,
            "processes": [],
            "debug_port": None,
            "main_pid": None,
        }

    # Check each process for Electron characteristics
    is_electron = False
    debug_port = None
    main_pid = None

    for proc in processes:
        pid = proc["pid"]
        if _is_electron_process(pid):
            is_electron = True
            if main_pid is None:
                main_pid = pid
            port = _find_debug_port_from_cmdline(pid)
            if port is not None:
                debug_port = port
                break

    display_name = known["display"] if known else app_name

    return {
        "is_electron": is_electron,
        "app_name": display_name,
        "processes": processes,
        "debug_port": debug_port,
        "main_pid": main_pid,
    }


def list_electron_apps() -> Dict[str, Any]:
    """List all detected Electron/CEF applications currently running.

    Returns:
        Dict with keys:
            - apps: list of dicts with app_name, pid, debug_port, exe_name
            - count: int
    """
    _require_windows()

    apps: List[Dict[str, Any]] = []
    seen_pids: set = set()

    # Check known Electron apps
    for key, info in _KNOWN_ELECTRON_APPS.items():
        processes = _find_processes_by_name(info["exe"])
        for proc in processes:
            pid = proc["pid"]
            if pid in seen_pids:
                continue
            seen_pids.add(pid)
            if _is_electron_process(pid):
                debug_port = _find_debug_port_from_cmdline(pid)
                apps.append({
                    "app_name": info["display"],
                    "exe_name": proc["name"],
                    "pid": pid,
                    "debug_port": debug_port,
                    "debuggable": debug_port is not None,
                })

    return {"apps": apps, "count": len(apps)}


def get_debug_port(app_name: str) -> Optional[int]:
    """Get the DevTools debugging port of a running Electron app.

    Args:
        app_name: Application name to check.

    Returns:
        Port number if the app is running with remote debugging,
        None otherwise.
    """
    info = detect_electron_app(app_name)
    return info.get("debug_port")


def launch_with_debug(
    app_path: str,
    port: int = 9229,
    extra_args: Optional[List[str]] = None,
) -> Dict[str, Any]:
    """Launch an Electron app with remote debugging enabled.

    Starts the application with ``--remote-debugging-port`` so it can
    be controlled via CDP.

    Args:
        app_path: Full path to the Electron app executable.
        port: DevTools port (default 9229, avoids conflict with Chrome 9222).
        extra_args: Additional command line arguments.

    Returns:
        Dict with pid, port, and app_path.

    Raises:
        NaturoError: If launch fails or path doesn't exist.
    """
    _require_windows()

    if not os.path.isfile(app_path):
        raise NaturoError(
            code="FILE_NOT_FOUND",
            message=f"Executable not found: {app_path}",
        )

    args = [app_path, f"--remote-debugging-port={port}"]
    if extra_args:
        args.extend(extra_args)

    try:
        proc = subprocess.Popen(
            args,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            creationflags=0x00000008,  # DETACHED_PROCESS on Windows
        )
        return {
            "pid": proc.pid,
            "port": port,
            "app_path": app_path,
        }
    except OSError as exc:
        raise NaturoError(
            code="LAUNCH_FAILED",
            message=f"Failed to launch {app_path}: {exc}",
        ) from exc


def connect_to_electron(
    app_name: str,
    port: Optional[int] = None,
) -> Dict[str, Any]:
    """Connect to a running Electron app via CDP.

    Detects if the app is running with a debug port. If ``port`` is
    specified, tries that port directly.

    Args:
        app_name: Application name.
        port: Specific port to try (overrides auto-detection).

    Returns:
        Dict with success, port, tabs list.

    Raises:
        NaturoError: If app not found or not debuggable.
    """
    _require_windows()

    if port is None:
        info = detect_electron_app(app_name)
        if not info["is_electron"]:
            raise NaturoError(
                code="NOT_ELECTRON",
                message=(
                    f"'{app_name}' is not an Electron application "
                    "or is not currently running."
                ),
            )
        port = info.get("debug_port")
        if port is None:
            raise NaturoError(
                code="NO_DEBUG_PORT",
                message=(
                    f"'{app_name}' is running but not with remote debugging. "
                    f"Restart it with --remote-debugging-port=<port> to enable "
                    f"CDP access."
                ),
            )

    # Try to connect via CDP
    from naturo.cdp import CDPClient, CDPConnectionError as CDPConnErr

    try:
        client = CDPClient(port=port)
        tabs = client.list_tabs()
        return {
            "port": port,
            "tabs": tabs,
            "count": len(tabs),
        }
    except CDPConnErr as exc:
        raise NaturoError(
            code="CDP_CONNECTION_ERROR",
            message=f"Cannot connect to {app_name} on port {port}: {exc}",
        ) from exc
