"""Periodic update check for CLI startup."""

from __future__ import annotations

import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path

import httpx
from packaging.version import InvalidVersion, Version
from platformdirs import user_cache_dir

from .config import CACHE_DIR_ENV

PYPI_URL = "https://pypi.org/pypi/ctxme/json"
CACHE_FILENAME = "update_check.json"
DEFAULT_TTL_SECONDS = 86400  # 24 hours
STARTUP_TIMEOUT_SECONDS = 2.0

# Environment variable names
UPDATE_CHECK_ENV = "CTXME_UPDATE_CHECK"
UPDATE_CHECK_INTERVAL_ENV = "CTXME_UPDATE_CHECK_INTERVAL"


def _is_update_check_disabled() -> bool:
    """Return True if update check is disabled via environment variable."""
    value = os.getenv(UPDATE_CHECK_ENV, "").strip().lower()
    return value in {"0", "false", "no"}


def _get_ttl_seconds() -> int:
    """Return the TTL in seconds, respecting CTXME_UPDATE_CHECK_INTERVAL override."""
    value = os.getenv(UPDATE_CHECK_INTERVAL_ENV, "").strip()
    if value:
        try:
            return int(value)
        except ValueError:
            pass
    return DEFAULT_TTL_SECONDS


def _get_cache_dir() -> Path:
    """Return the cache directory, respecting CTXME_CACHE_DIR override."""
    env_dir = os.getenv(CACHE_DIR_ENV)
    if env_dir:
        return Path(env_dir).expanduser()
    return Path(user_cache_dir("ctxme"))


def _read_cache() -> dict | None:
    """Read the update check cache file.

    Returns None if the cache doesn't exist or is invalid.
    """
    try:
        cache_path = _get_cache_dir() / CACHE_FILENAME
        if not cache_path.exists():
            return None
        data = json.loads(cache_path.read_text())
        # Validate required fields
        if "last_check" not in data or "latest_version" not in data:
            return None
        return data
    except (OSError, json.JSONDecodeError, KeyError):
        return None


def _write_cache(
    latest_version: str, installed_version: str, last_check: datetime | None = None
) -> None:
    """Write the update check result to cache.

    Cache format:
    {
        "last_check": "2024-01-21T12:00:00Z",
        "latest_version": "1.2.3",
        "installed_version": "1.0.0"
    }
    """
    try:
        cache_dir = _get_cache_dir()
        cache_dir.mkdir(parents=True, exist_ok=True)
        cache_path = cache_dir / CACHE_FILENAME

        data = {
            "last_check": (last_check or datetime.now(timezone.utc)).isoformat(),
            "latest_version": latest_version,
            "installed_version": installed_version,
        }

        cache_path.write_text(json.dumps(data, indent=2) + "\n")
    except OSError:
        # Best effort - don't fail if we can't write cache
        pass


def _is_cache_stale(cache: dict, ttl_seconds: int) -> bool:
    """Return True if the cache is older than the TTL."""
    try:
        last_check_str = cache.get("last_check", "")
        # Handle ISO format with or without timezone
        if last_check_str.endswith("Z"):
            last_check_str = last_check_str[:-1] + "+00:00"
        last_check = datetime.fromisoformat(last_check_str)
        # Ensure timezone-aware comparison
        if last_check.tzinfo is None:
            last_check = last_check.replace(tzinfo=timezone.utc)
        age = datetime.now(timezone.utc) - last_check
        return age.total_seconds() > ttl_seconds
    except (ValueError, TypeError):
        return True


def _fetch_latest_version_with_timeout() -> str | None:
    """Query PyPI for the latest ctxme version with startup timeout.

    Returns None if the request fails or times out.
    """
    try:
        with httpx.Client(timeout=STARTUP_TIMEOUT_SECONDS) as client:
            response = client.get(PYPI_URL)
            response.raise_for_status()
            data = response.json()
            return data.get("info", {}).get("version")
    except (httpx.HTTPError, KeyError, ValueError):
        return None


def _compare_versions(installed: str, latest: str) -> int:
    """Compare two version strings.

    Returns:
        -1 if installed < latest (update available)
         0 if installed == latest (up to date)
         1 if installed > latest (ahead of release)

    Returns 0 if either version is invalid (treat as up-to-date).
    """
    try:
        installed_v = Version(installed)
        latest_v = Version(latest)
        if installed_v < latest_v:
            return -1
        if installed_v > latest_v:
            return 1
        return 0
    except InvalidVersion:
        return 0


def run_startup_update_check(installed_version: str, invoked_command: str | None = None) -> None:
    """Run the periodic update check at CLI startup.

    This function:
    1. Checks if update check is disabled via env var
    2. Skips if the invoked command is 'update-check'
    3. Checks cache TTL and skips if recent
    4. Fetches latest version from PyPI (2s timeout)
    5. Prints single-line notification to stderr if update available
    6. Updates cache (even on failure, to prevent retry storms)

    Args:
        installed_version: The currently installed CLI version.
        invoked_command: The command being invoked (e.g., 'update-check').
    """
    # Skip if disabled
    if _is_update_check_disabled():
        return

    # Skip if running update-check command explicitly
    if invoked_command == "update-check":
        return

    # Skip for dev/unknown versions
    if installed_version in {"dev", "unknown"}:
        return

    # Validate installed version is parseable
    try:
        Version(installed_version)
    except InvalidVersion:
        return

    ttl_seconds = _get_ttl_seconds()
    cache = _read_cache()

    # Check if we need to fetch
    if cache is not None:
        # Skip if cache is fresh
        if not _is_cache_stale(cache, ttl_seconds):
            # Check if we already notified for this version combination
            cached_installed = cache.get("installed_version")
            cached_latest = cache.get("latest_version")

            # If installed version changed, we need to re-check
            if cached_installed == installed_version and cached_latest:
                # Already checked recently - show notification if needed
                comparison = _compare_versions(installed_version, cached_latest)
                if comparison < 0:
                    _print_update_notification(installed_version, cached_latest)
                return

    # Fetch latest version (with 2s timeout)
    latest_version = _fetch_latest_version_with_timeout()

    # Update cache even on failure (to prevent retry storms)
    if latest_version is None:
        # On failure, update the timestamp. Use existing latest_version if available,
        # otherwise use installed_version as a placeholder (marks "unknown" state).
        existing_latest = cache.get("latest_version") if cache else None
        _write_cache(existing_latest or installed_version, installed_version)
        return

    # Write successful result to cache
    _write_cache(latest_version, installed_version)

    # Show notification if update available
    comparison = _compare_versions(installed_version, latest_version)
    if comparison < 0:
        _print_update_notification(installed_version, latest_version)


def _print_update_notification(installed: str, latest: str) -> None:
    """Print a single-line update notification to stderr."""
    # Use plain print to stderr for minimal footprint
    print(f"Update available: {installed} → {latest}", file=sys.stderr)
