"""Is this copy of the CLI current, and how would you replace it?

Deliberately never automatic. This CLI's whole claim is "self-hosted,
offline-by-default" — a version check that phoned PyPI on every `abrade
run` would make that false for a convenience nobody asked for, and would
leak a usage signal from air-gapped environments that chose this tool
precisely because it does not talk to anyone. So the network call happens
only when someone types `abrade upgrade`.

The command deliberately does NOT perform the upgrade. It prints the
command for the installer it detected. Running the upgrade itself means
guessing between uv, pipx and pip, and guessing wrong means either a
confusing failure or — worse — a second copy installed alongside the first
under a different manager, which is exactly the state that produces "I
updated it and the new command still isn't there".
"""

from __future__ import annotations

import json
import os
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass
from pathlib import Path

PYPI_URL = "https://pypi.org/pypi/abrade-cli/json"


class UpgradeCheckError(RuntimeError):
    pass


@dataclass(frozen=True)
class Release:
    installed: str
    latest: str

    @property
    def outdated(self) -> bool:
        return _parse(self.latest) > _parse(self.installed)


def _parse(version: str) -> tuple:
    """Compare versions numerically, so 0.1.10 sorts above 0.1.9.

    A string comparison gets that backwards, and the failure is silent:
    the CLI would simply stop reporting upgrades once the patch number
    reached double digits. Non-numeric segments (rc, dev) sort below a
    plain release, which is the conservative direction — it can suggest an
    upgrade that is not needed, never hide one that is.
    """
    parts = []
    for chunk in version.split("."):
        digits = ""
        for char in chunk:
            if not char.isdigit():
                break
            digits += char
        parts.append((int(digits) if digits else 0, chunk == digits))
    return tuple(parts)


def latest_version(*, timeout: float = 10.0) -> str:
    try:
        with urllib.request.urlopen(PYPI_URL, timeout=timeout) as response:  # noqa: S310 — fixed https URL constant
            return str(json.loads(response.read())["info"]["version"])
    except urllib.error.URLError as exc:
        raise UpgradeCheckError(f"Could not reach PyPI: {exc.reason}") from exc
    except (KeyError, ValueError) as exc:
        raise UpgradeCheckError("PyPI returned an unexpected response.") from exc


def install_command() -> str:
    """The upgrade command for whichever installer put this here.

    Detected from the path of the running interpreter rather than asked
    for, because the common failure is not knowing which manager was used
    — and the answer is sitting in sys.executable.
    """
    executable = Path(sys.executable).resolve()
    parts = {p.lower() for p in executable.parts}

    if "uv" in parts or "uv-tool" in parts or os.environ.get("UV_TOOL_DIR"):
        return "uv tool upgrade abrade-cli"
    if "pipx" in parts:
        return "pipx upgrade abrade-cli"
    if any("virtualenvs" in p or ".venv" in p for p in parts):
        # Inside a project environment, so upgrading globally would touch a
        # different copy than the one being run.
        return "uv pip install --upgrade abrade-cli"
    return "pip install --upgrade abrade-cli"


def check(installed: str, *, timeout: float = 10.0) -> Release:
    return Release(installed=installed, latest=latest_version(timeout=timeout))
