/**
 * Update State Persistence
 * Manages persistent state for the proxy auto-update feature.
 * Tracks check timestamps, suppressed versions, and update history.
 *
 * State file location: ~/.neurolink/update-state.json
 * Suppressed versions expire after 24 hours.
 */
import fs from "fs";
import os from "os";
import path from "path";
import { randomUUID } from "node:crypto";
// ============================================
// Constants
// ============================================
const STATE_FILENAME = "update-state.json";
const SUPPRESSION_TTL_MS = 86_400_000; // 24 hours
// ============================================
// Internal Helpers
// ============================================
/**
 * Resolve the path to the update state file.
 * Accepts an override for testing; defaults to ~/.neurolink/update-state.json.
 */
function resolveStatePath(overridePath) {
    if (overridePath) {
        return overridePath;
    }
    return path.join(os.homedir(), ".neurolink", STATE_FILENAME);
}
/**
 * Ensure the parent directory of the given file path exists.
 */
function ensureParentDir(filePath) {
    const dir = path.dirname(filePath);
    if (!fs.existsSync(dir)) {
        fs.mkdirSync(dir, { recursive: true });
    }
}
// ============================================
// Exported Functions
// ============================================
/**
 * Return an empty/initial UpdateState.
 */
export function getDefaultUpdateState() {
    return {
        lastCheckAt: new Date(0).toISOString(),
        lastCheckVersion: "",
        suppressedVersions: {},
        lastUpdateAt: null,
        lastUpdateVersion: null,
    };
}
/**
 * Load the update state from disk.
 * Returns null if the file does not exist.
 * Returns the default state if the file contains corrupt JSON.
 *
 * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
 */
export function loadUpdateState(stateFilePath) {
    const filePath = resolveStatePath(stateFilePath);
    try {
        if (!fs.existsSync(filePath)) {
            return null;
        }
        const content = fs.readFileSync(filePath, "utf8");
        const parsed = JSON.parse(content);
        // Minimal shape check — reject valid JSON that isn't an UpdateState
        // Note: typeof null === "object", so we check both
        if (typeof parsed !== "object" ||
            parsed === null ||
            typeof parsed.suppressedVersions !== "object" ||
            parsed.suppressedVersions === null ||
            Array.isArray(parsed.suppressedVersions) ||
            typeof parsed.lastCheckAt !== "string") {
            return getDefaultUpdateState();
        }
        return parsed;
    }
    catch {
        // Corrupt or unreadable JSON — return default state
        return getDefaultUpdateState();
    }
}
/**
 * Save the update state to disk.
 *
 * @param state - The UpdateState to persist
 * @param stateFilePath - Override path for testing (default: ~/.neurolink/update-state.json)
 */
export function saveUpdateState(state, stateFilePath) {
    const filePath = resolveStatePath(stateFilePath);
    ensureParentDir(filePath);
    // A process-unique temp path prevents concurrent update checks from renaming
    // each other's file. The guard is single-owner now, but this also keeps
    // explicit update commands safe when they overlap.
    const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
    try {
        fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2), { mode: 0o600 });
        fs.renameSync(tmpPath, filePath);
    }
    finally {
        try {
            fs.rmSync(tmpPath, { force: true });
        }
        catch {
            // Best-effort cleanup after a successful rename or interrupted write.
        }
    }
}
/**
 * Check whether a version is currently suppressed (i.e., suppressed AND within the 24-hour window).
 *
 * @param version - Semver version string to check
 * @param stateFilePath - Override path for testing
 */
export function isVersionSuppressed(version, stateFilePath) {
    const state = loadUpdateState(stateFilePath);
    if (!state) {
        return false;
    }
    const entry = state.suppressedVersions[version];
    if (!entry) {
        return false;
    }
    return Date.now() - Date.parse(entry.suppressedAt) < SUPPRESSION_TTL_MS;
}
/**
 * Add a version to the suppressed list and persist.
 *
 * @param version - Semver version string to suppress
 * @param reason - Human-readable reason for suppression
 * @param stateFilePath - Override path for testing
 */
export function suppressVersion(version, reason, stateFilePath) {
    const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
    state.suppressedVersions[version] = {
        suppressedAt: new Date().toISOString(),
        reason,
    };
    saveUpdateState(state, stateFilePath);
}
/**
 * Record a successful update: set lastUpdateAt and lastUpdateVersion, then persist.
 *
 * @param version - The version that was successfully installed
 * @param stateFilePath - Override path for testing
 */
export function recordSuccessfulUpdate(version, stateFilePath) {
    const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
    state.lastUpdateAt = new Date().toISOString();
    state.lastUpdateVersion = version;
    saveUpdateState(state, stateFilePath);
}
/**
 * Record an update check: set lastCheckAt and lastCheckVersion, then persist.
 *
 * @param latestVersion - The latest version found during the check
 * @param stateFilePath - Override path for testing
 */
export function recordCheck(latestVersion, stateFilePath) {
    const state = loadUpdateState(stateFilePath) ?? getDefaultUpdateState();
    state.lastCheckAt = new Date().toISOString();
    state.lastCheckVersion = latestVersion;
    saveUpdateState(state, stateFilePath);
}
