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

from __future__ import annotations

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

from g_gremlin.paths import resolve_gremlin_home

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 resolve_gremlin_home()


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 _is_pipx_install() -> bool:
    """Detect if g-gremlin was installed via pipx."""
    exe = sys.executable or ""
    if os.getenv("PIPX_HOME") or os.getenv("PIPX_BIN_DIR"):
        return True
    # pipx venvs live under .local/pipx/venvs/ (Unix) or pipx\venvs\ (Windows)
    return "pipx" in exe.replace("\\", "/").lower()


def _upgrade_command() -> str:
    if _is_pipx_install():
        return "pipx upgrade g-gremlin"
    return "python -m pip install -U g-gremlin"


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}. "
                f"Run: {_upgrade_command()}"
            )

    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)
