"""Linux AppAdapter — launch/focus/type for canonical app_id (DSL v0.1 Phase 3b)."""

from __future__ import annotations

import shutil
import subprocess
import time
from pathlib import Path
from typing import Any

from iuve_connect_agents.desktop.active.xdotool_window_activate import run_windowactivate
from iuve_connect_agents.desktop.platform_adapters.adapter_types import (
    EmptyTrashResult,
    FocusResult,
    InstalledAppEntry,
    LaunchResult,
    TypeResult,
)

_BROWSER_WINDOW_CLASSES = (
    "google-chrome",
    "Google-chrome",
    "Chromium",
    "chromium",
    "firefox",
    "Firefox",
    "Brave-browser",
    "brave-browser",
)
_BROWSER_WINDOW_NAME_EXCLUDES = ("cursor",)

_LEGACY_APPLICATION_LAUNCH_CANDIDATES: dict[str, tuple[tuple[str, ...], ...]] = {
    "cursor": (("cursor", "."), ("cursor",), ("gtk-launch", "cursor")),
    "telegram": (
        ("telegram-desktop",),
        ("gtk-launch", "org.telegram.desktop"),
        ("gtk-launch", "telegram-desktop"),
    ),
    "telegram-desktop": (
        ("telegram-desktop",),
        ("gtk-launch", "org.telegram.desktop"),
    ),
    "viber": (
        ("viber",),
        ("gtk-launch", "viber"),
        ("com.viber.Viber",),
    ),
    "claude": (("claude",), ("gtk-launch", "claude")),
    "firefox": (
        ("firefox",),
        ("firefox-esr",),
        ("gtk-launch", "firefox"),
        ("gtk-launch", "firefox-esr"),
        ("gtk-launch", "firefox_firefox"),
    ),
}


def launch_candidates_from_installed_entry(entry: InstalledAppEntry) -> tuple[tuple[str, ...], ...]:
    """Build launch argv tuples from DCM installed_apps entry launch_spec."""
    launch_spec = entry.get("launch_spec")
    if not isinstance(launch_spec, dict):
        return ()
    kind = str(launch_spec.get("kind") or "").strip().lower()
    argv = launch_spec.get("argv")
    argv_parts = [str(part) for part in argv] if isinstance(argv, list) else []
    desktop_id = str(launch_spec.get("desktop_id") or "").strip()

    candidates: list[tuple[str, ...]] = []
    if kind == "desktop_entry" and desktop_id:
        candidates.append(("gtk-launch", desktop_id))
    if argv_parts:
        candidates.append(tuple(argv_parts))
    elif kind == "binary" and entry.get("binary_path"):
        candidates.append((str(entry.get("binary_path")),))

    deduped: list[tuple[str, ...]] = []
    seen: set[tuple[str, ...]] = set()
    for item in candidates:
        if item and item not in seen:
            seen.add(item)
            deduped.append(item)
    return tuple(deduped)


class LinuxAppAdapter:
    """Linux L2 adapter: xdotool / gtk-launch / xdg-open."""

    os_family = "linux"

    def launch(
        self,
        app_id: str,
        entry: InstalledAppEntry,
        *,
        new_window: bool = True,
    ) -> LaunchResult:
        del new_window  # reserved for future per-app window policy
        candidates = launch_candidates_from_installed_entry(entry)
        if not candidates:
            legacy_key = str(app_id or "").strip().lower()
            candidates = _LEGACY_APPLICATION_LAUNCH_CANDIDATES.get(legacy_key, ())
        errors: list[str] = []
        for cmd in candidates:
            launch_cmd = list(cmd)
            try:
                pid = self._spawn_detached(launch_cmd)
            except FileNotFoundError:
                errors.append(" ".join(launch_cmd))
                continue
            return LaunchResult(success=True, pid=pid, launch_command=launch_cmd)
        return LaunchResult(
            success=False,
            launch_command=list(candidates[0]) if candidates else [],
            error=" ".join(errors) if errors else "launch_failed",
        )

    def launch_uri(self, uri: str) -> LaunchResult:
        """Open URL or folder path via xdg-open."""
        launch_cmd = ["xdg-open", str(uri)]
        try:
            pid = self._spawn_detached(launch_cmd)
        except FileNotFoundError:
            return LaunchResult(success=False, launch_command=launch_cmd, error="xdg_open_not_found")
        return LaunchResult(success=True, pid=pid, launch_command=launch_cmd)

    def open_folder(self, folder_path: str) -> LaunchResult:
        return self.launch_uri(str(folder_path))

    def empty_trash(self) -> EmptyTrashResult:
        gio = shutil.which("gio")
        if gio:
            proc = subprocess.run(
                [gio, "trash", "--empty"],
                capture_output=True,
                text=True,
                timeout=30,
                check=False,
            )
            if proc.returncode != 0:
                return EmptyTrashResult(
                    success=False,
                    execution_result="trash_empty_failed",
                    error=(proc.stderr or proc.stdout or "gio_trash_empty_failed").strip(),
                    evidence={"tool": "gio", "handler": "empty_trash", "os_family": self.os_family},
                )
            return EmptyTrashResult(
                success=True,
                execution_result="trash_emptied",
                evidence={"tool": "gio", "handler": "empty_trash", "os_family": self.os_family},
            )

        trash_files = Path.home() / ".local" / "share" / "Trash" / "files"
        trash_info = Path.home() / ".local" / "share" / "Trash" / "info"
        removed = 0
        for trash_dir in (trash_files, trash_info):
            if not trash_dir.is_dir():
                continue
            for entry in trash_dir.iterdir():
                if entry.is_file() or entry.is_symlink():
                    entry.unlink(missing_ok=True)
                    removed += 1
                elif entry.is_dir():
                    shutil.rmtree(entry, ignore_errors=True)
                    removed += 1
        return EmptyTrashResult(
            success=True,
            execution_result="trash_emptied",
            evidence={
                "tool": "xdg_fallback",
                "entries_removed": removed,
                "handler": "empty_trash",
                "os_family": self.os_family,
            },
        )

    def focus(
        self,
        app_id: str,
        entry: InstalledAppEntry | None,
        *,
        window_hint: str = "",
    ) -> FocusResult:
        del entry
        xdotool = shutil.which("xdotool")
        if not xdotool:
            return FocusResult(success=False, detail="xdotool_not_found")

        needle = str(app_id or "").strip().lower()
        if needle in {"document_writer", "libreoffice"}:
            ok, detail, meta = self._xdotool_focus_libreoffice(
                xdotool,
                window_name=window_hint or "LibreOffice",
            )
            return FocusResult(success=ok, detail=detail, evidence=meta, degraded=bool(meta.get("focus_degraded")))

        if needle in {"web_browser", "browser"}:
            ok, detail, meta = self._xdotool_activate_browser_window(xdotool, window_name=window_hint)
            return FocusResult(success=ok, detail=detail, evidence=meta, degraded=bool(meta.get("focus_degraded")))

        if needle in {"code_editor", "cursor"}:
            focus = run_windowactivate(
                [xdotool, "search", "--name", "Cursor", "windowactivate", "--sync"],
                timeout=15.0,
                window_id="Cursor",
            )
            if focus.ok:
                time.sleep(0.5)
                return FocusResult(success=True, detail="", evidence=focus.as_evidence())
            return FocusResult(
                success=False,
                detail=focus.detail or "cursor_window_not_found",
                evidence=focus.as_evidence(),
            )

        if window_hint:
            focus = run_windowactivate(
                [xdotool, "search", "--name", window_hint, "windowactivate", "--sync"],
                timeout=15.0,
                window_id=window_hint,
            )
            if focus.ok:
                time.sleep(0.5)
                return FocusResult(success=True, detail=window_hint, evidence=focus.as_evidence())
            return FocusResult(
                success=False,
                detail=focus.detail or "window_not_found",
                evidence=focus.as_evidence(),
            )

        focus = run_windowactivate(
            [xdotool, "getactivewindow", "windowactivate", "--sync"],
            timeout=15.0,
        )
        if focus.ok:
            time.sleep(0.3)
            return FocusResult(success=True, detail="active", evidence=focus.as_evidence())
        return FocusResult(success=False, detail=focus.detail or "active_window_focus_failed", evidence=focus.as_evidence())

    def type_text(self, app_id: str, text: str, *, submit: bool = False) -> TypeResult:
        xdotool = shutil.which("xdotool")
        if not xdotool:
            return TypeResult(success=False, execution_result="xdotool_not_found", error="xdotool_not_found")

        needle = str(app_id or "").strip().lower()
        focus = self.focus(needle, None)
        if not focus.success:
            return TypeResult(
                success=False,
                execution_result="keyboard_focus_failed",
                error=focus.detail or "focus_failed",
                evidence=dict(focus.evidence),
            )

        if needle in {"code_editor", "cursor"}:
            chat_focus = subprocess.run(
                [xdotool, "key", "--clearmodifiers", "ctrl+l"],
                capture_output=True,
                text=True,
                timeout=10,
                check=False,
            )
            if chat_focus.returncode != 0:
                return TypeResult(
                    success=False,
                    execution_result="keyboard_focus_failed",
                    error=(chat_focus.stderr or chat_focus.stdout or "cursor_chat_focus_failed").strip(),
                )
            time.sleep(0.5)
        elif needle in {"web_browser", "browser"}:
            clear_field = subprocess.run(
                [xdotool, "key", "--clearmodifiers", "ctrl+a", "BackSpace"],
                capture_output=True,
                text=True,
                timeout=10,
                check=False,
            )
            if clear_field.returncode != 0:
                return TypeResult(
                    success=False,
                    execution_result="keyboard_focus_failed",
                    error=(clear_field.stderr or clear_field.stdout or "browser_input_clear_failed").strip(),
                )
            time.sleep(0.2)

        chunk_size = 400
        chunks = [text[i : i + chunk_size] for i in range(0, len(text), chunk_size)] or [text]
        inject_evidence: dict[str, object] = {}
        import sys

        if sys.platform.startswith("linux"):
            from iuve_connect_agents.desktop.active.linux_text_injection import inject_text_with_xdotool

            inject_ok, inject_detail, inject_evidence = inject_text_with_xdotool(xdotool, text)
            if not inject_ok:
                return TypeResult(
                    success=False,
                    execution_result="keyboard_type_failed",
                    error=inject_detail,
                    evidence={"app_id": needle, **inject_evidence},
                )
        else:
            for chunk in chunks:
                typed = subprocess.run(
                    [xdotool, "type", "--delay", "12", "--", chunk],
                    capture_output=True,
                    text=True,
                    timeout=max(30, min(120, len(chunk) // 5)),
                    check=False,
                )
                if typed.returncode != 0:
                    return TypeResult(
                        success=False,
                        execution_result="keyboard_type_failed",
                        error=(typed.stderr or typed.stdout or "xdotool_type_failed").strip(),
                        evidence={"app_id": needle},
                    )

        submitted = False
        if submit:
            submitted_key = subprocess.run(
                [xdotool, "key", "--clearmodifiers", "Return"],
                capture_output=True,
                text=True,
                timeout=10,
                check=False,
            )
            if submitted_key.returncode != 0:
                return TypeResult(
                    success=False,
                    execution_result="keyboard_submit_failed",
                    error=(submitted_key.stderr or submitted_key.stdout or "xdotool_submit_failed").strip(),
                )
            submitted = True

        execution_result = "keyboard_typed_and_submitted" if submitted else "keyboard_typed"
        return TypeResult(
            success=True,
            execution_result=execution_result,
            evidence={
                "app_id": needle,
                "tool": "xdotool",
                "submitted": submitted,
                **focus.evidence,
                **inject_evidence,
            },
        )

    def is_running(self, app_id: str, entry: InstalledAppEntry | None) -> bool:
        xdotool = shutil.which("xdotool")
        if not xdotool:
            return False
        needle = str(app_id or "").strip().lower()
        if needle in {"web_browser", "browser"}:
            for class_name in _BROWSER_WINDOW_CLASSES:
                if self._xdotool_list_class_windows(xdotool, class_name):
                    return True
            return False
        if needle in {"document_writer", "libreoffice"}:
            for class_name in ("soffice", "libreoffice", "LibreOffice"):
                if self._xdotool_list_class_windows(xdotool, class_name):
                    return True
            return False
        if needle in {"code_editor", "cursor"}:
            result = subprocess.run(
                [xdotool, "search", "--name", "Cursor"],
                capture_output=True,
                text=True,
                timeout=10,
                check=False,
            )
            return result.returncode == 0 and bool(str(result.stdout or "").strip())
        if entry:
            launch_spec = entry.get("launch_spec")
            if isinstance(launch_spec, dict):
                argv = launch_spec.get("argv")
                if isinstance(argv, list) and argv:
                    binary = Path(str(argv[0])).name
                    if binary:
                        result = subprocess.run(
                            ["pgrep", "-x", binary],
                            capture_output=True,
                            text=True,
                            timeout=5,
                            check=False,
                        )
                        return result.returncode == 0
        return False

    @staticmethod
    def _spawn_detached(cmd: list[str]) -> int:
        if not cmd:
            raise ValueError("empty_launch_command")
        proc = subprocess.Popen(
            cmd,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
            close_fds=True,
        )
        return int(proc.pid)

    @staticmethod
    def _xdotool_window_name(xdotool: str, window_id: str) -> str:
        result = subprocess.run(
            [xdotool, "getwindowname", window_id],
            capture_output=True,
            text=True,
            timeout=10,
            check=False,
        )
        if result.returncode != 0:
            return ""
        return str(result.stdout or "").strip()

    @staticmethod
    def _xdotool_list_class_windows(xdotool: str, class_name: str) -> list[str]:
        result = subprocess.run(
            [xdotool, "search", "--class", class_name],
            capture_output=True,
            text=True,
            timeout=15,
            check=False,
        )
        if result.returncode != 0:
            return []
        return [wid for wid in str(result.stdout or "").splitlines() if wid.strip()]

    @classmethod
    def _xdotool_activate_browser_window(
        cls,
        xdotool: str,
        *,
        window_name: str = "",
    ) -> tuple[bool, str, dict[str, Any]]:
        prefer = str(window_name or "").strip().lower()
        candidates: list[tuple[str, str]] = []
        for class_name in _BROWSER_WINDOW_CLASSES:
            for window_id in cls._xdotool_list_class_windows(xdotool, class_name):
                name = cls._xdotool_window_name(xdotool, window_id)
                name_lower = name.lower()
                if any(exclude in name_lower for exclude in _BROWSER_WINDOW_NAME_EXCLUDES):
                    continue
                candidates.append((window_id, name))

        if not candidates:
            return False, "browser_window_not_found", {}

        selected_id = candidates[-1][0]
        if prefer:
            for window_id, name in candidates:
                if prefer in name.lower():
                    selected_id = window_id
                    break

        focus = run_windowactivate(
            [xdotool, "windowactivate", "--sync", selected_id],
            timeout=15.0,
            window_id=selected_id,
        )
        if focus.ok:
            time.sleep(0.5)
            return True, selected_id, focus.as_evidence()
        return False, focus.detail, focus.as_evidence()

    @staticmethod
    def _xdotool_focus_libreoffice(
        xdotool: str,
        *,
        window_name: str,
        timeout_sec: int = 30,
    ) -> tuple[bool, str, dict[str, Any]]:
        class_names = ("soffice", "libreoffice", "LibreOffice")
        meta: dict[str, Any] = {}
        for _attempt in range(3):
            for class_name in class_names:
                windows = LinuxAppAdapter._xdotool_list_class_windows(xdotool, class_name)
                for window_id in windows:
                    name = LinuxAppAdapter._xdotool_window_name(xdotool, window_id)
                    if window_name.lower() not in name.lower() and "writer" not in name.lower():
                        continue
                    focus = run_windowactivate(
                        [xdotool, "windowactivate", "--sync", window_id],
                        timeout=float(timeout_sec),
                        window_id=window_id,
                    )
                    meta = focus.as_evidence()
                    if focus.ok:
                        time.sleep(0.6)
                        return True, name, meta
            for class_name in class_names:
                focus = run_windowactivate(
                    [xdotool, "search", "--class", class_name, "windowactivate", "--sync"],
                    timeout=float(timeout_sec),
                    window_id=class_name,
                )
                meta = focus.as_evidence()
                if focus.ok:
                    time.sleep(0.6)
                    return True, class_name, meta
            if window_name:
                focus = run_windowactivate(
                    [xdotool, "search", "--name", window_name, "windowactivate", "--sync"],
                    timeout=float(timeout_sec),
                    window_id=window_name,
                )
                meta = focus.as_evidence()
                if focus.ok:
                    time.sleep(0.6)
                    return True, window_name, meta
            time.sleep(2.0)
        return False, "libreoffice_window_not_found", meta
