"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_LIFECYCLE_RUN_PACK_MANIFEST_BYTES = exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME = exports.LIFECYCLE_RUN_PACK_SCHEMA_VERSION = void 0;
exports.previewLifecycleRunPackSync = previewLifecycleRunPackSync;
exports.applyLifecycleRunPackSync = applyLifecycleRunPackSync;
exports.createAndApplyLifecycleRunPackSync = createAndApplyLifecycleRunPackSync;
exports.inspectLifecycleRunPackSync = inspectLifecycleRunPackSync;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const errors_1 = require("../errors");
const mutation_preconditions_1 = require("../filesystem/mutation-preconditions");
const repository_transaction_1 = require("../filesystem/repository-transaction");
const paths_1 = require("../paths");
const canonical_write_target_1 = require("../paths/canonical-write-target");
const index_1 = require("./index");
const evidence_store_1 = require("./evidence-store");
exports.LIFECYCLE_RUN_PACK_SCHEMA_VERSION = 1;
exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME = "run-pack.json";
exports.MAX_LIFECYCLE_RUN_PACK_MANIFEST_BYTES = 2 * 1024 * 1024;
const PACK_DIRECTORY_PATTERN = /^\.day-shift\/runs\/[A-Za-z0-9][A-Za-z0-9._-]{1,127}$/u;
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const packPlans = new WeakMap();
function previewLifecycleRunPackSync(options) {
    const plan = buildLifecycleRunPackPlan(options);
    packPlans.set(plan.projection, plan);
    return plan.projection;
}
function applyLifecycleRunPackSync(preview, options = {}) {
    const plan = packPlans.get(preview);
    if (plan === undefined) {
        throw (0, errors_1.createUsageError)("Run pack apply requires the original in-process dry-run preview.");
    }
    return applyPackPlan(plan, options.transaction);
}
function createAndApplyLifecycleRunPackSync(options) {
    if (!SHA256_PATTERN.test(options.expectedSourceSnapshot)) {
        throw (0, errors_1.createUsageError)("Run pack apply requires the canonical source snapshot emitted by dry-run.");
    }
    const plan = buildLifecycleRunPackPlan(options);
    if (plan.projection.sourceSnapshot !== options.expectedSourceSnapshot) {
        throw (0, errors_1.createUsageError)("Run pack source changed after dry-run; refresh the exact source snapshot before apply.");
    }
    return applyPackPlan(plan, options.transaction);
}
function inspectLifecycleRunPackSync(options) {
    const repositoryRoot = node_fs_1.default.realpathSync(node_path_1.default.resolve(options.repositoryRoot));
    const destination = requirePackDirectory(options.destination);
    const destinationTarget = (0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, destination);
    if (!destinationTarget.exists) {
        throw (0, errors_1.createUsageError)(`Lifecycle run pack '${destination}' does not exist.`);
    }
    const destinationStat = node_fs_1.default.lstatSync(destinationTarget.runtimePath);
    if (destinationStat.isSymbolicLink() || !destinationStat.isDirectory()) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack destination must be a non-symbolic regular directory.");
    }
    const manifestAbsolute = node_path_1.default.join(destinationTarget.runtimePath, exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME);
    const manifestBytes = readPackRegularFile(manifestAbsolute, exports.MAX_LIFECYCLE_RUN_PACK_MANIFEST_BYTES);
    const parsed = parseJson(manifestBytes, "Lifecycle run pack manifest");
    const manifest = validatePackManifest(parsed, destination);
    const expectedFiles = new Set([exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME]);
    const objectIds = new Set();
    let uniqueObjectBytes = 0;
    for (const object of manifest.objects) {
        if (objectIds.has(object.objectId)) {
            throw (0, errors_1.createUsageError)(`Lifecycle run pack duplicates object identity '${object.objectId}'.`);
        }
        objectIds.add(object.objectId);
        const expectedObjectPath = packObjectPath(object.objectId);
        if (object.objectPath !== expectedObjectPath) {
            throw (0, errors_1.createUsageError)(`Lifecycle run pack object '${object.objectId}' has an inconsistent path.`);
        }
        expectedFiles.add(expectedObjectPath);
        const bytes = readPackRegularFile(node_path_1.default.join(destinationTarget.runtimePath, ...expectedObjectPath.split("/")), index_1.MAX_LIFECYCLE_RUN_RESULT_BYTES);
        if (bytes.byteLength !== object.bytes || (0, evidence_store_1.sha256LifecycleEvidence)(bytes) !== object.objectId) {
            throw (0, errors_1.createUsageError)(`Lifecycle run pack object '${object.objectId}' failed digest or byte verification.`);
        }
        uniqueObjectBytes += bytes.byteLength;
    }
    const entryIds = new Set();
    for (const logical of manifest.entries) {
        if (entryIds.has(logical.entry.entryId)) {
            throw (0, errors_1.createUsageError)(`Lifecycle run pack duplicates logical entry '${logical.entry.entryId}'.`);
        }
        entryIds.add(logical.entry.entryId);
        const object = manifest.objects.find((candidate) => candidate.objectId === logical.objectId);
        if (object === undefined
            || logical.objectPath !== object.objectPath
            || logical.entry.evidenceHash !== logical.objectId
            || logical.entry.evidenceBytes !== object.bytes) {
            throw (0, errors_1.createUsageError)(`Lifecycle run pack entry '${logical.entry.entryId}' has inconsistent object identity.`);
        }
    }
    if (manifest.entries.length !== manifest.sourceManifest.entries.length
        || manifest.entries.some((logical, index) => {
            const source = manifest.sourceManifest.entries[index];
            return source === undefined
                || source.entryId !== logical.entry.entryId
                || source.evidencePath !== logical.sourceEvidencePath
                || source.evidenceHash !== logical.objectId;
        })) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack logical entries do not match the embedded source manifest.");
    }
    if (manifest.sourceSnapshot.runId !== manifest.sourceManifest.runId
        || manifest.sourceSnapshot.runDirectory !== manifest.sourceManifest.runDirectory
        || manifest.sourceSnapshot.manifestPath !== node_path_1.default.posix.join(manifest.sourceManifest.runDirectory, "run-context.json")) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack source snapshot does not match its embedded manifest identity.");
    }
    const sourceSnapshotId = digestLogicalPack({
        manifestHash: manifest.sourceSnapshot.manifestHash,
        entries: manifest.entries.map((entry) => ({
            entryId: entry.entry.entryId,
            evidenceHash: entry.entry.evidenceHash,
            evidenceBytes: entry.entry.evidenceBytes,
            sourceEvidencePath: entry.sourceEvidencePath
        }))
    });
    if (manifest.sourceSnapshot.sourceSnapshotId !== sourceSnapshotId) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack source snapshot identity is inconsistent.");
    }
    const observedFiles = collectPackFiles(destinationTarget.runtimePath);
    if (observedFiles.length !== expectedFiles.size
        || observedFiles.some((candidate) => !expectedFiles.has(candidate))) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack contains unindexed, non-regular, or manifest-inconsistent files.");
    }
    const logicalDigest = digestLogicalPack(logicalPackValue(manifest));
    if (manifest.logicalPackDigest !== logicalDigest
        || manifest.packId !== packId(logicalDigest)) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack logical digest or identity is inconsistent.");
    }
    const sourceBytes = manifestBytesForSource(manifest.sourceManifest)
        + manifest.entries.reduce((total, entry) => total + entry.entry.evidenceBytes, 0);
    const projection = projectPack({
        operation: "verify",
        destination,
        manifest,
        manifestBytes,
        sourceBytes,
        uniqueObjectBytes,
        writeOutcome: "verified",
        readOnly: true,
        sourceUnchanged: true,
        changedPaths: [],
        expected: [],
        transactionId: null,
        conflicts: [],
        failure: null
    });
    return { ...projection, manifest };
}
function buildLifecycleRunPackPlan(options) {
    const repositoryRoot = node_fs_1.default.realpathSync(node_path_1.default.resolve(options.repositoryRoot));
    const destination = requirePackDirectory(options.destination);
    const inspection = (0, index_1.inspectLifecycleRunContext)({
        repositoryRoot,
        runDirectory: options.runDirectory
    });
    if (inspection.state !== "fresh" || !inspection.manifestComplete) {
        throw (0, errors_1.createUsageError)(`Lifecycle run '${inspection.manifest.runDirectory}' must be fresh and complete before packing.`);
    }
    const destinationTarget = (0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, destination);
    if (destinationTarget.exists) {
        throw (0, errors_1.createUsageError)(`Lifecycle run pack destination '${destination}' already exists.`);
    }
    const manifestTarget = (0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, inspection.manifestPath);
    const manifestBytes = readPackRegularFile(manifestTarget.runtimePath, index_1.MAX_LIFECYCLE_RUN_MANIFEST_BYTES);
    const objectBytes = new Map();
    const entries = inspection.manifest.entries.map((entry) => {
        const verified = (0, evidence_store_1.readLifecycleEvidenceBytesSync)({
            repositoryRoot,
            runDirectory: inspection.manifest.runDirectory,
            entry,
            maximumBytes: index_1.MAX_LIFECYCLE_RUN_RESULT_BYTES
        });
        if (!objectBytes.has(verified.objectId))
            objectBytes.set(verified.objectId, verified.bytes);
        return {
            entry: clone(entry),
            sourceEvidencePath: entry.evidencePath,
            objectId: verified.objectId,
            objectPath: packObjectPath(verified.objectId)
        };
    });
    const sourceSnapshotId = digestLogicalPack({
        manifestHash: inspection.manifestHash,
        entries: entries.map((entry) => ({
            entryId: entry.entry.entryId,
            evidenceHash: entry.entry.evidenceHash,
            evidenceBytes: entry.entry.evidenceBytes,
            sourceEvidencePath: entry.sourceEvidencePath
        }))
    });
    const objects = [...objectBytes.entries()]
        .map(([objectId, bytes]) => ({
        objectId,
        objectPath: packObjectPath(objectId),
        bytes: bytes.byteLength
    }))
        .sort((left, right) => left.objectId.localeCompare(right.objectId));
    const logical = {
        compatibility: {
            runContextSchemaVersions: [1],
            evidenceReferenceSchemaVersions: [1],
            legacyPerEntryEvidence: true
        },
        sourceSnapshot: {
            schemaVersion: 1,
            sourceSnapshotId,
            runId: inspection.manifest.runId,
            runDirectory: inspection.manifest.runDirectory,
            manifestPath: inspection.manifestPath,
            manifestHash: inspection.manifestHash
        },
        sourceManifest: clone(inspection.manifest),
        entries,
        objects
    };
    const logicalPackDigest = digestLogicalPack(logical);
    const packManifest = {
        schemaVersion: exports.LIFECYCLE_RUN_PACK_SCHEMA_VERSION,
        artifactType: "lifecycle-run-pack",
        packId: packId(logicalPackDigest),
        logicalPackDigest,
        ...logical
    };
    const packManifestBytes = serializePackManifest(packManifest);
    const preconditions = [
        (0, mutation_preconditions_1.captureContentRevisionPreconditionSync)(manifestTarget),
        ...[...new Set(inspection.manifest.entries.map((entry) => entry.evidenceStorage?.objectPath ?? entry.evidencePath))]
            .map((evidencePath) => (0, mutation_preconditions_1.captureContentRevisionPreconditionSync)((0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, evidencePath))),
        (0, mutation_preconditions_1.captureDestinationPresencePreconditionSync)(destinationTarget)
    ];
    const sourceBytes = manifestBytes.byteLength
        + entries.reduce((total, entry) => total + entry.entry.evidenceBytes, 0);
    const uniqueObjectBytes = objects.reduce((total, object) => total + object.bytes, 0);
    const projection = projectPack({
        operation: "dry-run",
        destination,
        manifest: packManifest,
        manifestBytes: packManifestBytes,
        sourceBytes,
        uniqueObjectBytes,
        writeOutcome: "previewed",
        readOnly: true,
        sourceUnchanged: true,
        changedPaths: [],
        expected: preconditions.map(mutation_preconditions_1.serializeMutationPrecondition),
        transactionId: null,
        conflicts: [],
        failure: null
    });
    const revalidateSource = () => {
        const currentManifestBytes = readPackRegularFile(manifestTarget.runtimePath, index_1.MAX_LIFECYCLE_RUN_MANIFEST_BYTES);
        if ((0, evidence_store_1.sha256LifecycleEvidence)(currentManifestBytes) !== inspection.manifestHash) {
            throw (0, errors_1.createUsageError)("Lifecycle run pack source manifest changed after dry-run.");
        }
        for (const entry of inspection.manifest.entries) {
            (0, evidence_store_1.readLifecycleEvidenceBytesSync)({
                repositoryRoot,
                runDirectory: inspection.manifest.runDirectory,
                entry,
                maximumBytes: index_1.MAX_LIFECYCLE_RUN_RESULT_BYTES
            });
        }
    };
    return {
        repositoryRoot,
        destination,
        manifest: packManifest,
        manifestBytes: packManifestBytes,
        objectBytes,
        preconditions,
        revalidateSource,
        projection
    };
}
function applyPackPlan(plan, transactionOptions) {
    const files = [
        { path: exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME, contents: plan.manifestBytes },
        ...plan.manifest.objects.map((object) => ({
            path: object.objectPath,
            contents: plan.objectBytes.get(object.objectId)
        }))
    ];
    const result = (0, repository_transaction_1.publishRepositoryDirectoryAtomicallySync)(plan.repositoryRoot, {
        destination: plan.destination,
        files,
        preconditions: plan.preconditions,
        revalidate: plan.revalidateSource,
        transactionId: `run-pack-${plan.manifest.logicalPackDigest.slice(7, 31)}`
    }, transactionOptions);
    return {
        ...plan.projection,
        operation: "apply",
        changedPaths: result.changedPaths,
        writeOutcome: result.outcome,
        transactionId: result.transactionId,
        conflicts: result.conflicts,
        readOnly: false,
        sourceUnchanged: result.outcome === "committed",
        noWriteBoundary: result.outcome === "committed"
            ? "Apply published one new immutable repository-local pack directory atomically and did not rewrite, delete, reclassify, or garbage-collect source run evidence."
            : "Pack apply failed before publishing the explicit destination; the canonical source run was not changed.",
        failure: result.failure
    };
}
function projectPack(options) {
    const intendedPaths = [
        node_path_1.default.posix.join(options.destination, exports.LIFECYCLE_RUN_PACK_MANIFEST_NAME),
        ...options.manifest.objects.map((object) => node_path_1.default.posix.join(options.destination, object.objectPath))
    ];
    return {
        schemaVersion: 1,
        packManifestVersion: options.manifest.schemaVersion,
        operation: options.operation,
        sourceRunDirectory: options.manifest.sourceSnapshot.runDirectory,
        destination: options.destination,
        sourceSnapshot: options.manifest.sourceSnapshot.sourceSnapshotId,
        logicalPackDigest: options.manifest.logicalPackDigest,
        packId: options.manifest.packId,
        sourceBytes: options.sourceBytes,
        uniqueObjectBytes: options.uniqueObjectBytes,
        projectedBytes: options.manifestBytes.byteLength + options.uniqueObjectBytes,
        duplicateCount: options.manifest.entries.length - options.manifest.objects.length,
        logicalEntryCount: options.manifest.entries.length,
        uniqueObjectCount: options.manifest.objects.length,
        intendedPaths,
        changedPaths: options.changedPaths,
        expected: options.expected,
        writeOutcome: options.writeOutcome,
        transactionId: options.transactionId,
        conflicts: options.conflicts,
        sourceUnchanged: options.sourceUnchanged,
        readOnly: options.readOnly,
        recovery: {
            verifyCommand: `day-shift resume run pack --destination ${options.destination} --verify --format json`,
            fullDetailCommand: `day-shift resume run pack --destination ${options.destination} --verify --detail full --format json`
        },
        noWriteBoundary: options.operation === "verify"
            ? "Pack verification reads and hashes only the immutable destination manifest and objects."
            : "Dry-run verifies the exact source manifest and every logical evidence reference, computes the pack, and writes no files.",
        failure: options.failure
    };
}
function validatePackManifest(value, destination) {
    if (!isRecord(value)
        || value.schemaVersion !== exports.LIFECYCLE_RUN_PACK_SCHEMA_VERSION
        || value.artifactType !== "lifecycle-run-pack"
        || typeof value.packId !== "string"
        || typeof value.logicalPackDigest !== "string"
        || !SHA256_PATTERN.test(value.logicalPackDigest)
        || !isRecord(value.compatibility)
        || !Array.isArray(value.compatibility.runContextSchemaVersions)
        || value.compatibility.runContextSchemaVersions.length !== 1
        || value.compatibility.runContextSchemaVersions[0] !== 1
        || !Array.isArray(value.compatibility.evidenceReferenceSchemaVersions)
        || value.compatibility.evidenceReferenceSchemaVersions.length !== 1
        || value.compatibility.evidenceReferenceSchemaVersions[0] !== 1
        || value.compatibility.legacyPerEntryEvidence !== true
        || !isRecord(value.sourceSnapshot)
        || value.sourceSnapshot.schemaVersion !== 1
        || typeof value.sourceSnapshot.sourceSnapshotId !== "string"
        || !SHA256_PATTERN.test(value.sourceSnapshot.sourceSnapshotId)
        || !isRecord(value.sourceManifest)
        || !Array.isArray(value.entries)
        || !Array.isArray(value.objects)) {
        throw (0, errors_1.createUsageError)(`Lifecycle run pack '${destination}' has an invalid or unsupported schema.`);
    }
    return value;
}
function collectPackFiles(root) {
    const files = [];
    const pending = [{ absolute: root, relative: "" }];
    while (pending.length > 0) {
        const current = pending.pop();
        for (const child of node_fs_1.default.readdirSync(current.absolute, { withFileTypes: true })) {
            const absolute = node_path_1.default.join(current.absolute, child.name);
            const relative = current.relative === "" ? child.name : node_path_1.default.posix.join(current.relative, child.name);
            const stat = node_fs_1.default.lstatSync(absolute);
            if (stat.isSymbolicLink()) {
                throw (0, errors_1.createUsageError)(`Lifecycle run pack contains symlinked path '${relative}'.`);
            }
            if (stat.isDirectory()) {
                pending.push({ absolute, relative });
            }
            else if (stat.isFile()) {
                files.push(relative);
            }
            else {
                throw (0, errors_1.createUsageError)(`Lifecycle run pack contains non-regular path '${relative}'.`);
            }
        }
    }
    return files.sort();
}
function readPackRegularFile(absolutePath, maximumBytes) {
    const stat = node_fs_1.default.lstatSync(absolutePath, { throwIfNoEntry: false });
    if (stat === undefined || stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1) {
        throw (0, errors_1.createUsageError)(`Lifecycle run pack file '${node_path_1.default.basename(absolutePath)}' is missing, linked, or non-regular.`);
    }
    if (stat.size <= 0 || stat.size > maximumBytes) {
        throw (0, errors_1.createUsageError)(`Lifecycle run pack file '${node_path_1.default.basename(absolutePath)}' exceeds its byte boundary.`);
    }
    return node_fs_1.default.readFileSync(absolutePath);
}
function logicalPackValue(manifest) {
    return {
        compatibility: manifest.compatibility,
        sourceSnapshot: manifest.sourceSnapshot,
        sourceManifest: manifest.sourceManifest,
        entries: manifest.entries,
        objects: manifest.objects
    };
}
function serializePackManifest(manifest) {
    const bytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
    if (bytes.byteLength > exports.MAX_LIFECYCLE_RUN_PACK_MANIFEST_BYTES) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack manifest exceeds its bounded byte limit.");
    }
    return bytes;
}
function manifestBytesForSource(manifest) {
    return Buffer.byteLength(`${JSON.stringify(manifest, null, 2)}\n`, "utf8");
}
function digestLogicalPack(value) {
    return `sha256:${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify(value)).digest("hex")}`;
}
function packId(digest) {
    return `lifecycle-run-pack-${digest.slice("sha256:".length, "sha256:".length + 24)}`;
}
function packObjectPath(objectId) {
    if (!SHA256_PATTERN.test(objectId)) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack object identity must be canonical SHA-256.");
    }
    return `objects/sha256/${objectId.slice("sha256:".length)}.json`;
}
function requirePackDirectory(value) {
    const normalized = (0, paths_1.parseRepositoryRelativePath)(value).normalizedPath;
    if (!PACK_DIRECTORY_PATTERN.test(normalized)) {
        throw (0, errors_1.createUsageError)("Lifecycle run pack destination must be one explicit repository-local '.day-shift/runs/<name>' path.");
    }
    return normalized;
}
function parseJson(bytes, label) {
    try {
        return JSON.parse(bytes.toString("utf8"));
    }
    catch {
        throw (0, errors_1.createUsageError)(`${label} must contain valid JSON.`);
    }
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function clone(value) {
    return JSON.parse(JSON.stringify(value));
}
