"""sshj.updatecheck — `sshj update-check`: a REAL update check (F4).

The existing `update_needed()` (jsonio) is local-only — it flags from
`SSHJ_UPDATE=1` or the `update.flag` file, and never touches the network.
This module is the one place sshj does reach out: it fetches the published
version, compares it with `VER`, and sets `update.flag` when the remote is
newer. It runs ONLY when invoked (`sshj update-check` / `sshj update-check
--now`) — never on the default path, never from the grid — which is what
keeps F4 inside the anti-goal "no runtime network" (the picker itself stays
a local tool; only this explicit opt-in command has a network surface).

Stdlib only (urllib), bounded (timeout), cached (one check per day per
`SSHJ_STATE_DIR` so a daily `sshj` doesn't hammer the endpoint).
Exit: 0 up-to-date or update-available (the flag is set, the grid shows
"- UPDATE"); 1 the check itself failed (endpoint unreachable, bad version).
"""
import json
import os
import time
import urllib.request
import urllib.error

from . import jsonio
from .theme import VER

# where the published version lives; override for a mirror or a test server
# (SSHJ_UPDATE_URL). The default is the project's raw VERSION file.
DEFAULT_UPDATE_URL = os.environ.get(
    "SSHJ_UPDATE_URL",
    "https://raw.githubusercontent.com/serafij/sshj/main/VERSION")
# one network check per day (seconds); `--now` bypasses the cache.
CHECK_EVERY = 24 * 3600
# how long to wait on the endpoint before giving up (bounded by contract).
FETCH_TIMEOUT = 5.0
# the cache: {remote_version, checked_at} — informational + the throttle.
CACHE_FILE = "update-cache.json"
FLAG_FILE = "update.flag"


def _state_dir():
    # read live (not a module-level snapshot) so a test's state-dir redirect
    # is honored.
    return jsonio.STATE_DIR


def _cache_path():
    return os.path.join(_state_dir(), CACHE_FILE)


def _flag_path():
    return os.path.join(_state_dir(), FLAG_FILE)


def _read_cache():
    try:
        with open(_cache_path()) as f:
            d = json.load(f)
        return d if isinstance(d, dict) else {}
    except (OSError, ValueError):
        return {}


def _write_cache(remote_version):
    os.makedirs(_state_dir(), exist_ok=True)
    d = {"remote_version": remote_version, "checked_at": int(time.time())}
    with open(_cache_path(), "w") as f:
        json.dump(d, f)


def _fetch(url):
    """Fetch the published version string (bounded). Returns the stripped
    version or None on any failure."""
    req = urllib.request.Request(url, headers={"User-Agent": "sshj/" + VER})
    try:
        with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as r:
            return r.read().decode("utf-8", "replace").strip()
    except (urllib.error.URLError, OSError, ValueError):
        return None


def _newer(remote, local):
    """True if `remote` > `local` as dotted versions (non-numeric parts
    compare lexicographically after the numeric prefix)."""
    def key(v):
        parts = []
        for p in str(v).split("."):
            num = ""
            for c in p:
                if c.isdigit():
                    num += c
                else:
                    break
            parts.append((int(num) if num else 0, p[len(num):]))
        return parts
    return key(remote) > key(local)


def run(args, _fetcher=_fetch):
    """Do the check and print the outcome. `args` may contain `--now`
    (bypass the cache). `_fetcher` is injectable for tests. Returns the
    exit code (0 up-to-date/update-available, 1 the check failed)."""
    force = "--now" in args
    url = os.environ.get("SSHJ_UPDATE_URL", DEFAULT_UPDATE_URL)
    cache = _read_cache()
    age = None
    if "checked_at" in cache:
        age = time.time() - float(cache.get("checked_at", 0))
    if not force and age is not None and age < CHECK_EVERY:
        remote = cache.get("remote_version", "")
        print("sshj update-check: cached (%.0fh old) — %s (use --now to "
              "force)" % (age / 3600.0,
                          "update available: %s" % remote if remote
                          else "no remote version"))
        return 0
    remote = _fetcher(url)
    if remote is None:
        print("sshj update-check: could not reach %s (no update check "
              "performed; the local flag is untouched)" % url)
        return 1
    _write_cache(remote)
    if _newer(remote, VER):
        os.makedirs(_state_dir(), exist_ok=True)
        with open(_flag_path(), "w") as f:
            f.write("update available: %s (current %s)\n" % (remote, VER))
        print("sshj update-check: UPDATE AVAILABLE — %s (you are on %s); "
              "the grid now shows '- UPDATE'" % (remote, VER))
    else:
        try:
            os.remove(_flag_path())
        except OSError:
            pass
        print("sshj update-check: up to date (%s)" % VER)
    return 0
