/**
 * Cached version-availability check against the public npm registry.
 *
 * Rationale: `indusk update` used to shell out to `npm view` and trigger an
 * in-process self-install on every invocation, which (a) loops infinitely
 * when the install lands somewhere other than the on-PATH binary, and
 * (b) couples project-state work to machine-state work. The new model is
 * notify-only: a quiet TTL-cached fetch surfaces a one-line "upgrade
 * available" notice, and the user explicitly runs `indusk upgrade` to
 * install. See `bin/commands/upgrade.ts` for the install side.
 *
 * Network failure is silent (returns the cached value if present, else
 * null). Cache lives at `${INDUSK_HOME}/version-check.json` with a 6h TTL,
 * tunable per call. Set `INDUSK_SKIP_UPDATE_CHECK=1` to suppress.
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
const PKG_NAME = "@infinitedusky/indusk-mcp";
const DEFAULT_TTL_MS = 6 * 60 * 60 * 1000; // 6h
const DEFAULT_TIMEOUT_MS = 5_000;
function induskHome() {
    return process.env.INDUSK_HOME ?? join(homedir(), ".indusk");
}
function defaultCachePath() {
    return join(induskHome(), "version-check.json");
}
function readCache(path) {
    if (!existsSync(path))
        return null;
    try {
        const parsed = JSON.parse(readFileSync(path, "utf-8"));
        if (typeof parsed.checkedAt !== "string" || typeof parsed.latestVersion !== "string") {
            return null;
        }
        return { checkedAt: parsed.checkedAt, latestVersion: parsed.latestVersion };
    }
    catch {
        return null;
    }
}
function writeCache(path, cache) {
    mkdirSync(dirname(path), { recursive: true });
    writeFileSync(path, `${JSON.stringify(cache, null, 2)}\n`);
}
function isFresh(cache, ttlMs, now = Date.now()) {
    const checkedAtMs = Date.parse(cache.checkedAt);
    if (!Number.isFinite(checkedAtMs))
        return false;
    return now - checkedAtMs < ttlMs;
}
/**
 * Resolve the latest published version from npm. 5s timeout, JSON-only,
 * no auth. Returns null on any error — callers fall back to cache.
 */
async function fetchLatestFromNpm(timeoutMs, fetchImpl) {
    const url = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
        const res = await fetchImpl(url, {
            headers: { Accept: "application/json" },
            signal: controller.signal,
        });
        if (!res.ok)
            return null;
        const body = (await res.json());
        return typeof body.version === "string" ? body.version : null;
    }
    catch {
        return null;
    }
    finally {
        clearTimeout(timer);
    }
}
/**
 * Resolve the latest version. Read-through cache: if the on-disk cache is
 * fresh, return it without touching the network; otherwise fetch, persist,
 * and return. On network failure with no cache, returns `latestVersion:
 * null`. Honors `INDUSK_SKIP_UPDATE_CHECK=1` (returns null without any
 * disk or network access).
 */
export async function checkLatestVersion(options = {}) {
    // INDUSK_SKIP_UPDATE_CHECK is the new name; INDUSK_SKIP_SELF_UPDATE is
    // the legacy alias used by ~7 existing tests (set to suppress network
    // access during `indusk update` runs against a tmp project). Honor both
    // so test scaffolding doesn't need to change.
    if (process.env.INDUSK_SKIP_UPDATE_CHECK === "1" || process.env.INDUSK_SKIP_SELF_UPDATE === "1") {
        return { latestVersion: null, fromCache: false, checkedAt: null };
    }
    const cachePath = options.cachePath ?? defaultCachePath();
    const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
    const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
    const fetchImpl = options.fetchImpl ?? fetch;
    const bypass = options.bypassCache === true;
    const cached = readCache(cachePath);
    if (!bypass && cached && isFresh(cached, ttlMs)) {
        return {
            latestVersion: cached.latestVersion,
            fromCache: true,
            checkedAt: cached.checkedAt,
        };
    }
    const fresh = await fetchLatestFromNpm(timeoutMs, fetchImpl);
    if (fresh === null) {
        // Network failed — fall back to whatever's cached (even if stale),
        // so the notice is "best known" rather than absent.
        if (cached) {
            return {
                latestVersion: cached.latestVersion,
                fromCache: true,
                checkedAt: cached.checkedAt,
            };
        }
        return { latestVersion: null, fromCache: false, checkedAt: null };
    }
    const checkedAt = new Date().toISOString();
    try {
        writeCache(cachePath, { checkedAt, latestVersion: fresh });
    }
    catch {
        // Cache write failed (permission, full disk) — return the fresh
        // value anyway; next invocation just won't have the cache hit.
    }
    return { latestVersion: fresh, fromCache: false, checkedAt };
}
/**
 * Pure comparator: returns true when `latest` is a different, presumably
 * newer version than `current`. Intentionally string-equality — pre-1.0
 * semver gymnastics aren't worth it for the notify path; if npm publishes
 * a different string than what's installed, we surface it. The user's
 * `upgrade` command does the actual decision.
 */
export function hasNewerVersion(current, latest) {
    return latest !== null && latest.length > 0 && latest !== current;
}
