"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OperationalRecoveryRejectedError = void 0;
exports.recoverOperationalTransactionSync = recoverOperationalTransactionSync;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const atomic_write_1 = require("./atomic-write");
const operational_state_1 = require("./operational-state");
const repository_transaction_1 = require("./repository-transaction");
class OperationalRecoveryRejectedError extends Error {
    reason;
    constructor(reason, message) {
        super(message);
        this.name = 'OperationalRecoveryRejectedError';
        this.reason = reason;
    }
}
exports.OperationalRecoveryRejectedError = OperationalRecoveryRejectedError;
function recoverOperationalTransactionSync(repositoryRoot, proof, options = {}) {
    const resolvedRoot = resolveRepositoryRoot(repositoryRoot);
    const actor = sanitizedAuditText(proof.actor, 'Recovery actor', 128);
    const reason = sanitizedAuditText(proof.reason, 'Recovery reason', 512);
    const recoveredAt = normalizeTimestamp(proof.recoveredAt ?? new Date().toISOString());
    const inspectionOptions = {
        now: options.now,
        isProcessAlive: options.isProcessAlive,
    };
    const initial = selectTransaction(resolvedRoot, proof.transactionId, proof.evidenceFingerprint, inspectionOptions);
    const transactionDirectory = (0, repository_transaction_1.repositoryTransactionDirectory)(resolvedRoot, proof.transactionId);
    if (proof.expectedTransactionKind !== undefined && initial.transactionKind !== proof.expectedTransactionKind) {
        throw new OperationalRecoveryRejectedError('not-promotion', `Recovery expected a '${proof.expectedTransactionKind}' transaction, but inspected '${initial.transactionKind ?? 'unclassified'}'.`);
    }
    const manifest = readManifest(transactionDirectory, proof.transactionId, initial.transactionKind);
    const recoveryRecordPath = `.day-shift/state/recovery-records/${proof.transactionId}-${proof.action}.json`;
    const recoveryRecordAbsolute = node_path_1.default.join(resolvedRoot, ...recoveryRecordPath.split('/'));
    const existingReceipt = readMatchingRecoveryReceipt(recoveryRecordAbsolute, proof, initial, recoveryRecordPath);
    validateRecoverability(initial, proof.action, false, existingReceipt !== null);
    let lock;
    try {
        lock = (0, repository_transaction_1.acquireRepositoryWriteLockSync)(resolvedRoot, proof.transactionId, {
            ownerToken: options.ownerToken,
            hostname: options.hostname,
            pid: options.pid,
            leaseMs: options.leaseMs,
            now: options.now,
            isProcessAlive: options.isProcessAlive,
        });
    }
    catch {
        throw new OperationalRecoveryRejectedError('live-lock', 'The repository write lock could not be acquired for exact-evidence recovery.');
    }
    const changedPaths = [];
    const cleanupErrors = [];
    const removedOperationalPaths = [];
    let receiptPath = existingReceipt === null ? null : recoveryRecordPath;
    let failure = null;
    try {
        const locked = selectTransaction(resolvedRoot, proof.transactionId, proof.evidenceFingerprint, inspectionOptions);
        validateRecoverability(locked, proof.action, true, existingReceipt !== null);
        if (existingReceipt === null) {
            for (let index = 0; index < locked.targets.length; index += 1) {
                const target = locked.targets[index];
                invokeHook(options, 'before-operation', proof, index, target.path);
                const changed = proof.action === 'roll-forward'
                    ? rollForwardTarget(resolvedRoot, transactionDirectory, target)
                    : restoreTarget(resolvedRoot, transactionDirectory, target);
                if (changed)
                    changedPaths.push(target.path);
                invokeHook(options, 'after-operation', proof, index, target.path);
                writeStatus(transactionDirectory, {
                    schemaVersion: 1,
                    transactionId: proof.transactionId,
                    state: 'applying',
                    updatedAtMs: (options.now ?? Date.now)(),
                    replacedPaths: [...new Set(changedPaths)].sort(),
                    failure: null,
                });
            }
            const verified = (0, operational_state_1.inspectOperationalStateSync)(resolvedRoot, inspectionOptions).transactions.find((candidate) => candidate.transactionId === proof.transactionId);
            const verifiedPostcondition = proof.action === 'roll-forward' ? verified?.postconditionsVerified : verified?.restorationVerified;
            if (verifiedPostcondition !== true) {
                throw new Error('recovery-postcondition');
            }
            writeStatus(transactionDirectory, {
                schemaVersion: 1,
                transactionId: proof.transactionId,
                state: proof.action === 'roll-forward' ? 'committed' : 'restored',
                updatedAtMs: (options.now ?? Date.now)(),
                replacedPaths: manifest.entries.map((entry) => entry.path).sort(),
                failure: null,
            });
            invokeHook(options, 'before-receipt', proof, null, null);
            node_fs_1.default.mkdirSync(node_path_1.default.dirname(recoveryRecordAbsolute), { recursive: true, mode: 0o700 });
            node_fs_1.default.chmodSync(node_path_1.default.dirname(recoveryRecordAbsolute), 0o700);
            (0, atomic_write_1.atomicWriteFileSync)(recoveryRecordAbsolute, renderRecoveryReceipt({
                proof,
                actor,
                reason,
                recoveredAt,
                transaction: verified,
                recoveryRecordPath,
            }), { mode: 0o600 });
            receiptPath = recoveryRecordPath;
        }
    }
    catch {
        failure = 'Repository transaction recovery was interrupted; inspect the retained evidence again.';
        try {
            writeStatus(transactionDirectory, {
                schemaVersion: 1,
                transactionId: proof.transactionId,
                state: 'incomplete',
                updatedAtMs: (options.now ?? Date.now)(),
                replacedPaths: [...new Set(changedPaths)].sort(),
                failure: 'sanitized-recovery-interruption',
            });
        }
        catch {
            failure = 'Repository recovery and sanitized status persistence were interrupted.';
        }
    }
    if (failure === null) {
        try {
            invokeHook(options, 'before-cleanup', proof, null, null);
            node_fs_1.default.rmSync(node_path_1.default.join(transactionDirectory, 'staged'), { recursive: true, force: true });
            node_fs_1.default.rmSync(node_path_1.default.join(transactionDirectory, 'retired'), { recursive: true, force: true });
        }
        catch {
            cleanupErrors.push('Recovered staging cleanup did not complete.');
        }
    }
    try {
        (0, repository_transaction_1.releaseRepositoryWriteLockSync)(lock);
    }
    catch {
        cleanupErrors.push('Repository write-lock release did not complete.');
    }
    if (failure === null && cleanupErrors.length === 0) {
        try {
            node_fs_1.default.rmSync(transactionDirectory, { recursive: true, force: true });
            removedOperationalPaths.push(`.day-shift/state/transactions/${proof.transactionId}`);
        }
        catch {
            cleanupErrors.push('Recovered transaction evidence cleanup did not complete.');
        }
    }
    const outcome = failure === null && cleanupErrors.length === 0 ? 'recovered' : 'incomplete';
    return {
        transactionId: proof.transactionId,
        action: proof.action,
        outcome,
        changedPaths: [...new Set(changedPaths)].sort(),
        recoveryRecordPath: receiptPath,
        removedOperationalPaths,
        cleanupErrors,
        cleanupEvidenceSource: 'operational-recovery-result-and-read-only-operational-inspection',
        recoveryPath: outcome === 'incomplete' ? `.day-shift/state/transactions/${proof.transactionId}` : null,
        failure,
        actor,
        reason,
        recoveredAt,
    };
}
function selectTransaction(repositoryRoot, transactionId, evidenceFingerprint, options) {
    if (!/^[A-Za-z0-9._-]{3,128}$/.test(transactionId)) {
        throw new OperationalRecoveryRejectedError('transaction-not-found', 'Recovery requires a valid transaction identity.');
    }
    if (!/^[a-f0-9]{64}$/.test(evidenceFingerprint)) {
        throw new OperationalRecoveryRejectedError('evidence-mismatch', 'Recovery requires the exact current inspection fingerprint.');
    }
    const inspection = (0, operational_state_1.inspectOperationalStateSync)(repositoryRoot, options);
    const transaction = inspection.transactions.find((candidate) => candidate.transactionId === transactionId);
    if (transaction === undefined) {
        throw new OperationalRecoveryRejectedError('transaction-not-found', `Operational transaction '${transactionId}' was not found.`);
    }
    if (transaction.evidenceFingerprint !== evidenceFingerprint) {
        throw new OperationalRecoveryRejectedError('evidence-mismatch', 'Operational transaction evidence changed after inspection; inspect again before recovery.');
    }
    if (inspection.lock?.classification === 'live' && inspection.lock.transactionId !== transactionId) {
        throw new OperationalRecoveryRejectedError('live-lock', 'A different live repository transaction prevents recovery.');
    }
    return transaction;
}
function validateRecoverability(transaction, action, matchingRecoveryLock = false, matchingReceipt = false) {
    if (!['promotion', 'migration'].includes(transaction.transactionKind ?? '') ||
        transaction.durableRecordPath === null) {
        throw new OperationalRecoveryRejectedError('not-promotion', 'Callable recovery requires a classified promotion or migration transaction with a durable record.');
    }
    if (!matchingRecoveryLock &&
        !matchingReceipt &&
        transaction.recovery.every((step) => step.action !== action && !(action === 'roll-forward' && step.action === 'resume')) &&
        !(action === 'roll-forward' && transaction.postconditionsVerified) &&
        !(action === 'restore' && transaction.restorationVerified)) {
        throw new OperationalRecoveryRejectedError('action-unavailable', `Recovery action '${action}' is not safe for the inspected transaction state.`);
    }
    if (transaction.targets.some((target) => target.targetState === 'diverged')) {
        throw new OperationalRecoveryRejectedError('diverged-target', 'Recovery rejected a diverged canonical target.');
    }
    if (action === 'restore' &&
        transaction.targets.some((target) => target.operation === 'write' && target.beforeRevision !== null)) {
        throw new OperationalRecoveryRejectedError('unsafe-restore', 'Restore rejected a destination overwrite whose before bytes were not retained.');
    }
}
function readMatchingRecoveryReceipt(recoveryRecordAbsolute, proof, transaction, recoveryRecordPath) {
    if (!node_fs_1.default.existsSync(recoveryRecordAbsolute))
        return null;
    let parsed;
    try {
        parsed = JSON.parse(node_fs_1.default.readFileSync(recoveryRecordAbsolute, 'utf8'));
    }
    catch {
        parsed = null;
    }
    const receipt = parsed;
    const expectedOutcome = proof.action === 'roll-forward' ? 'rolled-forward' : 'restored';
    const expectedPaths = transaction.targets.map((target) => target.path).sort();
    const canonicalOutcomeVerified = proof.action === 'roll-forward' ? transaction.postconditionsVerified : transaction.restorationVerified;
    const terminalStateVerified = proof.action === 'roll-forward' ? transaction.state === 'committed' : transaction.state === 'restored';
    const expectedKind = transaction.transactionKind;
    const expectedRecordType = expectedKind === 'promotion' ? 'promotion-recovery' : 'workspace-migration-recovery';
    if (receipt === null ||
        receipt.schemaVersion !== 1 ||
        receipt.recordType !== expectedRecordType ||
        receipt.classification !== 'canonical' ||
        receipt.transactionId !== proof.transactionId ||
        (expectedKind === 'promotion'
            ? receipt.promotionRecordPath !== transaction.durableRecordPath ||
                (receipt.durableRecordPath !== undefined && receipt.durableRecordPath !== transaction.durableRecordPath)
            : receipt.durableRecordPath !== transaction.durableRecordPath) ||
        (receipt.transactionKind !== undefined && receipt.transactionKind !== expectedKind) ||
        receipt.recoveryRecordPath !== recoveryRecordPath ||
        receipt.action !== proof.action ||
        receipt.canonicalOutcome !== expectedOutcome ||
        receipt.operationalCleanup !== 'pending' ||
        receipt.cleanupEvidenceSource !== 'operational-recovery-result-and-read-only-operational-inspection' ||
        typeof receipt.evidenceFingerprint !== 'string' ||
        !/^[a-f0-9]{64}$/.test(receipt.evidenceFingerprint) ||
        !Array.isArray(receipt.affectedPaths) ||
        JSON.stringify(receipt.affectedPaths) !== JSON.stringify(expectedPaths) ||
        !canonicalOutcomeVerified ||
        !terminalStateVerified) {
        throw new OperationalRecoveryRejectedError('recovery-record-conflict', 'The durable recovery receipt does not match current transaction identity and target evidence.');
    }
    return receipt;
}
function rollForwardTarget(repositoryRoot, transactionDirectory, target) {
    const canonical = node_path_1.default.join(repositoryRoot, ...target.path.split('/'));
    const staged = node_path_1.default.join(transactionDirectory, ...target.stagedPath.split('/'));
    if (target.operation === 'write') {
        if (target.targetState === 'intended')
            return false;
        if (target.beforeRevision !== null ||
            target.targetState !== 'before' ||
            !target.stagedPresent ||
            !target.stagedRevisionMatches) {
            throw new OperationalRecoveryRejectedError('missing-staged-evidence', `Roll-forward lacks safe staged evidence for '${target.path}'.`);
        }
        node_fs_1.default.mkdirSync(node_path_1.default.dirname(canonical), { recursive: true });
        node_fs_1.default.renameSync(staged, canonical);
        node_fs_1.default.chmodSync(canonical, target.stagedMode ?? 0o600);
        return true;
    }
    if (target.targetState === 'retired')
        return false;
    if (target.targetState !== 'before' || target.stagedPresent) {
        throw new OperationalRecoveryRejectedError('missing-staged-evidence', `Roll-forward cannot safely retire '${target.path}'.`);
    }
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(staged), { recursive: true, mode: 0o700 });
    node_fs_1.default.renameSync(canonical, staged);
    node_fs_1.default.chmodSync(staged, 0o600);
    return true;
}
function restoreTarget(repositoryRoot, transactionDirectory, target) {
    const canonical = node_path_1.default.join(repositoryRoot, ...target.path.split('/'));
    const staged = node_path_1.default.join(transactionDirectory, ...target.stagedPath.split('/'));
    if (target.operation === 'write') {
        if (target.targetState === 'before')
            return false;
        if (target.targetState !== 'intended' || target.beforeRevision !== null) {
            throw new OperationalRecoveryRejectedError('unsafe-restore', `Restore cannot safely remove '${target.path}'.`);
        }
        node_fs_1.default.rmSync(canonical);
        return true;
    }
    if (target.targetState === 'before')
        return false;
    if (target.targetState !== 'retired' || !target.stagedPresent || !target.stagedRevisionMatches) {
        throw new OperationalRecoveryRejectedError('missing-staged-evidence', `Restore lacks the retired source bytes for '${target.path}'.`);
    }
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(canonical), { recursive: true });
    node_fs_1.default.renameSync(staged, canonical);
    node_fs_1.default.chmodSync(canonical, target.stagedMode ?? 0o600);
    return true;
}
function readManifest(transactionDirectory, transactionId, expectedKind) {
    const parsed = JSON.parse(node_fs_1.default.readFileSync(node_path_1.default.join(transactionDirectory, 'manifest.json'), 'utf8'));
    if (parsed.transactionId !== transactionId ||
        parsed.transactionKind !== expectedKind ||
        !['promotion', 'migration'].includes(parsed.transactionKind ?? '') ||
        parsed.durableRecordPath === null) {
        throw new OperationalRecoveryRejectedError('not-promotion', 'Recovery manifest does not identify the inspected promotion or migration transaction.');
    }
    return parsed;
}
function writeStatus(transactionDirectory, status) {
    (0, atomic_write_1.atomicWriteFileSync)(node_path_1.default.join(transactionDirectory, 'status'), `${JSON.stringify(status, null, 2)}\n`, {
        mode: 0o600,
    });
}
function renderRecoveryReceipt(options) {
    const transactionKind = options.transaction.transactionKind;
    return `${JSON.stringify({
        schemaVersion: 1,
        recordType: transactionKind === 'promotion' ? 'promotion-recovery' : 'workspace-migration-recovery',
        classification: 'canonical',
        transactionId: options.proof.transactionId,
        transactionKind,
        durableRecordPath: options.transaction.durableRecordPath,
        ...(transactionKind === 'promotion' ? { promotionRecordPath: options.transaction.durableRecordPath } : {}),
        recoveryRecordPath: options.recoveryRecordPath,
        action: options.proof.action,
        canonicalOutcome: options.proof.action === 'roll-forward' ? 'rolled-forward' : 'restored',
        operationalCleanup: 'pending',
        cleanupEvidenceSource: 'operational-recovery-result-and-read-only-operational-inspection',
        evidenceFingerprint: options.proof.evidenceFingerprint,
        affectedPaths: options.transaction.targets.map((target) => target.path).sort(),
        actor: options.actor,
        reason: options.reason,
        recoveredAt: options.recoveredAt,
    }, null, 2)}\n`;
}
function invokeHook(options, phase, proof, entryIndex, targetPath) {
    options.hook?.({
        phase,
        action: proof.action,
        transactionId: proof.transactionId,
        entryIndex,
        path: targetPath,
    });
}
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 sanitizedAuditText(value, label, maximumLength) {
    if (typeof value !== 'string' || value.trim() === '') {
        throw new TypeError(`${label} must be non-empty.`);
    }
    const normalized = value.trim();
    if (normalized.length > maximumLength ||
        hasControlCharacters(normalized) ||
        /(?:^|\s)(?:\/[^\s]*|[A-Za-z]:[\\/][^\s]*)/.test(normalized)) {
        throw new TypeError(`${label} must be bounded single-line audit text without absolute filesystem paths.`);
    }
    return normalized;
}
function hasControlCharacters(value) {
    for (let index = 0; index < value.length; index += 1) {
        const codePoint = value.charCodeAt(index);
        if ((codePoint >= 0 && codePoint <= 31) || codePoint === 127) {
            return true;
        }
    }
    return false;
}
function normalizeTimestamp(value) {
    if (typeof value !== 'string' || value.trim() === '') {
        throw new TypeError('Recovery timestamp must be a non-empty ISO-8601 value.');
    }
    const parsed = Date.parse(value);
    if (!Number.isFinite(parsed) || !/(?:Z|[+-]\d{2}:\d{2})$/.test(value)) {
        throw new TypeError('Recovery timestamp must be ISO-8601 with an explicit timezone.');
    }
    return new Date(parsed).toISOString();
}
