"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.previewLifecycleRunMigrationSync = previewLifecycleRunMigrationSync;
exports.applyLifecycleRunMigrationSync = applyLifecycleRunMigrationSync;
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 sandbox_contract_1 = require("../paths/sandbox-contract");
const canonical_write_target_1 = require("../paths/canonical-write-target");
const context_capture_1 = require("./context-capture");
const context_inspection_1 = require("./context-inspection");
const context_storage_1 = require("./context-storage");
const MAX_MIGRATION_FILES = 4096;
const MAX_MIGRATION_BYTES = 256 * 1024 * 1024;
const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/u;
function previewLifecycleRunMigrationSync(options) {
    return buildMigrationPlan(options).projection;
}
function applyLifecycleRunMigrationSync(options) {
    if (!SHA256_PATTERN.test(options.expectedSourceSnapshot)) {
        throw (0, errors_1.createUsageError)("Lifecycle run migration apply requires the source snapshot emitted by dry-run.");
    }
    const plan = buildMigrationPlan(options);
    if (plan.sourceSnapshot !== options.expectedSourceSnapshot) {
        throw (0, errors_1.createUsageError)("Legacy lifecycle run changed after migration dry-run; refresh the source snapshot.");
    }
    const parent = node_path_1.default.join(plan.repositoryRoot, node_path_1.default.dirname(plan.destinationRunDirectory));
    assertManagedRunsParentSafe(plan.repositoryRoot, parent);
    node_fs_1.default.mkdirSync(parent, { recursive: true, mode: 0o700 });
    const result = (0, repository_transaction_1.publishRepositoryDirectoryAtomicallySync)(plan.repositoryRoot, {
        destination: plan.destinationRunDirectory,
        files: plan.files,
        preconditions: plan.preconditions,
        revalidate: () => {
            const refreshed = buildMigrationPlan({
                repositoryRoot: plan.repositoryRoot,
                runDirectory: plan.sourceRunDirectory
            });
            if (refreshed.sourceSnapshot !== plan.sourceSnapshot) {
                throw (0, errors_1.createUsageError)("Legacy lifecycle run changed while migration was being applied.");
            }
        },
        transactionId: `run-migrate-${plan.sourceSnapshot.slice(7, 31)}`
    }, options.transaction);
    return {
        ...plan.projection,
        operation: "apply",
        changedPaths: result.changedPaths,
        writeOutcome: result.outcome,
        transactionId: result.transactionId,
        conflicts: result.conflicts,
        readOnly: false,
        noWriteBoundary: result.outcome === "committed"
            ? "Migration atomically published the canonical managed run and preserved every legacy source byte for explicit cleanup."
            : "Migration did not publish the canonical destination and did not modify the legacy source run.",
        failure: result.failure
    };
}
function buildMigrationPlan(options) {
    const repositoryRoot = node_fs_1.default.realpathSync(node_path_1.default.resolve(options.repositoryRoot));
    const sourceRunDirectory = options.runDirectory.replace(/\\/gu, "/");
    if (!(0, sandbox_contract_1.isLegacySandboxRunDirectoryPath)(sourceRunDirectory)) {
        throw (0, errors_1.createUsageError)("Lifecycle run migration requires one legacy 'sandbox/run-<run-id>' source directory.");
    }
    const destinationRunDirectory = (0, sandbox_contract_1.assertSandboxRunDirectoryPath)(`.day-shift/runs/${node_path_1.default.posix.basename(sourceRunDirectory)}`);
    const destinationTarget = (0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, destinationRunDirectory);
    if (destinationTarget.exists) {
        throw (0, errors_1.createUsageError)(`Lifecycle run migration destination '${destinationRunDirectory}' already exists.`);
    }
    const inspection = (0, context_inspection_1.inspectLifecycleRunContext)({ repositoryRoot, runDirectory: sourceRunDirectory });
    if (!inspection.manifestComplete) {
        throw (0, errors_1.createUsageError)("Legacy lifecycle run manifest must be complete before migration.", inspection.incompleteReasons);
    }
    const loaded = (0, context_inspection_1.readManifest)(repositoryRoot, sourceRunDirectory);
    const sourceFiles = collectMigrationFiles(loaded.manifestAbsolute, sourceRunDirectory);
    const migratedManifest = migrateManifest(loaded.manifest, sourceRunDirectory, destinationRunDirectory);
    (0, context_inspection_1.validateManifest)(migratedManifest, node_path_1.default.posix.join(destinationRunDirectory, context_capture_1.LIFECYCLE_RUN_MANIFEST_NAME));
    const migratedManifestBytes = (0, context_storage_1.jsonBytes)(migratedManifest, context_capture_1.MAX_LIFECYCLE_RUN_MANIFEST_BYTES);
    const files = sourceFiles.map((file) => file.path === context_capture_1.LIFECYCLE_RUN_MANIFEST_NAME
        ? { ...file, contents: migratedManifestBytes }
        : file);
    const preconditions = [
        ...sourceFiles.map((file) => (0, mutation_preconditions_1.captureContentRevisionPreconditionSync)((0, canonical_write_target_1.resolveCanonicalWriteTargetSync)(repositoryRoot, node_path_1.default.posix.join(sourceRunDirectory, file.path)))),
        (0, mutation_preconditions_1.captureDestinationPresencePreconditionSync)(destinationTarget)
    ];
    const sourceSnapshot = digestSource(sourceFiles);
    const intendedPaths = files.map((file) => node_path_1.default.posix.join(destinationRunDirectory, file.path));
    const applyCommand = `day-shift resume run migrate --run-directory ${sourceRunDirectory} --apply --expected-source-snapshot ${sourceSnapshot} --format json`;
    const projection = {
        schemaVersion: 1,
        operation: "dry-run",
        sourceRunDirectory,
        destinationRunDirectory,
        manifestPath: node_path_1.default.posix.join(destinationRunDirectory, context_capture_1.LIFECYCLE_RUN_MANIFEST_NAME),
        sourceSnapshot,
        sourceManifestHash: loaded.manifestHash,
        sourceFileCount: sourceFiles.length,
        sourceBytes: sourceFiles.reduce((total, file) => total + file.contents.byteLength, 0),
        intendedPaths,
        changedPaths: [],
        expected: preconditions.map(mutation_preconditions_1.serializeMutationPrecondition),
        writeOutcome: "previewed",
        transactionId: null,
        conflicts: [],
        sourceUnchanged: true,
        readOnly: true,
        noWriteBoundary: "Dry-run verifies the legacy manifest, every indexed evidence object, and every source file without writing.",
        recovery: {
            applyCommand,
            continueCommand: `day-shift resume run status --run-directory ${destinationRunDirectory} --format json`,
            legacyCleanup: `After verifying and continuing from '${destinationRunDirectory}', remove '${sourceRunDirectory}' only through an explicit operator-controlled cleanup.`
        },
        failure: null
    };
    return {
        repositoryRoot,
        sourceRunDirectory,
        destinationRunDirectory,
        sourceSnapshot,
        files,
        preconditions,
        projection
    };
}
function migrateManifest(manifest, source, destination) {
    return {
        ...manifest,
        runDirectory: destination,
        entries: manifest.entries.map((entry) => ({
            ...entry,
            evidencePath: remapOwnedPath(entry.evidencePath, source, destination),
            ...(entry.sourceResultPath === undefined
                ? {}
                : { sourceResultPath: remapOwnedPath(entry.sourceResultPath, source, destination) }),
            ...(entry.evidenceStorage === undefined
                ? {}
                : {
                    evidenceStorage: {
                        ...entry.evidenceStorage,
                        objectPath: remapOwnedPath(entry.evidenceStorage.objectPath, source, destination)
                    }
                })
        })),
        contractRevisions: manifest.contractRevisions.map((checkpoint) => ({
            ...checkpoint,
            reviewSourcePath: remapOwnedPath(checkpoint.reviewSourcePath, source, destination),
            reviewEvidencePath: remapOwnedPath(checkpoint.reviewEvidencePath, source, destination)
        }))
    };
}
function remapOwnedPath(value, source, destination) {
    return value.startsWith(`${source}/`) ? `${destination}${value.slice(source.length)}` : value;
}
function collectMigrationFiles(manifestAbsolute, sourceRunDirectory) {
    const runAbsolute = node_path_1.default.dirname(manifestAbsolute);
    const pending = [{ absolute: runAbsolute, relative: "" }];
    const files = [];
    let totalBytes = 0;
    while (pending.length > 0) {
        const current = pending.pop();
        const entries = node_fs_1.default.readdirSync(current.absolute, { withFileTypes: true })
            .sort((left, right) => left.name.localeCompare(right.name));
        for (const entry of entries) {
            const relative = current.relative === "" ? entry.name : node_path_1.default.posix.join(current.relative, entry.name);
            const absolute = node_path_1.default.join(current.absolute, entry.name);
            const stat = node_fs_1.default.lstatSync(absolute);
            if (stat.isSymbolicLink()) {
                throw (0, errors_1.createUsageError)(`Legacy lifecycle run '${sourceRunDirectory}' contains symbolic link '${relative}'.`);
            }
            if (stat.isDirectory()) {
                pending.push({ absolute, relative });
                continue;
            }
            if (!stat.isFile() || stat.size <= 0) {
                throw (0, errors_1.createUsageError)(`Legacy lifecycle run '${sourceRunDirectory}' contains unsupported entry '${relative}'.`);
            }
            totalBytes += stat.size;
            if (files.length >= MAX_MIGRATION_FILES || totalBytes > MAX_MIGRATION_BYTES) {
                throw (0, errors_1.createUsageError)("Legacy lifecycle run exceeds the bounded migration file or byte budget.");
            }
            files.push({ path: relative, contents: node_fs_1.default.readFileSync(absolute), mode: stat.mode & 0o777 });
        }
    }
    if (!files.some((file) => file.path === context_capture_1.LIFECYCLE_RUN_MANIFEST_NAME)) {
        throw (0, errors_1.createUsageError)("Legacy lifecycle run migration source is missing run-context.json.");
    }
    return files.sort((left, right) => left.path.localeCompare(right.path));
}
function digestSource(files) {
    const hash = (0, node_crypto_1.createHash)("sha256");
    for (const file of files) {
        hash.update(file.path).update("\0").update(file.contents).update("\0");
    }
    return `sha256:${hash.digest("hex")}`;
}
function assertManagedRunsParentSafe(repositoryRoot, parent) {
    const managedRoot = node_path_1.default.join(repositoryRoot, ".day-shift");
    const managedStat = node_fs_1.default.lstatSync(managedRoot, { throwIfNoEntry: false });
    if (managedStat === undefined || managedStat.isSymbolicLink() || !managedStat.isDirectory()) {
        throw (0, errors_1.createUsageError)("Lifecycle run migration requires an existing non-symbolic '.day-shift' directory.");
    }
    const parentStat = node_fs_1.default.lstatSync(parent, { throwIfNoEntry: false });
    if (parentStat !== undefined && (parentStat.isSymbolicLink() || !parentStat.isDirectory())) {
        throw (0, errors_1.createUsageError)("Managed lifecycle runs parent must be a non-symbolic directory.");
    }
}
