/**
 * CLI self-update notifier (v3.13.0+).
 *
 * The framework payload inside `.framework/` is updated by `baldart update`.
 * The CLI itself (this package) is updated by `npm i -g baldart@latest`.
 * Until now those two layers drifted silently — users on an old CLI never
 * learned new flags, new doctor checks, new adapters had shipped.
 *
 * This module surfaces CLI drift WITHOUT auto-installing anything. It is
 * deliberately non-invasive:
 *
 *   - Best-effort HTTP check against the npm registry with a short timeout.
 *   - Result cached at `~/.baldart/cli-update-check.json` (TTL 24h).
 *   - Banner printed from the cache on the NEXT invocation (zero latency on
 *     every run). First-ever run shows nothing — that's intentional.
 *   - Suppressed in CI, non-TTY, --offline, NODE_ENV=test, or via
 *     BALDART_NO_UPDATE_CHECK=1.
 *   - Never blocks the process: the background refresh is fire-and-forget,
 *     its timer/socket are unref'd so they cannot hold the event loop open.
 *
 * Design rationale (see CHANGELOG v3.13.0): we explicitly DO NOT run
 * `npm i -g baldart@latest` automatically. Auto-updates inside a per-project
 * CLI invocation would (a) impact every other project on the user's machine,
 * (b) potentially require sudo, (c) break determinism in CI runs. The banner
 * gives the user the choice; the cache means they don't pay for it.
 */

const fs = require('fs');
const path = require('path');
const os = require('os');

const REGISTRY_URL = 'https://registry.npmjs.org/baldart/latest';
const CACHE_DIR = path.join(os.homedir(), '.baldart');
const CACHE_FILE = path.join(CACHE_DIR, 'cli-update-check.json');
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // 24h
const DEFAULT_TIMEOUT_MS = 1500;

function isSuppressed({ offline } = {}) {
  if (offline) return true;
  if (process.env.BALDART_NO_UPDATE_CHECK === '1') return true;
  if (process.env.CI === 'true' || process.env.CI === '1') return true;
  if (process.env.NODE_ENV === 'test') return true;
  // `process.stdout.isTTY` is `true` when attached to a terminal and
  // `undefined` (NOT `false`) when not — so we must test for "not strictly
  // true" rather than "=== false", otherwise piped/redirected runs silently
  // bypass suppression.
  if (!(process.stdout && process.stdout.isTTY === true)) return true;
  return false;
}

function readCache() {
  try {
    if (!fs.existsSync(CACHE_FILE)) return null;
    const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
    if (!data || typeof data !== 'object') return null;
    return data;
  } catch (_) {
    return null;
  }
}

function writeCache(data) {
  try {
    if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
    fs.writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2) + '\n');
  } catch (_) {
    // Best-effort: a read-only HOME or denied write is not worth surfacing.
  }
}

function cmpSemver(a, b) {
  // Returns 1 if a>b, -1 if a<b, 0 if equal. Strips prerelease and build
  // metadata; only X.Y.Z is compared. That is enough for a "is there a newer
  // stable on npm?" prompt — anything fancier belongs in `semver`, and we are
  // deliberately staying dep-free here.
  if (!a || !b) return 0;
  const norm = (v) => String(v).split(/[-+]/)[0].split('.').map((n) => Number(n) || 0);
  const pa = norm(a);
  const pb = norm(b);
  for (let i = 0; i < 3; i++) {
    const x = pa[i] || 0, y = pb[i] || 0;
    if (x > y) return 1;
    if (x < y) return -1;
  }
  return 0;
}

async function fetchLatest(timeoutMs) {
  if (typeof fetch !== 'function') return null; // Node < 18 fallback — node engine constraint covers this, but defensive.
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), timeoutMs);
  if (typeof timer.unref === 'function') timer.unref();
  try {
    const res = await fetch(REGISTRY_URL, {
      signal: ctrl.signal,
      headers: { Accept: 'application/json' },
    });
    if (!res.ok) return null;
    const body = await res.json();
    return body && body.version ? String(body.version) : null;
  } catch (_) {
    return null;
  } finally {
    clearTimeout(timer);
  }
}

async function refresh({ currentVersion, timeoutMs = DEFAULT_TIMEOUT_MS, force = false } = {}) {
  const cached = readCache();
  const now = Date.now();
  if (!force && cached && cached.checked_at) {
    const age = now - new Date(cached.checked_at).getTime();
    if (Number.isFinite(age) && age < DEFAULT_TTL_MS) return cached;
  }
  const latest = await fetchLatest(timeoutMs);
  if (!latest) return cached; // Keep stale cache rather than zeroing it.
  const data = {
    checked_at: new Date().toISOString(),
    latest,
    current_at_check: currentVersion,
  };
  writeCache(data);
  return data;
}

function hint({ currentVersion } = {}) {
  if (!currentVersion) return null;
  const cached = readCache();
  if (!cached || !cached.latest) return null;
  if (cmpSemver(cached.latest, currentVersion) <= 0) return null;
  return {
    latest: cached.latest,
    current: currentVersion,
    checked_at: cached.checked_at,
  };
}

function printBanner(info) {
  if (!info) return;
  const chalk = require('chalk');
  const line1 = chalk.yellow('↑') + ' ' + chalk.bold(`baldart ${info.latest}`) +
    chalk.gray(` available (you have ${info.current})`);
  const line2 = chalk.gray('  Update with: ') + chalk.cyan('npm i -g baldart@latest');
  const line3 = chalk.gray('  Suppress with: BALDART_NO_UPDATE_CHECK=1');
  console.log('');
  console.log(line1);
  console.log(line2);
  console.log(line3);
  console.log('');
}

function backgroundCheck({ currentVersion, offline = false } = {}) {
  // Fire-and-forget. Returns immediately; never throws; cannot block exit.
  if (isSuppressed({ offline })) return;
  refresh({ currentVersion }).catch(() => {});
}

/**
 * Cheap entry point for bin/baldart.js: print the banner if a cached update
 * is known, then kick off a refresh for next time. No-op when suppressed.
 */
function announceAndRefresh({ currentVersion, offline = false } = {}) {
  if (isSuppressed({ offline })) return null;
  const info = hint({ currentVersion });
  if (info) printBanner(info);
  backgroundCheck({ currentVersion, offline });
  return info;
}

module.exports = {
  isSuppressed,
  refresh,
  hint,
  printBanner,
  backgroundCheck,
  announceAndRefresh,
  // Exported for tests / introspection:
  _cmpSemver: cmpSemver,
  _CACHE_FILE: CACHE_FILE,
  _DEFAULT_TTL_MS: DEFAULT_TTL_MS,
};
