/**
 * Rate-limit state — `state/last-self-restart-at.json`.
 *
 * Tracks wall-clock of the last self-restart, ring-buffer history, and
 * rate-limit decisions across buckets. Atomic writes via tmp+rename.
 *
 * Fail-closed read semantics per spec:
 *   - missing → clear to restart
 *   - malformed / unreadable → block (treat as "just restarted")
 *   - future timestamp → ALLOW and overwrite (previous "block" was a deadlock)
 */
import fs from 'node:fs';
import path from 'node:path';
export const HISTORY_CAP = 50;
export const WATCHDOG_COOLDOWN_MS = 10 * 60 * 1000; // 10 min
export const VERSION_SKEW_COOLDOWN_MS = 10 * 60 * 1000; // 10 min between same-bucket
export const VERSION_SKEW_DAILY_CAP = 3; // max 3 per 24h
export const RESTART_STORM_WINDOW_MS = 60 * 60 * 1000; // 1h window
export const RESTART_STORM_THRESHOLD = 6; // 6 restarts in 1h
export function statePath(stateDir) {
    return path.join(stateDir, 'last-self-restart-at.json');
}
/**
 * Read + classify the rate-limit file.
 *
 * - Missing            → { clear }
 * - Parse error / bad  → { corrupt } (signal + block)
 * - Future timestamp   → { skew } (signal + ALLOW — overwrite avoids deadlock)
 * - Otherwise          → { ok, state }
 */
export function readRateLimitState(stateDir, now = Date.now()) {
    const p = statePath(stateDir);
    let raw;
    try {
        raw = fs.readFileSync(p, 'utf-8');
    }
    catch (err) {
        if (err.code === 'ENOENT') {
            return { kind: 'clear', state: null };
        }
        return { kind: 'corrupt', state: null, errorSignal: 'rateLimitFileCorrupt' };
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return { kind: 'corrupt', state: null, errorSignal: 'rateLimitFileCorrupt' };
    }
    if (!parsed ||
        typeof parsed !== 'object' ||
        typeof parsed.lastRestartAt !== 'string' ||
        !Array.isArray(parsed.history)) {
        return { kind: 'corrupt', state: null, errorSignal: 'rateLimitFileCorrupt' };
    }
    const state = parsed;
    const t = Date.parse(state.lastRestartAt);
    if (Number.isNaN(t)) {
        return { kind: 'corrupt', state: null, errorSignal: 'rateLimitFileCorrupt' };
    }
    if (t > now) {
        return { kind: 'skew', state, errorSignal: 'rateLimitFileSkew' };
    }
    return { kind: 'ok', state };
}
/** Decide whether a restart in the given bucket is allowed right now. */
export function decide(outcome, bucket, now = Date.now()) {
    if (outcome.kind === 'clear')
        return { allowed: true };
    if (outcome.kind === 'corrupt')
        return { allowed: false, reason: 'rate-limit-file-corrupt' };
    if (outcome.kind === 'skew')
        return { allowed: true };
    const state = outcome.state;
    const elapsed = Math.max(0, now - Date.parse(state.lastRestartAt));
    // versionSkew + plannedUpgrade are HARD INCOMPATIBILITY signals.
    // versionSkew = lifeline detected a 426 from /internal/telegram-forward.
    // plannedUpgrade = AutoUpdater / server / migrator wrote a coordinated
    //                  restart signal because a major.minor crossing happened.
    // Neither can be cured by waiting; only a restart resolves them. So we
    // bypass the watchdog cooldown for both. The daily cap (below) still
    // applies as the loop-safety backstop so a misconfigured handshake
    // can't infinitely restart-cycle.
    const isHardSkewBucket = bucket === 'versionSkew' || bucket === 'plannedUpgrade';
    if (!isHardSkewBucket && elapsed < WATCHDOG_COOLDOWN_MS) {
        return { allowed: false, reason: 'cooldown-active' };
    }
    if (isHardSkewBucket) {
        // Both buckets count toward the same daily cap so a misbehaving signal
        // can't sneak past via bucket-hopping.
        const recentHardSkew = state.history.filter(h => (h.bucket === 'versionSkew' || h.bucket === 'plannedUpgrade') &&
            now - Date.parse(h.at) < 24 * 60 * 60 * 1000).length;
        if (recentHardSkew >= VERSION_SKEW_DAILY_CAP) {
            return { allowed: false, reason: 'version-skew-daily-cap' };
        }
    }
    const stormActive = state.history.filter(h => now - Date.parse(h.at) < RESTART_STORM_WINDOW_MS).length >=
        RESTART_STORM_THRESHOLD;
    return { allowed: true, stormActive };
}
export function writeRateLimitState(stateDir, reason, bucket, prior, now = Date.now()) {
    const nowIso = new Date(now).toISOString();
    const history = [...(prior?.history ?? []), { at: nowIso, reason, bucket }].slice(-HISTORY_CAP);
    const next = {
        lastRestartAt: nowIso,
        lastReason: reason,
        history,
    };
    const p = statePath(stateDir);
    const tmp = `${p}.${process.pid}.tmp`;
    fs.mkdirSync(path.dirname(p), { recursive: true });
    fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
    fs.renameSync(tmp, p);
    return next;
}
export function isRestartStorm(state, now = Date.now()) {
    if (!state)
        return false;
    const recent = state.history.filter(h => now - Date.parse(h.at) < RESTART_STORM_WINDOW_MS);
    return recent.length >= RESTART_STORM_THRESHOLD;
}
//# sourceMappingURL=rateLimitState.js.map