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

from __future__ import annotations

import json
import os
import shutil
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"

# Install method detection constants
_HOMEBREW_PREFIXES = ("/opt/homebrew", "/usr/local/Cellar", "/home/linuxbrew")
_PIPX_BIN_SUFFIXES = (".local/bin", "pipx/venvs")
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 _get_ctxme_executable_path() -> str | None:
    """Get the path to the ctxme or ctx executable.

    Checks for `ctxme` first, then falls back to `ctx`. This ordering
    avoids collision with other `ctx` binaries that may exist in PATH.

    Uses shutil.which to find the binary, which gives us the actual
    installed location rather than the Python interpreter path.

    Returns:
        The resolved path to ctxme/ctx executable, or None if not found.
    """
    # Check ctxme first to avoid collision with other ctx binaries
    for cmd in ("ctxme", "ctx"):
        exe_path = shutil.which(cmd)
        if exe_path:
            # Resolve symlinks to get the actual install location
            return str(Path(exe_path).resolve())
    return None


def detect_install_method() -> str:
    """Detect how ctxme was installed based on the ctxme executable path.

    Uses the ctxme binary location (not sys.executable) to avoid
    misclassifying pip installs when using Homebrew Python.

    Returns:
        "brew" if installed via Homebrew
        "pipx" if installed via pipx
        "pip" as fallback (includes uv, pip, and unknown)
    """
    exe_path = _get_ctxme_executable_path()
    if exe_path is None:
        return "pip"

    # Check for Homebrew: ctxme executable path contains Homebrew prefix
    for prefix in _HOMEBREW_PREFIXES:
        if prefix in exe_path:
            return "brew"

    # Check for pipx: ctxme in pipx bin dir AND venv exists
    # Respect PIPX_BIN_DIR and PIPX_HOME environment variable overrides
    home = Path.home()
    pipx_bin = os.environ.get("PIPX_BIN_DIR", str(home / ".local" / "bin"))
    pipx_home = os.environ.get("PIPX_HOME", str(home / ".local" / "pipx"))

    # Check if ctxme is in pipx bin directory or pipx venvs
    if exe_path.startswith(pipx_bin) or "pipx/venvs" in exe_path:
        # Verify venv exists for ctxme
        venv_path = Path(pipx_home) / "venvs" / "ctxme"
        if venv_path.exists():
            return "pipx"

    # Default to pip
    return "pip"


def get_upgrade_command(install_method: str) -> str:
    """Return the upgrade command for the given install method.

    Args:
        install_method: One of "brew", "pipx", or "pip"

    Returns:
        The shell command to upgrade ctxme
    """
    commands = {
        "brew": "brew upgrade ctxme",
        "pipx": "pipx upgrade ctxme",
        "pip": "pip install --upgrade ctxme",
    }
    return commands.get(install_method, commands["pip"])


def format_update_notification(
    installed: str, latest: str, install_method: str | None = None
) -> str:
    """Format the update notification message.

    Args:
        installed: Currently installed version
        latest: Latest available version
        install_method: Install method (auto-detected if None)

    Returns:
        Formatted notification message per issue #184 spec
    """
    if install_method is None:
        install_method = detect_install_method()
    command = get_upgrade_command(install_method)
    return f"Note: ctxme v{latest} is available (you have v{installed}). Run '{command}' to update."


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
    message = format_update_notification(installed, latest)
    print(message, file=sys.stderr)
