"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiFileMutationError = void 0;
exports.hashMutationContent = hashMutationContent;
exports.inspectMutationPathSync = inspectMutationPathSync;
exports.createMultiFileMutationPlan = createMultiFileMutationPlan;
exports.executeMultiFileMutationPlanSync = executeMultiFileMutationPlanSync;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const node_crypto_1 = require("node:crypto");
const atomic_write_js_1 = require("./atomic-write.js");
class MultiFileMutationError extends Error {
    receipt;
    cause;
    constructor(message, receipt, cause) {
        super(message);
        this.name = "MultiFileMutationError";
        this.receipt = receipt;
        this.cause = cause;
    }
}
exports.MultiFileMutationError = MultiFileMutationError;
function hashMutationContent(contents, encoding = "utf8") {
    return sha256(toBuffer(contents, encoding));
}
function inspectMutationPathSync(filePath) {
    if (typeof filePath !== "string" || filePath.trim() === "") {
        throw new TypeError("Mutation path must be a non-empty string.");
    }
    const resolvedPath = node_path_1.default.resolve(filePath);
    const stats = node_fs_1.default.statSync(resolvedPath, { throwIfNoEntry: false });
    if (stats !== undefined && !stats.isFile()) {
        throw new Error(`Mutation destination '${filePath}' is not a regular file.`);
    }
    const exists = stats !== undefined;
    const contentHash = exists ? sha256(node_fs_1.default.readFileSync(resolvedPath)) : null;
    return {
        path: filePath,
        resolvedPath,
        exists,
        contentHash,
        preconditionToken: createPreconditionToken(resolvedPath, exists, contentHash)
    };
}
function createMultiFileMutationPlan(inputs) {
    if (!Array.isArray(inputs) || inputs.length === 0) {
        throw new TypeError("A multi-file mutation plan requires at least one entry.");
    }
    const destinations = new Set();
    const entries = inputs.map((input, index) => {
        if (input === null || typeof input !== "object") {
            throw new TypeError(`Mutation plan entry ${index} must be an object.`);
        }
        if (typeof input.path !== "string" || input.path.trim() === "") {
            throw new TypeError(`Mutation plan entry ${index} requires a non-empty path.`);
        }
        if (typeof input.preconditionToken !== "string" || input.preconditionToken === "") {
            throw new TypeError(`Mutation plan entry '${input.path}' requires a precondition token.`);
        }
        const resolvedPath = node_path_1.default.resolve(input.path);
        if (destinations.has(resolvedPath)) {
            throw new TypeError(`Mutation plan contains duplicate destination '${input.path}'.`);
        }
        destinations.add(resolvedPath);
        const contents = toBuffer(input.contents, input.encoding);
        return {
            path: input.path,
            resolvedPath,
            contents: Uint8Array.from(contents),
            intendedHash: sha256(contents),
            preconditionToken: input.preconditionToken,
            ...(input.mode === undefined ? {} : { mode: input.mode })
        };
    });
    return {
        planHash: hashPlan(entries),
        entries
    };
}
function executeMultiFileMutationPlanSync(plan, options = {}) {
    const inspectPath = options.inspectPath ?? inspectMutationPathSync;
    const atomicWrite = options.atomicWrite ?? atomic_write_js_1.atomicWriteFileSync;
    validatePlanShape(plan);
    const observed = [];
    const inspectionFailures = new Map();
    for (let index = 0; index < plan.entries.length; index += 1) {
        const entry = plan.entries[index];
        try {
            const evidence = inspectPath(entry.path);
            observed.push(evidence);
            if (evidence.resolvedPath !== entry.resolvedPath) {
                inspectionFailures.set(index, {
                    reason: "inspector returned mismatched path identity",
                    cause: new Error(`Mutation inspection path mismatch for '${entry.path}'.`)
                });
            }
            else if (evidence.preconditionToken !== entry.preconditionToken) {
                inspectionFailures.set(index, {
                    reason: "precondition token is stale",
                    cause: new Error(`Mutation precondition failed for '${entry.path}'.`)
                });
            }
        }
        catch (cause) {
            observed.push(unknownEvidence(entry));
            inspectionFailures.set(index, { reason: errorMessage(cause), cause });
        }
    }
    const planned = plan.entries.map((entry, index) => receiptEntry(entry, observed[index]));
    if (inspectionFailures.size > 0) {
        const failed = planned.map((entry, index) => ({
            ...entry,
            reason: inspectionFailures.get(index)?.reason ?? "plan not applied because prevalidation failed"
        }));
        const firstFailure = inspectionFailures.values().next().value;
        throw new MultiFileMutationError("Multi-file mutation prevalidation failed before any writes.", receipt(plan.planHash, "rolled-back", planned, [], [], failed), firstFailure.cause);
    }
    const applied = [];
    const unchanged = [];
    let failingIndex = -1;
    let failure;
    for (let index = 0; index < plan.entries.length; index += 1) {
        const entry = plan.entries[index];
        const before = observed[index];
        if (before.contentHash === entry.intendedHash) {
            unchanged.push({ ...planned[index], afterHash: before.contentHash });
            continue;
        }
        try {
            atomicWrite(entry.resolvedPath, entry.contents, entry.mode === undefined ? {} : { mode: entry.mode });
            const after = inspectPath(entry.path);
            if (after.contentHash !== entry.intendedHash) {
                throw new Error(`Mutation postcondition failed for '${entry.path}'.`);
            }
            applied.push({ ...planned[index], afterHash: after.contentHash });
        }
        catch (cause) {
            failingIndex = index;
            failure = cause;
            break;
        }
    }
    if (failure === undefined) {
        return receipt(plan.planHash, "fully-applied", planned, applied, unchanged, []);
    }
    const failedStates = plan.entries.slice(failingIndex).map((entry, offset) => {
        const index = failingIndex + offset;
        let afterHash = null;
        let afterKnown = false;
        try {
            afterHash = inspectPath(entry.path).contentHash;
            afterKnown = true;
        }
        catch {
            // The receipt keeps an unknown current state as null and preserves the primary failure.
        }
        return {
            entry: {
                ...planned[index],
                afterHash,
                reason: index === failingIndex ? errorMessage(failure) : "not attempted after earlier failure"
            },
            afterKnown
        };
    });
    const failed = failedStates.map((state) => state.entry);
    let outcome = "partial";
    if (applied.length === 0 && failedStates[0]?.afterKnown && failed[0]?.afterHash === failed[0]?.beforeHash) {
        outcome = "rolled-back";
    }
    const affected = [
        ...applied,
        ...failedStates
            .filter((state) => !state.afterKnown || state.entry.afterHash !== state.entry.beforeHash)
            .map((state) => state.entry)
    ];
    if (options.rollback !== undefined && affected.length > 0) {
        try {
            options.rollback({ plan, applied, affected, cause: failure });
            const restored = affected.every((entry) => {
                try {
                    return inspectPath(entry.path).contentHash === entry.beforeHash;
                }
                catch {
                    return false;
                }
            });
            if (restored) {
                outcome = "rolled-back";
            }
        }
        catch {
            outcome = "partial";
        }
    }
    const mutationReceipt = receipt(plan.planHash, outcome, planned, applied, unchanged, failed);
    throw new MultiFileMutationError("Multi-file mutation did not fully apply.", mutationReceipt, failure);
}
function validatePlanShape(plan) {
    if (plan === null || typeof plan !== "object" || !Array.isArray(plan.entries) || plan.entries.length === 0) {
        throw new TypeError("Invalid multi-file mutation plan.");
    }
    if (plan.planHash !== hashPlan(plan.entries)) {
        throw new TypeError("Multi-file mutation plan hash does not match its entries.");
    }
}
function receiptEntry(entry, before) {
    return {
        path: entry.path,
        resolvedPath: entry.resolvedPath,
        beforeHash: before.contentHash,
        intendedHash: entry.intendedHash,
        afterHash: null
    };
}
function receipt(planHash, outcome, planned, applied, unchanged, failed) {
    return { planHash, outcome, planned, applied, unchanged, failed };
}
function unknownEvidence(entry) {
    return {
        path: entry.path,
        resolvedPath: entry.resolvedPath,
        exists: false,
        contentHash: null,
        preconditionToken: ""
    };
}
function createPreconditionToken(resolvedPath, exists, contentHash) {
    return sha256(Buffer.from(JSON.stringify([resolvedPath, exists, contentHash]), "utf8"));
}
function hashPlan(entries) {
    const canonical = entries.map((entry) => [
        entry.resolvedPath,
        entry.intendedHash,
        entry.preconditionToken,
        entry.mode ?? null
    ]);
    return sha256(Buffer.from(JSON.stringify(canonical), "utf8"));
}
function toBuffer(contents, encoding = "utf8") {
    if (typeof contents === "string") {
        return Buffer.from(contents, encoding);
    }
    if (contents instanceof Uint8Array) {
        return Buffer.from(contents);
    }
    throw new TypeError("Mutation contents must be a string or Uint8Array.");
}
function sha256(contents) {
    return (0, node_crypto_1.createHash)("sha256").update(contents).digest("hex");
}
function errorMessage(error) {
    return error instanceof Error ? error.message : String(error);
}
