// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// SPEC-251 — Update Notifier: checks npm registry for newer @planu/cli versions.
// Fire-and-forget at MCP server startup. Caches result 24h to avoid hammering npm.
import { get } from 'node:https';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { PLANU_VERSION } from '../config/version.js';
const NPM_REGISTRY_URL = 'https://registry.npmjs.org/@planu%2Fcli/latest';
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
// ---------------------------------------------------------------------------
// In-process banner cache — consumed once per tool response
// ---------------------------------------------------------------------------
let pendingUpdateBanner = null;
/**
 * When true the version gap is significant (≥3 minor versions behind).
 * The banner is NOT cleared after consumption so it repeats on every tool call,
 * effectively forcing the user to notice and update.
 */
let isDeprecatedVersion = false;
/**
 * Returns the pending update banner.
 * - Minor updates: cleared after first consumption (shows once per session).
 * - Deprecated versions (≥3 minor behind): persists on every tool call.
 */
export function consumeUpdateBanner() {
    const banner = pendingUpdateBanner;
    if (!isDeprecatedVersion) {
        pendingUpdateBanner = null; // Clear for minor updates
    }
    // Deprecated: keep banner set so it shows on every subsequent tool call
    return banner;
}
/** Resets in-process banner state. Used only in tests. */
export function resetBannerState() {
    pendingUpdateBanner = null;
    isDeprecatedVersion = false;
}
function getCachePath(dataDir) {
    return join(dataDir, 'global', 'update-check.json');
}
function readCache(cachePath) {
    if (!existsSync(cachePath)) {
        return null;
    }
    try {
        const raw = JSON.parse(readFileSync(cachePath, 'utf8'));
        if (typeof raw === 'object' &&
            raw !== null &&
            typeof raw.checkedAt === 'string' &&
            typeof raw.latestVersion === 'string') {
            return raw;
        }
    }
    catch {
        // corrupt cache — ignore
    }
    return null;
}
function writeCache(cachePath, latest) {
    try {
        writeFileSync(cachePath, JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion: latest }, null, 2), 'utf8');
    }
    catch {
        // best-effort
    }
}
function isCacheValid(cache) {
    const age = Date.now() - new Date(cache.checkedAt).getTime();
    return age < CACHE_TTL_MS;
}
function fetchLatestVersion() {
    return new Promise((resolve, reject) => {
        const req = get(NPM_REGISTRY_URL, { timeout: 5000 }, (res) => {
            if (res.statusCode !== 200) {
                reject(new Error(`npm registry returned ${String(res.statusCode)}`));
                return;
            }
            let body = '';
            res.on('data', (chunk) => {
                body += chunk.toString();
            });
            res.on('end', () => {
                try {
                    const parsed = JSON.parse(body);
                    const version = typeof parsed === 'object' &&
                        parsed !== null &&
                        typeof parsed.version === 'string'
                        ? parsed.version
                        : null;
                    if (version) {
                        resolve(version);
                    }
                    else {
                        reject(new Error('version field missing in npm response'));
                    }
                }
                catch {
                    reject(new Error('failed to parse npm registry response'));
                }
            });
        });
        req.on('error', reject);
        req.on('timeout', () => {
            req.destroy();
            reject(new Error('npm registry request timed out'));
        });
    });
}
function parseVersion(v) {
    const parts = v.replace(/^v/, '').split('.').map(Number);
    return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
}
function isNewerVersion(current, latest) {
    const [cMaj, cMin, cPat] = parseVersion(current);
    const [lMaj, lMin, lPat] = parseVersion(latest);
    if (lMaj !== cMaj) {
        return lMaj > cMaj;
    }
    if (lMin !== cMin) {
        return lMin > cMin;
    }
    return lPat > cPat;
}
/**
 * Returns true when the version gap is >= DEPRECATED_MINOR_GAP minor versions.
 * In that case the update banner persists on every tool call.
 */
const DEPRECATED_MINOR_GAP = 3;
function isDeprecated(current, latest) {
    const [cMaj, cMin] = parseVersion(current);
    const [lMaj, lMin] = parseVersion(latest);
    if (lMaj !== cMaj) {
        return true;
    } // Major version behind → always deprecated
    return lMin - cMin >= DEPRECATED_MINOR_GAP;
}
function emitUpdateHint(latest, deprecated) {
    const urgency = deprecated ? '🚨 OUTDATED' : '🆕 Update';
    const msg = [
        `[Planu] ${urgency}: v${PLANU_VERSION} → v${latest}`,
        `[Planu]    Run: npm install -g @planu/cli@latest`,
        `[Planu]    Or:  npx @planu/cli@latest`,
    ].join('\n');
    process.stderr.write(msg + '\n');
    isDeprecatedVersion = deprecated;
    if (deprecated) {
        pendingUpdateBanner = [
            `🚨 Planu OUTDATED: v${PLANU_VERSION} is ${DEPRECATED_MINOR_GAP}+ minor versions behind v${latest}.`,
            `Some tools may not work correctly. Update NOW:`,
            `  npm install -g @planu/cli@latest`,
            `  — or run: check_update_status for instructions`,
        ].join('\n');
    }
    else {
        pendingUpdateBanner = `⚠️  Update available: v${PLANU_VERSION} → v${latest}. Run: npm install -g @planu/cli@latest`;
    }
}
/**
 * Checks npm registry for a newer version of @planu/cli.
 * Uses a 24h cache. Fire-and-forget — never throws or blocks startup.
 *
 * @param dataDir - Project data directory (default: process.cwd()/data)
 */
export async function checkForUpdates(dataDir) {
    const resolvedDataDir = dataDir ?? join(process.cwd(), 'data');
    const cachePath = getCachePath(resolvedDataDir);
    try {
        const cache = readCache(cachePath);
        let latest;
        if (cache && isCacheValid(cache)) {
            latest = cache.latestVersion;
        }
        else {
            latest = await fetchLatestVersion();
            writeCache(cachePath, latest);
        }
        if (isNewerVersion(PLANU_VERSION, latest)) {
            emitUpdateHint(latest, isDeprecated(PLANU_VERSION, latest));
        }
    }
    catch {
        // best-effort: network errors, missing data dir, etc. — never surface to user
    }
}
//# sourceMappingURL=update-notifier.js.map