"""
Non-blocking update check for g-gremlin.
"""

from __future__ import annotations

import json
import os
import re
import threading
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any

PYPI_URL = "https://pypi.org/pypi/g-gremlin/json"
DEFAULT_INTERVAL_HOURS = 24
MAX_AGE_DAYS = 7
CACHE_FILENAME = "update_cache.json"


@dataclass
class UpdateCheckState:
    message: str | None
    latest_version: str | None
    current_version: str
    checked_at: datetime | None
    thread: threading.Thread | None


def _now() -> datetime:
    return datetime.now(timezone.utc)


def _cache_dir() -> Path:
    try:
        from platformdirs import user_cache_dir

        return Path(user_cache_dir("g_gremlin"))
    except Exception:
        return Path.home() / ".g_gremlin"


def _cache_path() -> Path:
    return _cache_dir() / CACHE_FILENAME


def _parse_timestamp(value: str | None) -> datetime | None:
    if not value:
        return None
    ts = value.strip()
    if not ts:
        return None
    try:
        if ts.endswith("Z"):
            ts = ts[:-1] + "+00:00"
        return datetime.fromisoformat(ts).astimezone(timezone.utc)
    except Exception:
        return None


def _is_stale(last_checked: datetime | None, interval_hours: int) -> bool:
    if not last_checked:
        return True
    return _now() - last_checked >= timedelta(hours=interval_hours)


def _is_too_old(last_checked: datetime | None, max_age_days: int) -> bool:
    if not last_checked:
        return True
    return _now() - last_checked >= timedelta(days=max_age_days)


def _bool_env(value: str | None) -> bool:
    if value is None:
        return False
    return value.strip().lower() in ("1", "true", "yes", "on")


def _disabled() -> bool:
    if _bool_env(os.getenv("G_GREMLIN_DISABLE_UPDATE_CHECK")):
        return True
    raw = os.getenv("G_GREMLIN_UPDATE_CHECK")
    if raw is None:
        return False
    return raw.strip().lower() in ("0", "false", "no", "off")


def _interval_hours() -> int:
    raw = os.getenv("G_GREMLIN_UPDATE_INTERVAL_HOURS")
    if not raw:
        return DEFAULT_INTERVAL_HOURS
    try:
        value = int(float(raw))
        return max(1, value)
    except Exception:
        return DEFAULT_INTERVAL_HOURS


def _numeric_version(value: str) -> tuple[int, ...] | None:
    nums = [int(x) for x in re.findall(r"\d+", value)]
    if not nums:
        return None
    return tuple(nums)


def _is_version_newer(latest: str, current: str) -> bool:
    if latest == current:
        return False
    try:
        from packaging.version import Version

        return Version(latest) > Version(current)
    except Exception:
        latest_tuple = _numeric_version(latest)
        current_tuple = _numeric_version(current)
        if latest_tuple is None or current_tuple is None:
            return False
        return latest_tuple > current_tuple


def _read_cache() -> dict[str, Any]:
    path = _cache_path()
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def _write_cache(payload: dict[str, Any]) -> None:
    path = _cache_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2), encoding="utf-8")


def _fetch_latest_version(timeout_seconds: float = 2.0) -> tuple[str | None, str | None]:
    try:
        import requests
    except Exception:
        return None, "missing_requests"

    try:
        resp = requests.get(PYPI_URL, timeout=timeout_seconds)
    except Exception as exc:
        return None, f"{exc}"

    if not resp.ok:
        return None, f"http_{resp.status_code}"

    try:
        data = resp.json()
    except Exception as exc:
        return None, f"invalid_json:{exc}"

    latest = str(data.get("info", {}).get("version") or "").strip()
    if not latest:
        return None, "missing_version"
    return latest, None


def _should_run_update_check(argv: list[str] | None) -> bool:
    if _disabled():
        return False
    if os.getenv("PYTEST_CURRENT_TEST") or _bool_env(os.getenv("CI")):
        return False
    if not argv:
        return False
    if "--help" in argv or "-h" in argv:
        return False
    if "--version" in argv:
        return False
    if len(argv) > 1 and argv[1] == "help":
        return False
    return True


def _flag_present(argv: list[str], flag: str) -> bool:
    if flag in argv:
        return True
    prefix = f"{flag}="
    return any(arg.startswith(prefix) for arg in argv)


def _value_for_flag(argv: list[str], flag: str) -> str | None:
    if flag not in argv:
        # look for --flag=value
        prefix = f"{flag}="
        for arg in argv:
            if arg.startswith(prefix):
                return arg[len(prefix):]
        return None
    idx = argv.index(flag)
    if idx + 1 >= len(argv):
        return None
    return argv[idx + 1]


def _should_emit_banner(argv: list[str] | None) -> bool:
    if not argv:
        return False
    if "--help" in argv or "-h" in argv:
        return False
    if "--version" in argv:
        return False
    if len(argv) > 1 and argv[1] == "help":
        return False
    if _flag_present(argv, "--json") or _flag_present(argv, "--json-summary"):
        return False
    fmt = _value_for_flag(argv, "--format")
    if fmt and fmt.lower() == "json":
        return False
    output = _value_for_flag(argv, "--output")
    if output and output.strip() == "-":
        return False
    return True


def _refresh_cache_async(interval_hours: int) -> threading.Thread:
    def _worker() -> None:
        latest, err = _fetch_latest_version()
        payload: dict[str, Any] = {
            "last_checked": _now().isoformat(),
            "source": "pypi",
        }
        if latest:
            payload["latest_version"] = latest
        if err:
            payload["error"] = err
        _write_cache(payload)

    thread = threading.Thread(target=_worker, daemon=True)
    thread.start()
    return thread


def prepare_update_state(current_version: str, argv: list[str] | None) -> UpdateCheckState:
    if not _should_run_update_check(argv):
        return UpdateCheckState(None, None, current_version, None, None)

    cache = _read_cache()
    last_checked = _parse_timestamp(str(cache.get("last_checked") or ""))
    latest_version = str(cache.get("latest_version") or "").strip() or None
    interval_hours = _interval_hours()

    message = None
    if latest_version and _should_emit_banner(argv) and not _is_too_old(last_checked, MAX_AGE_DAYS):
        if _is_version_newer(latest_version, current_version):
            message = (
                f"Update available: {current_version} -> {latest_version}. "
                "Run: pip install --upgrade g-gremlin"
            )

    thread = None
    if _is_stale(last_checked, interval_hours):
        try:
            thread = _refresh_cache_async(interval_hours)
        except Exception:
            thread = None

    return UpdateCheckState(message, latest_version, current_version, last_checked, thread)
