"""Update checker for cokodo-agent.

Checks PyPI for newer versions in a background thread and displays
a pip-style notice at the end of the command.

The check is cached for 24 hours so it does not add latency to
subsequent invocations.
"""

from __future__ import annotations

import json
import platform
import subprocess
import threading
import time
import urllib.error
import urllib.request
from pathlib import Path
from typing import Optional, Tuple

from cokodo_agent.config import DEFAULT_CACHE_DIR, VERSION

PYPI_URL = "https://pypi.org/pypi/cokodo-agent/json"
_CHECK_INTERVAL = 86400  # 24 hours
_CACHE_FILE = DEFAULT_CACHE_DIR / "update_check.json"
_NETWORK_TIMEOUT = 3  # seconds

# AI tool processes that host MCP clients and hold file handles to cokodo-agent.
# Used to warn the user before pip upgrade so they know to close these apps first.
_MCP_CLIENT_PROCESS_NAMES = [
    "cursor",
    "cursor.exe",
    "claude",
    "claude.exe",
    "windsurf",
    "windsurf.exe",
    "code",          # VS Code / Copilot
    "code.exe",
]


def _parse_version(v: str) -> tuple:
    """Parse a version string into a comparable tuple.

    Strips pre-release suffixes (a/b/rc) so e.g. '1.6.0a1' < '1.6.0'.
    """
    try:
        parts = []
        for seg in v.strip().split(".")[:3]:
            # Strip any non-numeric suffix (alpha, beta, rc)
            num = ""
            for ch in seg:
                if ch.isdigit():
                    num += ch
                else:
                    break
            parts.append(int(num) if num else 0)
        return tuple(parts)
    except Exception:
        return (0,)


def _fetch_latest_version() -> Optional[str]:
    """Fetch the latest cokodo-agent version from PyPI.

    Returns the version string or None if the request fails.
    Uses only stdlib so no extra dependency is needed.
    """
    try:
        req = urllib.request.Request(
            PYPI_URL,
            headers={"User-Agent": f"cokodo-agent/{VERSION} update-checker"},
        )
        with urllib.request.urlopen(req, timeout=_NETWORK_TIMEOUT) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return data["info"]["version"]
    except Exception:
        return None


def _load_cache() -> dict:
    try:
        if _CACHE_FILE.exists():
            return json.loads(_CACHE_FILE.read_text(encoding="utf-8"))
    except Exception:
        pass
    return {}


def _save_cache(data: dict) -> None:
    try:
        _CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        _CACHE_FILE.write_text(
            json.dumps(data, ensure_ascii=False), encoding="utf-8"
        )
    except Exception:
        pass


class UpdateChecker:
    """Non-blocking update checker.

    Usage::

        checker = UpdateChecker()
        checker.start()          # starts background thread if cache is stale
        ...
        result = checker.result()  # (current, latest) or None
    """

    def __init__(self) -> None:
        self._result: Optional[Tuple[str, str]] = None
        self._thread: Optional[threading.Thread] = None
        self._done = threading.Event()

    def start(self) -> None:
        """Start the update check.

        If the cache is fresh AND the cached latest is newer than the current
        version, return the result immediately (no network call).
        Otherwise spawn a background thread to fetch from PyPI.

        The cache is also considered stale when its recorded latest version is
        older than or equal to the currently installed version — this handles
        the common case where the user just upgraded and the cache still holds
        the previous "latest" value.
        """
        cache = _load_cache()
        now = time.time()
        cache_age_ok = (now - cache.get("ts", 0)) < _CHECK_INTERVAL
        cached_latest = cache.get("latest", "")

        if cache_age_ok and cached_latest:
            if _parse_version(cached_latest) <= _parse_version(VERSION):
                # Cache is fresh but already outdated relative to current install
                # (e.g. user just upgraded). Force a new check.
                cache_age_ok = False

        if cache_age_ok and cached_latest:
            if _parse_version(cached_latest) > _parse_version(VERSION):
                self._result = (VERSION, cached_latest)
            self._done.set()
            return

        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def _run(self) -> None:
        try:
            latest = _fetch_latest_version()
            now = time.time()
            if latest:
                _save_cache({"ts": now, "latest": latest})
                if _parse_version(latest) > _parse_version(VERSION):
                    self._result = (VERSION, latest)
            else:
                cache = _load_cache()
                cache["ts"] = now
                _save_cache(cache)
        finally:
            self._done.set()

    def result(self, timeout: float = 2.0) -> Optional[Tuple[str, str]]:
        """Return ``(current, latest)`` if an update is available, else ``None``.

        Waits up to *timeout* seconds for the background thread to finish.
        """
        self._done.wait(timeout=timeout)
        return self._result


def format_update_notice(current: str, latest: str) -> str:
    """Return a plain-text update notice (used in tests / non-rich output)."""
    return (
        f"A new release of cokodo-agent is available: {current} → {latest}\n"
        f"Run: pip install --upgrade cokodo-agent"
    )


def _detect_mcp_clients() -> list[str]:
    """Return names of running AI tools that may hold cokodo-agent file handles.

    Uses only stdlib (subprocess + platform) — no psutil required.
    Returns a deduplicated list of friendly app names found running, e.g.
    ["Cursor", "Claude Desktop"].  Returns [] if none found or on error.
    """
    found: list[str] = []
    try:
        if platform.system() == "Windows":
            out = subprocess.check_output(
                ["tasklist", "/FO", "CSV", "/NH"],
                stderr=subprocess.DEVNULL,
                timeout=3,
            ).decode("utf-8", errors="replace").lower()
            rows = out.splitlines()
            running = {r.split(",")[0].strip('"') for r in rows if "," in r}
        else:
            out = subprocess.check_output(
                ["ps", "-eo", "comm"],
                stderr=subprocess.DEVNULL,
                timeout=3,
            ).decode("utf-8", errors="replace").lower()
            running = {line.strip() for line in out.splitlines()}

        _FRIENDLY: dict[str, str] = {
            "cursor": "Cursor",
            "cursor.exe": "Cursor",
            "claude": "Claude Desktop",
            "claude.exe": "Claude Desktop",
            "windsurf": "Windsurf",
            "windsurf.exe": "Windsurf",
            "code": "VS Code",
            "code.exe": "VS Code",
        }
        seen: set[str] = set()
        for proc in _MCP_CLIENT_PROCESS_NAMES:
            if proc in running:
                friendly = _FRIENDLY.get(proc, proc)
                if friendly not in seen:
                    found.append(friendly)
                    seen.add(friendly)
    except Exception:
        pass
    return found


def print_update_notice(current: str, latest: str) -> None:
    """Print a pip-style update notice to stderr using Rich.

    If AI tools that host MCP clients are running, display an additional
    warning telling the user to close those apps before upgrading —
    otherwise pip may fail mid-install due to Windows file locks and leave
    behind broken ~okodo_agent* directories.
    """
    try:
        from rich.console import Console
        from rich.panel import Panel
        from rich.text import Text

        err = Console(stderr=True)
        clients = _detect_mcp_clients()

        if clients:
            client_list = ", ".join(clients)
            body = Text.assemble(
                "A new release of ",
                ("cokodo-agent", "bold cyan"),
                " is available: ",
                (current, "yellow"),
                " → ",
                (latest, "bold green"),
                "\n\n",
                ("Note: ", "bold yellow"),
                (f"{client_list} ", "yellow"),
                "is running and may hold cokodo-agent open.\n",
                "Upgrading while it is open can leave broken files behind.\n\n",
                "To upgrade safely:\n",
                ("  1. ", "dim"),
                (f"Close {client_list}\n", "white"),
                ("  2. ", "dim"),
                ("pip install --upgrade cokodo-agent\n", "bold cyan"),
                ("  3. ", "dim"),
                (f"Reopen {client_list}", "white"),
            )
        else:
            body = Text.assemble(
                "A new release of ",
                ("cokodo-agent", "bold cyan"),
                " is available: ",
                (current, "yellow"),
                " → ",
                (latest, "bold green"),
                "\n",
                "Run: ",
                ("pip install --upgrade cokodo-agent", "bold cyan"),
            )

        err.print()
        err.print(
            Panel(
                body,
                title="[yellow]Update available[/yellow]",
                border_style="yellow",
                expand=False,
            )
        )
    except Exception:
        import sys
        print(f"\n{format_update_notice(current, latest)}\n", file=sys.stderr)
