"""
Module: update_checker
Description: Check PyPI for outdated huitzo packages and warn developers

Implements:
    - docs/cli/reference.md#outdated-dependencies

See Also:
    - docs/guides/developer-environment.md#development-session-flow
"""

from __future__ import annotations

import json
import logging
import os
import threading
import time
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from typing import NamedTuple

import httpx
from packaging.version import InvalidVersion, Version
from rich.console import Console

from huitzo_cli.config import ConfigManager

logger = logging.getLogger(__name__)

PYPI_URL = "https://pypi.org/pypi/{package}/json"
CACHE_FILENAME = "update_check.json"
CACHE_MAX_AGE_SECONDS = 86400  # 24 hours
PYPI_TIMEOUT_SECONDS = 5.0


class UpdateInfo(NamedTuple):
    """Information about an outdated package."""

    package: str
    installed: str
    latest: str


def _get_installed_version(package: str) -> str | None:
    """Get the installed version of a package."""
    try:
        return version(package)
    except PackageNotFoundError:
        return None


def _fetch_latest_version(package: str) -> str | None:
    """Fetch the latest version of a package from PyPI."""
    try:
        resp = httpx.get(PYPI_URL.format(package=package), timeout=PYPI_TIMEOUT_SECONDS)
        resp.raise_for_status()
        result: str = resp.json()["info"]["version"]
        return result
    except (httpx.HTTPError, json.JSONDecodeError, KeyError, TypeError):
        logger.debug("Failed to fetch latest version for %s", package, exc_info=True)
        return None


def _is_outdated(installed: str, latest: str) -> bool:
    """Check if the installed version is older than the latest."""
    try:
        return Version(installed) < Version(latest)
    except InvalidVersion:
        return False


def _get_cache_path() -> Path:
    """Get the path to the update check cache file."""
    return ConfigManager.get_config_dir() / CACHE_FILENAME


def _load_cache(cache_path: Path) -> dict[str, object]:
    """Load cached version data. Returns empty dict on any failure."""
    if not cache_path.exists():
        return {}
    try:
        data = json.loads(cache_path.read_text())
        if isinstance(data, dict):
            return data
    except (OSError, json.JSONDecodeError):
        pass
    return {}


def _is_cache_fresh(cache_data: dict[str, object]) -> bool:
    """Check if the cache is less than 24 hours old."""
    timestamp = cache_data.get("timestamp")
    if not isinstance(timestamp, (int, float)):
        return False
    return (time.time() - timestamp) < CACHE_MAX_AGE_SECONDS


def _save_cache(cache_path: Path, versions: dict[str, str]) -> None:
    """Save version data to cache file using atomic write."""
    try:
        cache_path.parent.mkdir(parents=True, exist_ok=True)
        cache_path.parent.chmod(0o700)
        tmp = cache_path.with_suffix(".tmp")
        tmp.write_text(json.dumps({"timestamp": time.time(), "versions": versions}))
        os.replace(tmp, cache_path)
    except OSError:
        logger.debug("Failed to write update cache", exc_info=True)


def check_for_updates(packages: list[str]) -> list[UpdateInfo]:
    """Check if any of the given packages have newer versions on PyPI.

    Uses a 24-hour file cache to avoid hitting PyPI on every invocation.
    """
    cache_path = _get_cache_path()
    cache_data = _load_cache(cache_path)

    # Use cache if fresh
    if _is_cache_fresh(cache_data):
        cached_versions = cache_data.get("versions", {})
        if isinstance(cached_versions, dict):
            results: list[UpdateInfo] = []
            for pkg in packages:
                installed = _get_installed_version(pkg)
                latest = cached_versions.get(pkg)
                if installed and isinstance(latest, str) and _is_outdated(installed, latest):
                    results.append(UpdateInfo(package=pkg, installed=installed, latest=latest))
            return results

    # Fetch from PyPI
    latest_versions: dict[str, str] = {}
    results = []
    for pkg in packages:
        installed = _get_installed_version(pkg)
        latest = _fetch_latest_version(pkg)
        if latest:
            latest_versions[pkg] = latest
        if installed and latest and _is_outdated(installed, latest):
            results.append(UpdateInfo(package=pkg, installed=installed, latest=latest))

    # Update cache
    if latest_versions:
        _save_cache(cache_path, latest_versions)

    return results


def should_skip(cli_flag: bool) -> bool:
    """Check if update checking should be skipped."""
    if cli_flag:
        return True
    return os.environ.get("HUITZO_SKIP_UPDATE_CHECK", "").lower() in ("1", "true", "yes")


class UpdateCheck:
    """Handle for a background update check. Holds the thread and its results."""

    def __init__(self, thread: threading.Thread, results: list[UpdateInfo]) -> None:
        self.thread = thread
        self.results = results


def start_update_check(packages: list[str]) -> UpdateCheck:
    """Start an update check in a background thread.

    Returns an UpdateCheck handle. Pass it to collect_and_warn() to get results.
    """
    results: list[UpdateInfo] = []

    def _worker() -> None:
        try:
            results.extend(check_for_updates(packages))
        except Exception:
            logger.debug("Update check failed", exc_info=True)

    thread = threading.Thread(target=_worker, daemon=True, name="huitzo-update-check")
    thread.start()
    return UpdateCheck(thread=thread, results=results)


def collect_and_warn(handle: UpdateCheck, console: Console, timeout: float = 3.0) -> None:
    """Wait for the update check thread and print warnings."""
    try:
        handle.thread.join(timeout=timeout)
        if handle.thread.is_alive():
            logger.debug("Update check timed out, skipping results")
            return
        for info in handle.results:
            console.print(
                f"[yellow]Warning: {info.package} {info.installed} installed, "
                f"{info.latest} available — run: pip install --upgrade {info.package}[/yellow]"
            )
    except Exception:
        logger.debug("Failed to collect update check results", exc_info=True)
