"""Out-of-date version warning for aablocks.

On the first API call of a process, aablocks asks PyPI for the latest
released version and, if the installed version is behind, prints a one-line
upgrade hint to stderr. The check runs at most once per calendar day: the
date of the last check (and the latest version it saw) is persisted to
~/.aab/version_check.json, so repeated commands the same day make no
network call and the warning appears only once a day.

The warning only appears when stderr is an interactive terminal, and can be
disabled with `aablocks config version_check false`.

Like banners, this is best-effort and fully isolated: every exception is
swallowed (after logging the fetch) so the version check can never break a
CLI command or SDK call.
"""

from __future__ import annotations

import functools
import json
import sys
import time
from datetime import date

import requests
from packaging.version import InvalidVersion, Version

from .config import CONFIG_DIR, get_version_check_enabled, write_secure_file
from .logging import log_request

PYPI_URL = "https://pypi.org/pypi/aablocks/json"
STATE_FILE_PATH = CONFIG_DIR / "version_check.json"


@functools.cache
def warn_if_outdated_once() -> None:
    """Warn on stderr if a newer aablocks is on PyPI, at most once per process.

    Best-effort and fully isolated: every exception is swallowed so the check
    can never break a CLI command or SDK call. Because the call always
    completes, `functools.cache` memoizes it and a failed attempt is never
    retried within the process.
    """
    # One catch-all guard: the version check is best-effort and must never
    # propagate (the inner try/except below still logs fetch failures).
    try:
        if _warnings_suppressed():
            return

        # Already checked today: stay quiet and make no network call. The
        # warning, if any, was printed on the first invocation of the day.
        if _load_state().get("last_checked") == _today():
            return

        latest = _fetch_latest()
        if latest is None:
            return

        # Persist before warning so a flaky terminal can't make us re-hit
        # PyPI on every command for the rest of the day.
        write_secure_file(STATE_FILE_PATH, {"last_checked": _today(), "latest": latest})

        installed = _installed_version()
        if _is_outdated(installed, latest):
            print(file=sys.stderr)
            print(
                f"aablocks: a newer version is available ({latest} > {installed}). "
                "Upgrade with: pip install --upgrade aablocks",
                file=sys.stderr,
            )
            print(file=sys.stderr)
    except Exception:
        # Do nothing on purpose: the version check is best-effort, so we never
        # let a failure here prevent the actual command from succeeding.
        pass


def _warnings_suppressed() -> bool:
    """Return True when the version warning is disabled for this invocation."""
    if not sys.stderr.isatty():
        return True
    return not get_version_check_enabled()


def _fetch_latest() -> str | None:
    """Return the latest version string from PyPI, or None on any failure."""
    start_time = time.time()
    error_msg = None
    status_code = None
    try:
        resp = requests.get(PYPI_URL, timeout=5)
        status_code = resp.status_code
        resp.raise_for_status()
        version = resp.json()["info"]["version"]
        return version if isinstance(version, str) else None
    except Exception as e:
        error_msg = str(e)
        return None
    finally:
        latency_ms = int((time.time() - start_time) * 1000)
        log_request("GET", PYPI_URL, None, status_code, error_msg, latency_ms)


def _installed_version() -> str:
    """Return the installed aablocks version (imported lazily to avoid a cycle)."""
    from . import __version__

    return __version__


def _is_outdated(installed: str, latest: str) -> bool:
    """Return True when ``latest`` is a strictly newer release than ``installed``."""
    try:
        return Version(latest) > Version(installed)
    except InvalidVersion:
        return False


def _today() -> str:
    """Return today's local date as an ISO string (e.g. '2026-06-05')."""
    return date.today().isoformat()


def _load_state() -> dict[str, str]:
    """Load the once-per-day state file. Empty dict on any error."""
    try:
        with open(STATE_FILE_PATH) as f:
            data = json.load(f)
    except (OSError, ValueError):
        return {}
    return data if isinstance(data, dict) else {}
