/**
 * Update-check core for madarX OS self-update (T3).
 *
 * Pure-ish: version compare + npm-registry lookup + read/write of the
 * $MADARX_HOME/.update_check ledger ({ ts, current, latest, behind }). Network
 * and clock are injectable so the logic is unit-testable without hitting the
 * registry. Network failures are soft — a check that can't reach npm leaves the
 * previous ledger untouched and reports behind:false.
 */

import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

export const PKG_NAME = 'madarx';
const REGISTRY_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;

const here = path.dirname(fileURLToPath(import.meta.url));

/** Read package.json at `p`; return its version only if it is the madarx package. */
function readMadarxVersion(p) {
  try {
    const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
    if (pkg && pkg.name === PKG_NAME && pkg.version) return pkg.version;
  } catch { /* missing / unreadable / not our package */ }
  return null;
}

/**
 * The installed version.
 *
 * Reading `<here>/../package.json` works when lib/update-check.js runs as a real
 * file (CLI, standalone daemons). But inside the Next standalone web server this
 * module is BUNDLED into a server chunk, so `import.meta.url` points deep into
 * `.next/server/…` and the relative read misses — which silently degraded to
 * '0.0.0' and made the update banner claim every install was out of date.
 *
 * Resolve defensively instead: an explicit MADARX_VERSION env override first
 * (set by the CLI supervisor), then a list of candidate package.json paths. The
 * web server is spawned with cwd = package root, and Next copies the real
 * name+version into `.next/standalone/package.json`, so both cover the bundled
 * case. Every candidate is validated to be the madarx package before trusting it.
 */
export function currentVersion() {
  if (process.env.MADARX_VERSION) return process.env.MADARX_VERSION;
  const candidates = [
    path.join(here, '..', 'package.json'),
    path.join(process.cwd(), 'package.json'),
    path.join(process.cwd(), '.next', 'standalone', 'package.json'),
  ];
  for (const c of candidates) {
    const v = readMadarxVersion(c);
    if (v) return v;
  }
  return '0.0.0';
}

/**
 * Compare two semver-ish strings. Returns 1 if a > b, -1 if a < b, 0 if equal.
 * Ignores any prerelease suffix and a 4th segment — good enough for "is there a
 * newer release" gating.
 */
export function compareSemver(a, b) {
  const parse = (v) => String(v).split('-')[0].split('.').slice(0, 3).map((n) => parseInt(n, 10) || 0);
  const x = parse(a);
  const y = parse(b);
  for (let i = 0; i < Math.max(x.length, y.length); i++) {
    const d = (x[i] || 0) - (y[i] || 0);
    if (d !== 0) return d > 0 ? 1 : -1;
  }
  return 0;
}

/** Fetch the latest published version from the npm registry. null on any failure. */
export async function fetchLatestVersion({ fetchImpl = fetch, timeoutMs = 4000 } = {}) {
  try {
    const res = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(timeoutMs) });
    if (!res.ok) return null;
    const body = await res.json();
    return body?.version || null;
  } catch {
    return null;
  }
}

export function readUpdateCheck(file) {
  try {
    return JSON.parse(fs.readFileSync(file, 'utf8'));
  } catch {
    return null;
  }
}

export function writeUpdateCheck(file, data) {
  fs.mkdirSync(path.dirname(file), { recursive: true });
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
  return data;
}

/**
 * Run a check and persist the ledger. Returns { ts, current, latest, behind }.
 * If the registry is unreachable, leaves the file untouched and returns a
 * behind:false result carrying the previous `latest` if known.
 */
export async function runUpdateCheck({ file, current = currentVersion(), fetchImpl = fetch, nowIso } = {}) {
  const latest = await fetchLatestVersion({ fetchImpl });
  const ts = nowIso || new Date().toISOString();
  if (!latest) {
    const prev = file ? readUpdateCheck(file) : null;
    return { ts, current, latest: prev?.latest || null, behind: false, reachable: false };
  }
  const behind = compareSemver(latest, current) > 0;
  const result = { ts, current, latest, behind, reachable: true };
  if (file) writeUpdateCheck(file, result);
  return result;
}

/**
 * Should the weekly job auto-apply? Pure decision. True only when the operator
 * opted into hands-off updates at setup time AND a newer version exists.
 *
 *   mode  — config 'auto_update_mode'  ('weekly' | 'off')
 *   apply — config 'auto_update_apply' ('auto' | 'notify')
 */
export function shouldAutoApply({ behind, mode, apply }) {
  return Boolean(behind) && mode === 'weekly' && apply === 'auto';
}
