"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationalCleanupRejectedError = void 0;
exports.inspectOperationalStateSync = inspectOperationalStateSync;
exports.cleanupOperationalTransactionSync = cleanupOperationalTransactionSync;
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 repository_transaction_js_1 = require("./repository-transaction.js");
const canonical_write_target_js_1 = require("../paths/canonical-write-target.js");
const cli_constants_js_1 = require("../config/cli-constants.js");
class OperationalCleanupRejectedError extends Error {
    reason;
    constructor(reason, message) {
        super(message);
        this.name = 'OperationalCleanupRejectedError';
        this.reason = reason;
    }
}
exports.OperationalCleanupRejectedError = OperationalCleanupRejectedError;
function inspectOperationalStateSync(repositoryRoot, options = {}) {
    return inspectInternal(repositoryRoot, options).publicEvidence;
}
function cleanupOperationalTransactionSync(repositoryRoot, proof, options = {}) {
    validateTransactionId(proof.transactionId);
    if (typeof proof.evidenceFingerprint !== 'string' || proof.evidenceFingerprint === '') {
        throw new TypeError('Operational cleanup requires a non-empty evidence fingerprint.');
    }
    const inspection = inspectInternal(repositoryRoot, options);
    const transaction = inspection.publicEvidence.transactions.find((candidate) => candidate.transactionId === proof.transactionId);
    if (transaction === undefined) {
        throw new OperationalCleanupRejectedError('transaction-not-found', `Operational transaction '${proof.transactionId}' was not found; no state was removed.`);
    }
    const lock = inspection.publicEvidence.lock;
    if (lock?.classification === 'live') {
        throw new OperationalCleanupRejectedError('live-lock', 'Operational cleanup cannot run while a live repository write lock exists.');
    }
    if (lock !== null && lock.transactionId !== null && lock.transactionId !== proof.transactionId) {
        throw new OperationalCleanupRejectedError('mismatched-transaction', 'The repository write lock belongs to a different transaction; no state was removed.');
    }
    if (transaction.evidenceFingerprint !== proof.evidenceFingerprint) {
        throw new OperationalCleanupRejectedError('evidence-mismatch', 'Operational cleanup evidence changed after inspection; inspect again before cleanup.');
    }
    if (transaction.targets.some((target) => target.targetState === 'diverged')) {
        throw new OperationalCleanupRejectedError('ambiguous-target', 'Operational cleanup rejected ambiguous canonical target evidence.');
    }
    if (transaction.state !== 'restored' &&
        transaction.recoverableStagedContentPresent &&
        transaction.targets.some((target) => target.stagedPresent && !isTargetPostcondition(target))) {
        throw new OperationalCleanupRejectedError('only-recoverable-copy', 'Operational cleanup would remove the only verified copy of intended authored content.');
    }
    if (!transaction.cleanupEligible || proof.action !== cleanupActionFor(transaction)) {
        throw new OperationalCleanupRejectedError('ineligible-state', 'Operational transaction state does not satisfy the requested cleanup contract.');
    }
    const resolvedRoot = resolveRepositoryRoot(repositoryRoot);
    const transactionDirectory = (0, repository_transaction_js_1.repositoryTransactionDirectory)(resolvedRoot, proof.transactionId);
    const removedPaths = [];
    const removeStaleLock = lock?.transactionId === proof.transactionId && lock.classification === 'stale';
    const lockPath = (0, repository_transaction_js_1.repositoryWriteLockPath)(resolvedRoot);
    if (removeStaleLock && digestFile(lockPath) !== inspection.lockDigest) {
        throw new OperationalCleanupRejectedError('evidence-mismatch', 'Repository write-lock evidence changed before cleanup.');
    }
    node_fs_1.default.rmSync(transactionDirectory, { recursive: true, force: false });
    removedPaths.push(transaction.path);
    if (removeStaleLock) {
        node_fs_1.default.rmSync(lockPath);
        removedPaths.push('.day-shift/state/write.lock');
    }
    return { transactionId: proof.transactionId, action: proof.action, removedPaths };
}
function inspectInternal(repositoryRoot, options) {
    const resolvedRoot = resolveRepositoryRoot(repositoryRoot);
    const inspectedAtMs = (options.now ?? Date.now)();
    const lockPath = (0, repository_transaction_js_1.repositoryWriteLockPath)(resolvedRoot);
    const parsedLock = readJsonFile(lockPath, isLockRecord);
    const lockRecord = parsedLock.value;
    const lock = inspectLock(parsedLock, inspectedAtMs, options.isProcessAlive);
    const transactionsRoot = node_path_1.default.join(resolvedRoot, '.day-shift', 'state', 'transactions');
    const transactionIds = listTransactionIds(transactionsRoot);
    const transactions = transactionIds.map((transactionId) => inspectTransaction(resolvedRoot, transactionId, lock));
    const classification = classifyOverall(lock, transactions);
    const recovery = lock?.classification === 'live'
        ? [
            {
                action: 'wait',
                preconditions: ['The repository write-lock owner remains live or its liveness is not safely disproven.'],
                noWrite: true,
            },
        ]
        : transactions.flatMap((transaction) => transaction.recovery);
    return {
        publicEvidence: {
            schemaVersion: 1,
            classification,
            inspectedAtMs,
            lock,
            transactions,
            recovery,
        },
        lockRecord,
        lockDigest: parsedLock.digest,
    };
}
function inspectLock(parsed, now, isProcessAlive) {
    if (parsed.status === 'missing') {
        return null;
    }
    if (parsed.value === null) {
        return {
            path: '.day-shift/state/write.lock',
            transactionId: null,
            classification: 'invalid',
            acquiredAtMs: null,
            heartbeatAtMs: null,
            expiresAtMs: null,
            ownerLiveness: 'unknown',
            mode: parsed.mode,
            permissionsRestricted: isRestrictedMode(parsed.mode),
        };
    }
    const record = parsed.value;
    const liveness = (isProcessAlive ?? defaultProcessAlive)(record);
    const ownerLiveness = liveness === true ? 'alive' : liveness === false ? 'dead' : 'unknown';
    const classification = record.expiresAtMs > now || liveness === true ? 'live' : 'stale';
    return {
        path: '.day-shift/state/write.lock',
        transactionId: record.transactionId,
        classification,
        acquiredAtMs: record.acquiredAtMs,
        heartbeatAtMs: record.heartbeatAtMs,
        expiresAtMs: record.expiresAtMs,
        ownerLiveness,
        mode: parsed.mode,
        permissionsRestricted: isRestrictedMode(parsed.mode),
    };
}
function inspectTransaction(repositoryRoot, transactionId, lock) {
    const transactionDirectory = (0, repository_transaction_js_1.repositoryTransactionDirectory)(repositoryRoot, transactionId);
    const relativePath = `.day-shift/state/transactions/${transactionId}`;
    const manifestFile = readJsonFile(node_path_1.default.join(transactionDirectory, 'manifest.json'), isTransactionManifest);
    const statusFile = readJsonFile(node_path_1.default.join(transactionDirectory, 'status'), isTransactionStatus);
    const manifest = manifestFile.value?.transactionId === transactionId ? manifestFile.value : null;
    const status = statusFile.value?.transactionId === transactionId ? statusFile.value : null;
    const targets = manifest === null
        ? []
        : manifest.entries.map((entry) => inspectTarget(repositoryRoot, transactionDirectory, entry));
    const postconditionsVerified = targets.length > 0 && targets.every(isTargetPostcondition);
    const restorationVerified = targets.length > 0 && targets.every(isTargetRestoration);
    const committed = status?.state === 'committed' && postconditionsVerified;
    const recoverableStagedContentPresent = targets.some((target) => target.stagedPresent);
    const classification = committed ? 'committed' : 'incomplete';
    const cleanupEligible = committed ||
        (status !== null &&
            ((manifest?.transactionKind === 'generic' &&
                ['applying', 'incomplete'].includes(status.state) &&
                postconditionsVerified) ||
                (status.state === 'restored' && restorationVerified)));
    const directoryMode = statMode(transactionDirectory);
    const base = {
        transactionId,
        path: relativePath,
        transactionKind: manifest?.transactionKind ?? 'generic',
        durableRecordPath: manifest?.durableRecordPath ?? null,
        classification,
        manifestStatus: manifest === null ? (manifestFile.status === 'missing' ? 'missing' : 'invalid') : 'valid',
        statusStatus: status === null ? (statusFile.status === 'missing' ? 'missing' : 'invalid') : 'valid',
        state: status?.state ?? 'unknown',
        createdAtMs: manifest?.createdAtMs ?? null,
        updatedAtMs: status?.updatedAtMs ?? null,
        replacedPaths: status?.replacedPaths ?? [],
        failurePresent: status?.failure !== null && status?.failure !== undefined,
        directoryMode,
        directoryPermissionsRestricted: isRestrictedMode(directoryMode),
        manifestMode: manifestFile.mode,
        manifestPermissionsRestricted: isRestrictedMode(manifestFile.mode),
        statusMode: statusFile.mode,
        statusPermissionsRestricted: isRestrictedMode(statusFile.mode),
        targets,
        postconditionsVerified,
        restorationVerified,
        recoverableStagedContentPresent,
        cleanupEligible,
    };
    const recovery = buildRecovery(base, lock);
    const evidenceFingerprint = hashJson({
        transaction: base,
        manifestDigest: manifestFile.digest,
        statusDigest: statusFile.digest,
    });
    return { ...base, recovery, evidenceFingerprint };
}
function inspectTarget(repositoryRoot, transactionDirectory, entry) {
    const operation = entry.operation ?? 'write';
    let currentRevision = null;
    let targetState = 'diverged';
    try {
        const target = (0, canonical_write_target_js_1.resolveCanonicalWriteTargetSync)(repositoryRoot, entry.path);
        currentRevision = digestFile(target.runtimePath);
        if (operation === 'retire') {
            targetState =
                currentRevision === null ? 'retired' : currentRevision === entry.beforeRevision ? 'before' : 'diverged';
        }
        else {
            targetState =
                currentRevision === entry.intendedRevision
                    ? 'intended'
                    : currentRevision === entry.beforeRevision
                        ? 'before'
                        : currentRevision === null
                            ? 'missing'
                            : 'diverged';
        }
    }
    catch {
        targetState = 'diverged';
    }
    const stagedRuntimePath = resolveStagedPath(transactionDirectory, entry.stagedPath);
    const stagedRevision = stagedRuntimePath === null ? null : digestFile(stagedRuntimePath);
    const stagedMode = stagedRuntimePath === null ? null : statMode(stagedRuntimePath);
    return {
        operation,
        path: entry.path,
        beforeRevision: entry.beforeRevision,
        intendedRevision: entry.intendedRevision,
        observedAfterRevision: entry.observedAfterRevision,
        currentRevision,
        targetState,
        stagedPath: entry.stagedPath,
        stagedPresent: stagedRevision !== null,
        stagedRevision,
        stagedRevisionMatches: stagedRevision === (operation === 'retire' ? entry.beforeRevision : entry.intendedRevision),
        stagedMode,
        stagedPermissionsRestricted: isRestrictedMode(stagedMode),
    };
}
function buildRecovery(transaction, lock) {
    if (lock?.classification === 'live') {
        return [
            {
                action: 'wait',
                preconditions: [
                    'The matching repository write-lock owner remains live or its liveness is not safely disproven.',
                ],
                noWrite: true,
            },
        ];
    }
    if (transaction.classification === 'committed') {
        return [
            {
                action: 'committed-cleanup',
                preconditions: [
                    'The committed status and every intended target revision still match this evidence fingerprint.',
                    'No live repository write lock exists.',
                ],
                noWrite: true,
            },
        ];
    }
    const targets = transaction.targets;
    const steps = [];
    if (transaction.transactionKind !== 'generic' && transaction.postconditionsVerified) {
        steps.push({
            action: 'roll-forward',
            preconditions: [
                'Reinspect and match the exact transaction evidence fingerprint.',
                'Finalize the verified canonical outcome and durable recovery receipt under the repository write lock.',
            ],
            noWrite: true,
        });
    }
    if (transaction.transactionKind !== 'generic' && transaction.restorationVerified) {
        steps.push({
            action: 'restore',
            preconditions: [
                'Reinspect and match the exact transaction evidence fingerprint.',
                'Finalize the verified restoration and durable recovery receipt under the repository write lock.',
            ],
            noWrite: true,
        });
    }
    if (targets.some((target) => target.operation === 'write'
        ? target.stagedPresent && target.targetState !== 'intended'
        : target.targetState === 'before') &&
        !steps.some((step) => step.action === 'roll-forward')) {
        steps.push({
            action: targets.some((target) => target.targetState === 'intended') ? 'resume' : 'roll-forward',
            preconditions: [
                'Reinspect and match the transaction evidence fingerprint.',
                'Verify every staged revision before writing canonical targets.',
                'Acquire the repository write lock before recovery writes.',
            ],
            noWrite: true,
        });
    }
    if (targets.some((target) => target.operation === 'write'
        ? target.targetState === 'before'
        : target.targetState === 'retired' && target.stagedPresent) &&
        !steps.some((step) => step.action === 'restore')) {
        steps.push({
            action: 'restore',
            preconditions: [
                'Select a verified source or repository-history copy for every target that is not at its before revision.',
                'Acquire the repository write lock and revalidate canonical target revisions before restoration.',
            ],
            noWrite: true,
        });
    }
    if (transaction.cleanupEligible && (transaction.postconditionsVerified || transaction.restorationVerified)) {
        steps.push({
            action: 'verified-incomplete-cleanup',
            preconditions: [
                'Every intended revision exists at its canonical target.',
                'The evidence fingerprint still matches and no live repository write lock exists.',
            ],
            noWrite: true,
        });
    }
    return steps;
}
function cleanupActionFor(transaction) {
    return transaction.classification === 'committed' ? 'committed-cleanup' : 'verified-incomplete-cleanup';
}
function classifyOverall(lock, transactions) {
    if (lock?.classification === 'live')
        return 'live';
    if (lock !== null)
        return 'stale';
    if (transactions.some((transaction) => transaction.classification === 'incomplete')) {
        return 'incomplete';
    }
    if (transactions.some((transaction) => transaction.classification === 'committed')) {
        return 'committed';
    }
    return 'absent';
}
function listTransactionIds(transactionsRoot) {
    try {
        return node_fs_1.default
            .readdirSync(transactionsRoot, { withFileTypes: true })
            .filter((entry) => entry.isDirectory() && isTransactionId(entry.name))
            .map((entry) => entry.name)
            .sort(compareText);
    }
    catch (error) {
        if (isNodeError(error) && error.code === 'ENOENT')
            return [];
        throw error;
    }
}
function readJsonFile(filePath, validate) {
    const mode = statMode(filePath);
    if (mode === null) {
        return { status: 'missing', value: null, digest: null, mode: null };
    }
    let contents;
    try {
        contents = node_fs_1.default.readFileSync(filePath);
    }
    catch {
        return { status: 'invalid', value: null, digest: null, mode };
    }
    const digest = sha256(contents);
    try {
        const parsed = JSON.parse(contents.toString('utf8'));
        return validate(parsed)
            ? { status: 'valid', value: parsed, digest, mode }
            : { status: 'invalid', value: null, digest, mode };
    }
    catch {
        return { status: 'invalid', value: null, digest, mode };
    }
}
function isLockRecord(value) {
    if (!isRecord(value))
        return false;
    return (value.schemaVersion === 1 &&
        isTransactionId(value.transactionId) &&
        typeof value.ownerToken === 'string' &&
        value.ownerToken !== '' &&
        typeof value.hostname === 'string' &&
        Number.isInteger(value.pid) &&
        Number(value.pid) > 0 &&
        isFiniteNumber(value.acquiredAtMs) &&
        isFiniteNumber(value.heartbeatAtMs) &&
        isFiniteNumber(value.expiresAtMs));
}
function isTransactionManifest(value) {
    if (!isRecord(value) ||
        value.schemaVersion !== 1 ||
        !isTransactionId(value.transactionId) ||
        !isFiniteNumber(value.createdAtMs) ||
        !Array.isArray(value.entries) ||
        !value.entries.every(isManifestEntry) ||
        !hasDeterministicOperationOrder(value.entries)) {
        return false;
    }
    const transactionKind = value.transactionKind ?? 'generic';
    if (transactionKind !== 'generic' && transactionKind !== 'promotion' && transactionKind !== 'migration')
        return false;
    const durableRecordPath = value.durableRecordPath ?? null;
    return ((durableRecordPath === null || isRepositoryRelativePath(durableRecordPath)) &&
        (transactionKind === 'generic' || durableRecordPath !== null));
}
function isManifestEntry(value) {
    if (!isRecord(value))
        return false;
    const operation = value.operation ?? 'write';
    return ((operation === 'write' || operation === 'retire') &&
        isRepositoryRelativePath(value.path) &&
        typeof value.stagedPath === 'string' &&
        /^(?:staged|retired)\/[A-Za-z0-9._-]+$/.test(value.stagedPath) &&
        (value.beforeRevision === null || isSha256(value.beforeRevision)) &&
        (operation === 'write' ? isSha256(value.intendedRevision) : value.intendedRevision === null) &&
        (value.observedAfterRevision === null || isSha256(value.observedAfterRevision)) &&
        Number.isInteger(value.mode) &&
        Number(value.mode) >= 0 &&
        Number(value.mode) <= 0o777);
}
function isTransactionStatus(value) {
    return (isRecord(value) &&
        value.schemaVersion === 1 &&
        isTransactionId(value.transactionId) &&
        typeof value.state === 'string' &&
        cli_constants_js_1.TRANSACTION_STATES.has(value.state) &&
        isFiniteNumber(value.updatedAtMs) &&
        Array.isArray(value.replacedPaths) &&
        value.replacedPaths.every(isRepositoryRelativePath) &&
        isSortedUnique(value.replacedPaths) &&
        (value.failure === null || typeof value.failure === 'string'));
}
function resolveStagedPath(transactionDirectory, stagedPath) {
    if (!/^(?:staged|retired)\/[A-Za-z0-9._-]+$/.test(stagedPath))
        return null;
    const resolved = node_path_1.default.resolve(transactionDirectory, stagedPath);
    const relative = node_path_1.default.relative(transactionDirectory, resolved);
    return relative.startsWith(`staged${node_path_1.default.sep}`) || relative.startsWith(`retired${node_path_1.default.sep}`) ? resolved : null;
}
function isTargetPostcondition(target) {
    return target.operation === 'retire' ? target.targetState === 'retired' : target.targetState === 'intended';
}
function isTargetRestoration(target) {
    return target.operation === 'retire' ? target.targetState === 'before' : target.targetState === 'before';
}
function hasDeterministicOperationOrder(entries) {
    const paths = entries.map((entry) => entry.path);
    if (new Set(paths).size !== paths.length)
        return false;
    const writes = entries.filter((entry) => (entry.operation ?? 'write') === 'write');
    const retirements = entries.filter((entry) => entry.operation === 'retire');
    if (entries.some((entry, index) => index < writes.length ? (entry.operation ?? 'write') !== 'write' : entry.operation !== 'retire')) {
        return false;
    }
    return isSortedUnique(writes.map((entry) => entry.path)) && isSortedUnique(retirements.map((entry) => entry.path));
}
function resolveRepositoryRoot(repositoryRoot) {
    if (typeof repositoryRoot !== 'string' || repositoryRoot.trim() === '') {
        throw new TypeError('A non-empty repository root is required.');
    }
    const resolved = node_fs_1.default.realpathSync(node_path_1.default.resolve(repositoryRoot));
    if (!node_fs_1.default.statSync(node_path_1.default.join(resolved, '.day-shift', 'config.toml'), { throwIfNoEntry: false })?.isFile()) {
        throw new Error(`Repository root '${repositoryRoot}' has no .day-shift/config.toml marker.`);
    }
    return resolved;
}
function defaultProcessAlive(record) {
    if (record.hostname !== node_os_1.default.hostname())
        return null;
    try {
        process.kill(record.pid, 0);
        return true;
    }
    catch (error) {
        return isNodeError(error) && error.code === 'ESRCH' ? false : null;
    }
}
function statMode(filePath) {
    try {
        return node_fs_1.default.statSync(filePath).mode & 0o777;
    }
    catch (error) {
        if (isNodeError(error) && error.code === 'ENOENT')
            return null;
        throw error;
    }
}
function isRestrictedMode(mode) {
    return mode !== null && (mode & 0o077) === 0;
}
function digestFile(filePath) {
    try {
        return sha256(node_fs_1.default.readFileSync(filePath));
    }
    catch (error) {
        if (isNodeError(error) && (error.code === 'ENOENT' || error.code === 'EISDIR'))
            return null;
        throw error;
    }
}
function hashJson(value) {
    return sha256(Buffer.from(JSON.stringify(value), 'utf8'));
}
function sha256(contents) {
    return (0, node_crypto_1.createHash)('sha256').update(contents).digest('hex');
}
function isRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function isFiniteNumber(value) {
    return typeof value === 'number' && Number.isFinite(value);
}
function isSha256(value) {
    return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
}
function isRepositoryRelativePath(value) {
    if (typeof value !== 'string' || value === '' || value.includes('\\'))
        return false;
    const normalized = node_path_1.default.posix.normalize(value);
    return (normalized === value &&
        normalized !== '.' &&
        normalized !== '..' &&
        !normalized.startsWith('../') &&
        !node_path_1.default.posix.isAbsolute(normalized));
}
function isSortedUnique(values) {
    return values.every((value, index) => index === 0 || compareText(values[index - 1], value) < 0);
}
function compareText(left, right) {
    return left < right ? -1 : left > right ? 1 : 0;
}
function isTransactionId(value) {
    return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
}
function validateTransactionId(value) {
    if (!isTransactionId(value)) {
        throw new TypeError('Transaction identity must be a safe, non-empty repository-local name.');
    }
}
function isNodeError(error) {
    return error instanceof Error && 'code' in error;
}
