import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
import path from "node:path";
export class StatePersistenceError extends Error {
    filePath;
    causeDetail;
    constructor(message, filePath, causeDetail) {
        super(message);
        this.filePath = filePath;
        this.causeDetail = causeDetail;
        this.name = "StatePersistenceError";
    }
}
const PRIVATE_DIR_MODE = 0o700;
const PRIVATE_FILE_MODE = 0o600;
export function writeJsonFileAtomic(filePath, value, options = {}) {
    writeTextFileAtomic(filePath, `${JSON.stringify(value, null, 2)}\n`, options);
}
export function updateJsonFileAtomic(filePath, updater) {
    return withJsonFileLock(filePath, () => {
        const read = readJsonFileWithBackup(filePath);
        const next = updater(read.value);
        writeJsonFileAtomic(filePath, next, { preserveExistingBackup: read.recoveredFromBackup });
        return next;
    });
}
export function writeTextFileAtomic(filePath, value, options = {}) {
    const dir = path.dirname(filePath);
    ensurePrivateDir(dir);
    const backupPath = `${filePath}.bak`;
    const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
    try {
        writeFileSync(tempPath, value, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
        chmodPrivateFile(tempPath);
        if (existsSync(filePath) && !options.preserveExistingBackup) {
            copyFileSync(filePath, backupPath);
            chmodPrivateFile(backupPath);
        }
        else if (options.preserveExistingBackup && existsSync(backupPath)) {
            chmodPrivateFile(backupPath);
        }
        renameSync(tempPath, filePath);
        chmodPrivateFile(filePath);
    }
    catch (error) {
        rmSync(tempPath, { force: true });
        throw error;
    }
}
export function ensurePrivateDir(dir) {
    mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
    chmodPrivateDir(dir);
}
export function chmodPrivateFile(filePath) {
    try {
        chmodSync(filePath, PRIVATE_FILE_MODE);
    }
    catch {
        // Best effort on filesystems/platforms that do not support POSIX modes.
    }
}
function chmodPrivateDir(dir) {
    try {
        chmodSync(dir, PRIVATE_DIR_MODE);
    }
    catch {
        // Best effort on filesystems/platforms that do not support POSIX modes.
    }
}
export function readJsonFileWithBackup(filePath) {
    const primary = readJsonFile(filePath);
    const primaryReadError = primary.ok ? undefined : primary.error;
    const primaryVersionError = primary.ok ? unsupportedVersionError(primary.value) : undefined;
    if (primary.ok && !primaryVersionError) {
        return { value: primary.value, recoveredFromBackup: false };
    }
    if (primaryVersionError) {
        preserveUnreadableState(filePath);
        throw new StatePersistenceError(`Cannot read state file ${filePath}: ${primaryVersionError}`, filePath, primaryVersionError);
    }
    const backupPath = `${filePath}.bak`;
    const backup = readJsonFile(backupPath);
    const backupReadError = backup.ok ? undefined : backup.error;
    const backupVersionError = backup.ok ? unsupportedVersionError(backup.value) : undefined;
    if (backup.ok && !backupVersionError) {
        return {
            value: backup.value,
            recoveredFromBackup: true,
            error: primaryReadError ?? primaryVersionError,
        };
    }
    const error = primaryReadError ?? primaryVersionError ?? backupReadError ?? backupVersionError;
    if (error && existsSync(filePath)) {
        preserveUnreadableState(filePath);
    }
    if (error && existsSync(filePath)) {
        throw new StatePersistenceError(`Cannot read state file ${filePath}: ${error}`, filePath, error);
    }
    return {
        value: undefined,
        recoveredFromBackup: false,
        error,
    };
}
function preserveUnreadableState(filePath) {
    const corruptPath = `${filePath}.corrupt-${Date.now()}`;
    try {
        copyFileSync(filePath, corruptPath);
    }
    catch {
        // Best-effort only. The original file remains untouched.
    }
}
export function assertSupportedStatePayload(value, filePath) {
    const error = unsupportedVersionError(value);
    if (error) {
        throw new StatePersistenceError(`Cannot read state file ${filePath}: ${error}`, filePath, error);
    }
}
function unsupportedVersionError(value) {
    if (typeof value !== "object" || value === null || Array.isArray(value) || !("version" in value)) {
        return undefined;
    }
    return value.version === 1
        ? undefined
        : `Unsupported state payload version: ${String(value.version)}`;
}
function readJsonFile(filePath) {
    if (!existsSync(filePath)) {
        return { ok: false };
    }
    try {
        return { ok: true, value: JSON.parse(readFileSync(filePath, "utf8")) };
    }
    catch (error) {
        return {
            ok: false,
            error: error instanceof Error ? error.message : String(error),
        };
    }
}
function withJsonFileLock(filePath, fn) {
    const dir = path.dirname(filePath);
    ensurePrivateDir(dir);
    const lockPath = `${filePath}.lock`;
    const deadline = Date.now() + 10_000;
    while (true) {
        try {
            mkdirSync(lockPath);
            break;
        }
        catch (error) {
            if (!isFileExistsError(error)) {
                throw error;
            }
            removeStaleLock(lockPath);
            if (Date.now() > deadline) {
                throw new Error(`Timed out waiting for state file lock: ${filePath}`);
            }
            sleepSync(25);
        }
    }
    try {
        return fn();
    }
    finally {
        rmSync(lockPath, { recursive: true, force: true });
    }
}
function removeStaleLock(lockPath) {
    try {
        const ageMs = Date.now() - statSync(lockPath).mtimeMs;
        if (ageMs > 30_000) {
            rmSync(lockPath, { recursive: true, force: true });
        }
    }
    catch {
        // The lock may have disappeared between attempts.
    }
}
function isFileExistsError(error) {
    return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
}
function sleepSync(ms) {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
