"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RecoverableMutationError = exports.MutationInterruptionError = exports.MutationLockError = exports.DEFAULT_MUTATION_STALE_AFTER_MS = exports.DEFAULT_MUTATION_LEASE_MS = void 0;
exports.mutationLockPath = mutationLockPath;
exports.mutationJournalPath = mutationJournalPath;
exports.acquireMutationLeaseSync = acquireMutationLeaseSync;
exports.renewMutationLeaseSync = renewMutationLeaseSync;
exports.releaseMutationLeaseSync = releaseMutationLeaseSync;
exports.executeRecoverableMutationPlanSync = executeRecoverableMutationPlanSync;
exports.recoverMutationJournalSync = recoverMutationJournalSync;
const node_fs_1 = __importDefault(require("node:fs"));
const node_os_1 = __importDefault(require("node:os"));
const node_path_1 = __importDefault(require("node:path"));
const node_crypto_1 = require("node:crypto");
const atomic_write_js_1 = require("./atomic-write.js");
const multi_file_mutation_js_1 = require("./multi-file-mutation.js");
exports.DEFAULT_MUTATION_LEASE_MS = 30_000;
exports.DEFAULT_MUTATION_STALE_AFTER_MS = 120_000;
class MutationLockError extends Error {
    constructor(message) {
        super(message);
        this.name = 'MutationLockError';
    }
}
exports.MutationLockError = MutationLockError;
class MutationInterruptionError extends Error {
    constructor(message = 'Recoverable mutation interrupted.') {
        super(message);
        this.name = 'MutationInterruptionError';
    }
}
exports.MutationInterruptionError = MutationInterruptionError;
class RecoverableMutationError extends Error {
    result;
    cause;
    constructor(message, result, cause) {
        super(message);
        this.name = 'RecoverableMutationError';
        this.result = result;
        this.cause = cause;
    }
}
exports.RecoverableMutationError = RecoverableMutationError;
function mutationLockPath(scopePath) {
    const resolvedScopePath = resolveScopePath(scopePath);
    return node_path_1.default.join(node_path_1.default.dirname(resolvedScopePath), `.${node_path_1.default.basename(resolvedScopePath)}.mutation.lock`);
}
function mutationJournalPath(scopePath) {
    const resolvedScopePath = resolveScopePath(scopePath);
    return node_path_1.default.join(node_path_1.default.dirname(resolvedScopePath), `.${node_path_1.default.basename(resolvedScopePath)}.mutation-journal.json`);
}
function acquireMutationLeaseSync(scopePath, options = {}) {
    const resolvedScopePath = resolveScopePath(scopePath);
    const lockPath = mutationLockPath(resolvedScopePath);
    const now = options.now ?? Date.now;
    const leaseMs = positiveDuration(options.leaseMs, exports.DEFAULT_MUTATION_LEASE_MS, 'lease');
    const staleAfterMs = positiveDuration(options.staleAfterMs, exports.DEFAULT_MUTATION_STALE_AFTER_MS, 'stale threshold');
    const timeoutMs = nonNegativeDuration(options.timeoutMs, 0, 'acquisition timeout');
    const pollIntervalMs = positiveDuration(options.pollIntervalMs, 25, 'poll interval');
    const sleep = options.sleep ?? sleepSync;
    const ownerToken = options.ownerToken ?? (0, node_crypto_1.randomBytes)(18).toString('hex');
    if (ownerToken.length === 0) {
        throw new TypeError('Mutation lease owner token must be non-empty.');
    }
    const pid = options.pid ?? process.pid;
    const hostname = options.hostname ?? node_os_1.default.hostname();
    const startedAt = now();
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(lockPath), { recursive: true });
    while (true) {
        const timestamp = now();
        const record = {
            schemaVersion: 1,
            scopePath: resolvedScopePath,
            ownerToken,
            hostname,
            pid,
            acquiredAtMs: timestamp,
            heartbeatAtMs: timestamp,
            leaseExpiresAtMs: timestamp + leaseMs,
        };
        try {
            writeExclusiveJson(lockPath, record);
            return { lockPath, record };
        }
        catch (error) {
            if (!isNodeError(error) || error.code !== 'EEXIST') {
                throw error;
            }
        }
        const current = readLeaseRecord(lockPath);
        if (current !== null && isRecoverablyStale(current, timestamp, staleAfterMs, hostname, options.isProcessAlive)) {
            removeStaleLease(lockPath, current.ownerToken);
            continue;
        }
        if (timestamp - startedAt >= timeoutMs) {
            throw new MutationLockError(`Timed out acquiring mutation lease for '${scopePath}'.`);
        }
        sleep(Math.min(pollIntervalMs, timeoutMs - (timestamp - startedAt)));
    }
}
function renewMutationLeaseSync(lease, options = {}) {
    const current = requireOwnedLease(lease);
    const timestamp = (options.now ?? Date.now)();
    const leaseMs = positiveDuration(options.leaseMs, exports.DEFAULT_MUTATION_LEASE_MS, 'lease');
    const renewed = {
        ...current,
        heartbeatAtMs: timestamp,
        leaseExpiresAtMs: timestamp + leaseMs,
    };
    (0, atomic_write_js_1.atomicWriteFileSync)(lease.lockPath, serializeJson(renewed));
    const verified = readLeaseRecord(lease.lockPath);
    if (verified?.ownerToken !== lease.record.ownerToken) {
        throw new MutationLockError('Mutation lease ownership changed during renewal.');
    }
    return { lockPath: lease.lockPath, record: verified };
}
function releaseMutationLeaseSync(lease) {
    requireOwnedLease(lease);
    const verified = readLeaseRecord(lease.lockPath);
    if (verified?.ownerToken !== lease.record.ownerToken) {
        throw new MutationLockError('Mutation lease ownership changed before release.');
    }
    node_fs_1.default.rmSync(lease.lockPath);
}
function executeRecoverableMutationPlanSync(scopePath, plan, options = {}) {
    const lease = acquireMutationLeaseSync(scopePath, options);
    const journalPath = options.journalPath ?? mutationJournalPath(scopePath);
    const now = options.now ?? Date.now;
    const atomicWrite = options.atomicWrite ?? atomic_write_js_1.atomicWriteFileSync;
    const snapshots = new Map();
    const entries = plan.entries.map((entry) => {
        const evidence = (0, multi_file_mutation_js_1.inspectMutationPathSync)(entry.path);
        snapshots.set(entry.resolvedPath, evidence.exists ? Uint8Array.from(node_fs_1.default.readFileSync(entry.resolvedPath)) : null);
        return {
            path: entry.path,
            resolvedPath: entry.resolvedPath,
            beforeHash: evidence.contentHash,
            intendedHash: entry.intendedHash,
            observedAfterHash: evidence.contentHash,
        };
    });
    let journal = {
        schemaVersion: 1,
        scopePath: lease.record.scopePath,
        ownerToken: lease.record.ownerToken,
        planHash: plan.planHash,
        phase: 'prepared',
        updatedAtMs: now(),
        entries,
        receipt: null,
    };
    const persistJournal = (invokeHook = true) => {
        (0, atomic_write_js_1.atomicWriteFileSync)(journalPath, serializeJson(journal));
        if (invokeHook) {
            options.afterJournalWrite?.(journal);
        }
    };
    try {
        persistJournal();
        const receipt = (0, multi_file_mutation_js_1.executeMultiFileMutationPlanSync)(plan, {
            atomicWrite(destination, contents, writeOptions) {
                atomicWrite(destination, contents, writeOptions);
                const journalEntry = journal.entries.find((entry) => entry.resolvedPath === destination);
                if (journalEntry !== undefined) {
                    journalEntry.observedAfterHash = (0, multi_file_mutation_js_1.inspectMutationPathSync)(destination).contentHash;
                }
                journal = { ...journal, phase: 'applying', updatedAtMs: now() };
                persistJournal();
            },
            rollback(context) {
                if (context.cause instanceof MutationInterruptionError) {
                    throw context.cause;
                }
                journal = { ...journal, phase: 'rolling-back', updatedAtMs: now() };
                persistJournal(false);
                for (const affected of [...context.affected].reverse()) {
                    const current = (0, multi_file_mutation_js_1.inspectMutationPathSync)(affected.path);
                    if (current.contentHash !== affected.afterHash) {
                        throw new Error(`Rollback identity changed for '${affected.path}'.`);
                    }
                    const snapshot = snapshots.get(affected.resolvedPath);
                    if (snapshot === undefined) {
                        throw new Error(`Rollback snapshot is unavailable for '${affected.path}'.`);
                    }
                    if (snapshot === null) {
                        node_fs_1.default.rmSync(affected.resolvedPath, { force: true });
                    }
                    else {
                        atomicWrite(affected.resolvedPath, snapshot);
                    }
                    const restored = (0, multi_file_mutation_js_1.inspectMutationPathSync)(affected.path).contentHash;
                    if (restored !== affected.beforeHash) {
                        throw new Error(`Rollback postcondition failed for '${affected.path}'.`);
                    }
                    const journalEntry = journal.entries.find((entry) => entry.resolvedPath === affected.resolvedPath);
                    if (journalEntry !== undefined) {
                        journalEntry.observedAfterHash = restored;
                    }
                    journal = { ...journal, updatedAtMs: now() };
                    persistJournal(false);
                }
            },
        });
        const result = {
            outcome: receipt.outcome,
            planHash: plan.planHash,
            journalPath,
            receipt,
            entries: journal.entries,
        };
        node_fs_1.default.rmSync(journalPath, { force: true });
        releaseMutationLeaseSync(lease);
        return result;
    }
    catch (error) {
        if (isInterruption(error)) {
            throw error instanceof multi_file_mutation_js_1.MultiFileMutationError && error.cause instanceof MutationInterruptionError
                ? error.cause
                : error;
        }
        const receipt = error instanceof multi_file_mutation_js_1.MultiFileMutationError ? error.receipt : null;
        const outcome = receipt?.outcome ?? 'partial';
        journal = {
            ...journal,
            phase: outcome === 'partial' ? 'partial' : journal.phase,
            updatedAtMs: now(),
            receipt,
        };
        try {
            persistJournal(false);
        }
        catch {
            // Destination evidence remains authoritative when journal persistence itself fails.
        }
        if (outcome === 'rolled-back') {
            node_fs_1.default.rmSync(journalPath, { force: true });
        }
        try {
            releaseMutationLeaseSync(lease);
        }
        catch {
            // Preserve the mutation failure; owner-checked release is best effort here.
        }
        throw new RecoverableMutationError('Recoverable mutation did not fully apply.', { outcome, planHash: plan.planHash, journalPath, receipt, entries: journal.entries }, error instanceof multi_file_mutation_js_1.MultiFileMutationError ? error.cause : error);
    }
}
function recoverMutationJournalSync(scopePath, options = {}) {
    const lease = acquireMutationLeaseSync(scopePath, options);
    const journalPath = options.journalPath ?? mutationJournalPath(scopePath);
    const atomicWrite = options.atomicWrite ?? atomic_write_js_1.atomicWriteFileSync;
    try {
        if (!node_fs_1.default.existsSync(journalPath)) {
            return null;
        }
        const journal = readJournal(journalPath);
        if (journal.scopePath !== lease.record.scopePath) {
            throw new Error('Mutation journal scope does not match the acquired lease.');
        }
        let outcome = classifyJournal(journal);
        if (outcome === 'partial' && options.restoreEntry !== undefined) {
            for (const entry of [...journal.entries].reverse()) {
                const current = (0, multi_file_mutation_js_1.inspectMutationPathSync)(entry.path);
                if (current.contentHash === entry.beforeHash) {
                    continue;
                }
                if (current.contentHash !== entry.observedAfterHash) {
                    continue;
                }
                const restoration = options.restoreEntry(entry);
                if (restoration === undefined) {
                    continue;
                }
                const verified = (0, multi_file_mutation_js_1.inspectMutationPathSync)(entry.path);
                if (verified.contentHash !== entry.observedAfterHash) {
                    continue;
                }
                if (restoration === null) {
                    node_fs_1.default.rmSync(entry.resolvedPath, { force: true });
                }
                else {
                    atomicWrite(entry.resolvedPath, restoration);
                }
            }
            outcome = classifyJournal(journal);
        }
        const result = {
            outcome,
            planHash: journal.planHash,
            journalPath,
            receipt: journal.receipt,
            entries: journal.entries,
        };
        if (outcome !== 'partial') {
            node_fs_1.default.rmSync(journalPath, { force: true });
        }
        return result;
    }
    finally {
        releaseMutationLeaseSync(lease);
    }
}
function classifyJournal(journal) {
    const hashes = journal.entries.map((entry) => {
        try {
            return (0, multi_file_mutation_js_1.inspectMutationPathSync)(entry.path).contentHash;
        }
        catch {
            return undefined;
        }
    });
    if (hashes.every((hash, index) => hash === journal.entries[index].intendedHash)) {
        return 'fully-applied';
    }
    if (hashes.every((hash, index) => hash === journal.entries[index].beforeHash)) {
        return 'rolled-back';
    }
    return 'partial';
}
function removeStaleLease(lockPath, ownerToken) {
    const verified = readLeaseRecord(lockPath);
    if (verified?.ownerToken !== ownerToken) {
        return;
    }
    node_fs_1.default.rmSync(lockPath, { force: true });
}
function requireOwnedLease(lease) {
    const current = readLeaseRecord(lease.lockPath);
    if (current === null || current.ownerToken !== lease.record.ownerToken) {
        throw new MutationLockError('Mutation lease is no longer owned by this owner token.');
    }
    return current;
}
function isRecoverablyStale(record, timestamp, staleAfterMs, localHostname, processCheck) {
    if (timestamp - record.heartbeatAtMs < staleAfterMs || timestamp < record.leaseExpiresAtMs) {
        return false;
    }
    if (record.hostname !== localHostname) {
        return true;
    }
    const alive = (processCheck ?? isLocalProcessAlive)(record.pid);
    return alive !== true;
}
function isLocalProcessAlive(pid) {
    try {
        process.kill(pid, 0);
        return true;
    }
    catch (error) {
        if (isNodeError(error) && error.code === 'ESRCH') {
            return false;
        }
        if (isNodeError(error) && error.code === 'EPERM') {
            return true;
        }
        return null;
    }
}
function readLeaseRecord(lockPath) {
    try {
        const parsed = JSON.parse(node_fs_1.default.readFileSync(lockPath, 'utf8'));
        if (parsed.schemaVersion !== 1 ||
            typeof parsed.scopePath !== 'string' ||
            typeof parsed.ownerToken !== 'string' ||
            typeof parsed.hostname !== 'string' ||
            typeof parsed.pid !== 'number' ||
            typeof parsed.acquiredAtMs !== 'number' ||
            typeof parsed.heartbeatAtMs !== 'number' ||
            typeof parsed.leaseExpiresAtMs !== 'number') {
            return null;
        }
        return parsed;
    }
    catch (error) {
        if (isNodeError(error) && error.code === 'ENOENT') {
            return null;
        }
        throw error;
    }
}
function readJournal(journalPath) {
    const parsed = JSON.parse(node_fs_1.default.readFileSync(journalPath, 'utf8'));
    if (parsed.schemaVersion !== 1 ||
        typeof parsed.scopePath !== 'string' ||
        typeof parsed.ownerToken !== 'string' ||
        typeof parsed.planHash !== 'string' ||
        !Array.isArray(parsed.entries)) {
        throw new Error(`Invalid mutation journal '${journalPath}'.`);
    }
    return parsed;
}
function writeExclusiveJson(filePath, value) {
    const descriptor = node_fs_1.default.openSync(filePath, 'wx', 0o600);
    try {
        node_fs_1.default.writeFileSync(descriptor, serializeJson(value), 'utf8');
        node_fs_1.default.fsyncSync(descriptor);
    }
    finally {
        node_fs_1.default.closeSync(descriptor);
    }
}
function serializeJson(value) {
    return `${JSON.stringify(value, null, 2)}\n`;
}
function resolveScopePath(scopePath) {
    if (typeof scopePath !== 'string' || scopePath.trim() === '') {
        throw new TypeError('Mutation lock scope must be a non-empty path.');
    }
    return node_path_1.default.resolve(scopePath);
}
function positiveDuration(value, fallback, label) {
    const selected = value ?? fallback;
    if (!Number.isFinite(selected) || selected <= 0) {
        throw new TypeError(`Mutation ${label} must be a positive duration.`);
    }
    return selected;
}
function nonNegativeDuration(value, fallback, label) {
    const selected = value ?? fallback;
    if (!Number.isFinite(selected) || selected < 0) {
        throw new TypeError(`Mutation ${label} must be a non-negative duration.`);
    }
    return selected;
}
function sleepSync(milliseconds) {
    const signal = new Int32Array(new SharedArrayBuffer(4));
    Atomics.wait(signal, 0, 0, milliseconds);
}
function isInterruption(error) {
    return (error instanceof MutationInterruptionError ||
        (error instanceof multi_file_mutation_js_1.MultiFileMutationError && error.cause instanceof MutationInterruptionError));
}
function isNodeError(error) {
    return error instanceof Error && 'code' in error;
}
