"""Raise and probe the Agent Console window (X11 / Wayland best-effort)."""

from __future__ import annotations

import os
import shutil
import subprocess
import time
from collections.abc import Callable
from pathlib import Path

from ...core.version import AGENT_MVP_VERSION
from .console_instance_lock import (
    default_console_lock_path,
    read_running_console_pid,
    request_console_activation,
)

CONSOLE_WM_CLASS = "iuve-connect-agent"
CONSOLE_WINDOW_NAME_HINTS = ("iuve connect", "iuve-connect")


def _run(cmd: list[str], *, timeout: float = 10.0) -> tuple[int, str, str]:
    try:
        proc = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=timeout,
            check=False,
        )
        return int(proc.returncode), str(proc.stdout or "").strip(), str(proc.stderr or "").strip()
    except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
        return 1, "", str(exc)


def _session_type() -> str:
    return str(os.environ.get("XDG_SESSION_TYPE", "")).strip().lower()


def _is_process_alive(pid: int) -> bool:
    if pid <= 0:
        return False
    try:
        os.kill(pid, 0)
        return True
    except OSError:
        return False


def is_console_running(*, lock_path: Path | None = None) -> tuple[bool, int | None]:
    pid = read_running_console_pid(lock_path=lock_path)
    if pid is None:
        return False, None
    if not _is_process_alive(pid):
        return False, pid
    return True, pid


def _window_title_matches(title: str) -> bool:
    lowered = str(title or "").strip().lower()
    if not lowered:
        return False
    return any(hint in lowered for hint in CONSOLE_WINDOW_NAME_HINTS)


def _try_dbus_activate(application_id: str) -> tuple[bool, str]:
    object_path = "/" + application_id.replace(".", "/")
    gdbus = shutil.which("gdbus")
    if gdbus:
        code, out, err = _run(
            [
                gdbus,
                "call",
                "--session",
                "--dest",
                application_id,
                "--object-path",
                object_path,
                "--method",
                "org.freedesktop.Application.Activate",
                "{}",
            ],
            timeout=8.0,
        )
        if code == 0:
            return True, "dbus_activate"
        code2, _, err2 = _run(
            [
                gdbus,
                "call",
                "--session",
                "--dest",
                application_id,
                "--object-path",
                object_path,
                "--method",
                "org.gtk.Application.Activate",
                "",
            ],
            timeout=8.0,
        )
        if code2 == 0:
            return True, "gtk_dbus_activate"
        return False, err or err2 or "dbus_activate_failed"

    try:
        import gi  # type: ignore

        gi.require_version("Gio", "2.0")
        from gi.repository import Gio  # type: ignore

        bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        proxy = Gio.DBusProxy.new_sync(
            bus,
            Gio.DBusProxyFlags.NONE,
            None,
            application_id,
            object_path,
            "org.freedesktop.Application",
            None,
        )
        proxy.call_sync("Activate", None, Gio.DBusCallFlags.NONE, 5000, None)
        return True, "gio_dbus_activate"
    except Exception as exc:
        return False, str(exc)


def _wmctrl_list() -> list[tuple[str, str, str, str, str]]:
    wmctrl = shutil.which("wmctrl")
    if not wmctrl:
        return []
    code, out, _ = _run([wmctrl, "-lx"], timeout=10.0)
    if code != 0:
        code, out, _ = _run([wmctrl, "-l"], timeout=10.0)
        if code != 0:
            return []
    rows: list[tuple[str, str, str, str, str]] = []
    for line in out.splitlines():
        parts = line.split(None, 4)
        if len(parts) < 4:
            continue
        win_id, host, desk, rest = parts[0], parts[1], parts[2], parts[3]
        class_hint = ""
        title = rest
        if len(parts) == 5:
            tail = parts[4]
            if " " in tail:
                class_hint, title = tail.split(" ", 1)
            else:
                class_hint = tail
        rows.append((win_id, host, desk, class_hint, title))
    return rows


def _wmctrl_current_desktop() -> str | None:
    wmctrl = shutil.which("wmctrl")
    if not wmctrl:
        return None
    code, out, _ = _run([wmctrl, "-d"], timeout=8.0)
    if code != 0:
        return None
    for line in out.splitlines():
        if "*" in line:
            parts = line.split()
            if parts:
                return parts[0]
    return None


def _find_wmctrl_window(*, pid: int = 0, wm_class: str = CONSOLE_WM_CLASS) -> tuple[str, str, str]:
    class_lower = wm_class.lower()
    for win_id, _host, desk, class_hint, title in _wmctrl_list():
        title_lower = title.lower()
        if class_lower in class_hint.lower() or class_lower in title_lower or _window_title_matches(title):
            return win_id, desk, title
    if pid > 0:
        for win_id, _host, desk, _class_hint, title in _wmctrl_list():
            if _window_title_matches(title):
                return win_id, desk, title
    return "", "", ""


def _try_wmctrl_raise(*, pid: int = 0, wm_class: str = CONSOLE_WM_CLASS) -> tuple[bool, str]:
    wmctrl = shutil.which("wmctrl")
    if not wmctrl:
        return False, "wmctrl_missing"
    win_id, desk, title = _find_wmctrl_window(pid=pid, wm_class=wm_class)
    if not win_id:
        return False, "wmctrl_window_not_found"
    current = _wmctrl_current_desktop()
    if current is not None and desk not in {"", "-1", current}:
        _run([wmctrl, "-i", "-t", desk, win_id], timeout=8.0)
    code, _, err = _run([wmctrl, "-i", "-a", win_id], timeout=10.0)
    if code == 0:
        time.sleep(0.35)
        return True, title or win_id
    return False, err or "wmctrl_activate_failed"


def _try_xdotool_raise(*, pid: int = 0, wm_class: str = CONSOLE_WM_CLASS) -> tuple[bool, str]:
    xdotool = shutil.which("xdotool")
    if not xdotool:
        return False, "xdotool_missing"
    window_id = ""
    if pid > 0:
        code, out, err = _run([xdotool, "search", "--pid", str(pid)], timeout=10.0)
        if code == 0 and out.strip():
            window_id = out.splitlines()[-1].strip()
    if not window_id:
        code, out, err = _run([xdotool, "search", "--class", wm_class], timeout=10.0)
        if code == 0 and out.strip():
            window_id = out.splitlines()[-1].strip()
    if not window_id:
        for hint in CONSOLE_WINDOW_NAME_HINTS:
            code, out, err = _run(
                [xdotool, "search", "--name", hint],
                timeout=10.0,
            )
            if code == 0 and out.strip():
                window_id = out.splitlines()[-1].strip()
                break
    if not window_id:
        return False, "xdotool_window_not_found"
    code, _, err = _run([xdotool, "windowactivate", "--sync", window_id], timeout=12.0)
    if code != 0:
        return False, err or "xdotool_activate_failed"
    _run([xdotool, "windowfocus", "--sync", window_id], timeout=8.0)
    _run([xdotool, "windowraise", window_id], timeout=8.0)
    time.sleep(0.35)
    return True, window_id


def probe_console_window(
    *,
    pid: int = 0,
    wm_class: str = CONSOLE_WM_CLASS,
) -> dict[str, object]:
    """Best-effort window presence / focus / workspace probe."""
    window_found = False
    focused = False
    workspace: str | None = None
    window_id = ""

    xdotool = shutil.which("xdotool")
    if xdotool:
        search_cmd: list[str] | None = None
        if pid > 0:
            search_cmd = [xdotool, "search", "--pid", str(pid)]
        else:
            search_cmd = [xdotool, "search", "--class", wm_class]
        code, out, _ = _run(search_cmd, timeout=10.0)
        if code == 0 and out.strip():
            window_found = True
            window_id = out.splitlines()[-1].strip()
            code2, active, _ = _run([xdotool, "getactivewindow"], timeout=6.0)
            if code2 == 0 and active.strip() == window_id:
                focused = True
            code3, desk, _ = _run([xdotool, "get_desktop_for_window", window_id], timeout=6.0)
            if code3 == 0 and desk.strip().isdigit():
                workspace = str(int(desk.strip()) + 1)

    if not window_found:
        win_id, desk, _title = _find_wmctrl_window(pid=pid, wm_class=wm_class)
        if win_id:
            window_found = True
            window_id = win_id
            if desk not in {"", "-1"}:
                try:
                    workspace = str(int(desk) + 1)
                except ValueError:
                    workspace = desk

    if window_found and xdotool and not focused:
        code, active, _ = _run([xdotool, "getactivewindow"], timeout=6.0)
        if code == 0 and active.strip() == window_id:
            focused = True

    return {
        "window_found": window_found,
        "focused": focused,
        "workspace": workspace,
        "window_id": window_id or None,
        "session_type": _session_type() or None,
    }


def raise_console_window(
    *,
    pid: int = 0,
    application_id: str,
    wm_class: str = CONSOLE_WM_CLASS,
    signal_running_app: bool = True,
) -> tuple[bool, dict[str, object]]:
    """Raise an existing console window: in-app flag, D-Bus, wmctrl, xdotool."""
    details: dict[str, object] = {"strategies": []}
    if signal_running_app:
        request_console_activation()
        time.sleep(0.45)

    strategies: list[tuple[str, Callable[[], tuple[bool, str]]]] = [
        ("dbus", lambda: _try_dbus_activate(application_id)),
    ]
    if _session_type() != "wayland" and os.environ.get("DISPLAY"):
        strategies.append(("wmctrl", lambda: _try_wmctrl_raise(pid=pid, wm_class=wm_class)))
        strategies.append(("xdotool", lambda: _try_xdotool_raise(pid=pid, wm_class=wm_class)))

    for name, attempt in strategies:
        ok, detail = attempt()
        details["strategies"].append({"name": name, "ok": ok, "detail": detail})
        if ok:
            probe = probe_console_window(pid=pid, wm_class=wm_class)
            details.update(probe)
            details["raised_via"] = name
            if probe.get("window_found") or name == "dbus":
                return True, details

    probe = probe_console_window(pid=pid, wm_class=wm_class)
    details.update(probe)
    if probe.get("window_found"):
        return True, details
    return False, details


def raise_existing_console_window(
    *,
    pid: int = 0,
    application_id: str,
    wm_class: str = CONSOLE_WM_CLASS,
) -> tuple[bool, dict[str, object]]:
    resolved_pid = pid or read_running_console_pid() or 0
    if resolved_pid > 0 and not _is_process_alive(resolved_pid):
        return False, {"error": "stale_lock_pid", "pid": resolved_pid}
    return raise_console_window(
        pid=resolved_pid,
        application_id=application_id,
        wm_class=wm_class,
        signal_running_app=True,
    )


def _backend_label(snapshot: dict[str, object]) -> str:
    runtime = str(snapshot.get("runtime_state") or "").strip()
    if runtime.lower() in {"connected", "online", "active"}:
        return "Connected"
    if runtime.lower() in {"degraded"}:
        return "Degraded"
    if runtime.lower() in {"offline"}:
        return "Offline"
    return runtime or "Unknown"


def _heartbeat_label(snapshot: dict[str, object]) -> str:
    age = snapshot.get("heartbeat_age_sec")
    if age is None:
        return "Unknown"
    try:
        age_sec = int(age)
    except (TypeError, ValueError):
        return "Unknown"
    if age_sec < 0:
        return "Unknown"
    if age_sec <= 90:
        return "OK"
    if age_sec <= 600:
        return "Stale"
    return "Offline"


def collect_console_status(*, config_path: str | Path) -> dict[str, object]:
    running, pid = is_console_running(lock_path=default_console_lock_path())
    window_info: dict[str, object] = {
        "window_found": False,
        "focused": False,
        "workspace": None,
    }
    if running and pid:
        window_info = probe_console_window(pid=pid)

    backend_snapshot: dict[str, object] = {}
    try:
        from .status_adapter import StatusAdapter

        backend_snapshot = StatusAdapter(config_path=config_path).read_dashboard_snapshot()
    except Exception as exc:
        backend_snapshot = {"runtime_state": "Unknown", "heartbeat_age_sec": -1, "error": str(exc)}

    return {
        "running": running,
        "pid": pid,
        "window_found": bool(window_info.get("window_found")),
        "focused": bool(window_info.get("focused")),
        "workspace": window_info.get("workspace"),
        "backend": _backend_label(backend_snapshot),
        "heartbeat": _heartbeat_label(backend_snapshot),
        "version": AGENT_MVP_VERSION,
        "session_type": _session_type() or None,
    }


def format_console_status_text(status: dict[str, object]) -> str:
    running = bool(status.get("running"))
    pid = status.get("pid")
    pid_text = str(pid) if pid else "-"
    workspace = status.get("workspace")
    workspace_text = str(workspace) if workspace is not None else "-"
    lines = [
        "Console status",
        f"Running: {'YES' if running else 'NO'}",
        f"PID: {pid_text}",
        f"Window: {'YES' if status.get('window_found') else 'NO'}",
        f"Focused: {'YES' if status.get('focused') else 'NO'}",
        f"Workspace: {workspace_text}",
        f"Backend: {status.get('backend', 'Unknown')}",
        f"Heartbeat: {status.get('heartbeat', 'Unknown')}",
        f"Version: {status.get('version', AGENT_MVP_VERSION)}",
    ]
    return "\n".join(lines)
