"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.frontMatterEvidenceDigest = frontMatterEvidenceDigest;
exports.frontMatterBlocks = frontMatterBlocks;
exports.resolveCanonicalRepositoryRoot = resolveCanonicalRepositoryRoot;
exports.resolveCanonicalSources = resolveCanonicalSources;
exports.resolveExactRehearsalTarget = resolveExactRehearsalTarget;
exports.removeOwnedTargetIfPresent = removeOwnedTargetIfPresent;
exports.writeOwnerMarker = writeOwnerMarker;
exports.ownerMarker = ownerMarker;
exports.parseOwnerMarker = parseOwnerMarker;
exports.copyTreePreservingBytesAndModes = copyTreePreservingBytesAndModes;
exports.copyFilePreservingBytesAndMode = copyFilePreservingBytesAndMode;
exports.walkDirectories = walkDirectories;
exports.walkFiles = walkFiles;
exports.collectReferencedSpecifications = collectReferencedSpecifications;
exports.validateReferencedSpecifications = validateReferencedSpecifications;
exports.readManifestIdentity = readManifestIdentity;
exports.authoredBody = authoredBody;
exports.digestTree = digestTree;
exports.rejectSymlinkAncestors = rejectSymlinkAncestors;
exports.rejectSymlinkPath = rejectSymlinkPath;
exports.readVolatileEvidence = readVolatileEvidence;
exports.requireDirectory = requireDirectory;
exports.requireFile = requireFile;
exports.modeToken = modeToken;
exports.nullable = nullable;
exports.unquote = unquote;
exports.sha256 = sha256;
exports.stableJson = stableJson;
exports.stableValue = stableValue;
exports.toPosix = toPosix;
const node_child_process_1 = require("node:child_process");
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const cli_constants_1 = require("../../config/cli-constants");
const planning_document_utils_1 = require("../planning-document-utils");
const contracts_1 = require("./contracts");
const validation_1 = require("./validation");
function frontMatterEvidenceDigest(markdownText, selector) {
    const parsed = (0, planning_document_utils_1.parseMarkdownFrontMatter)(markdownText);
    if (parsed.frontMatterLines.length === 0)
        return null;
    const blocks = frontMatterBlocks(parsed.frontMatterLines);
    const selected = blocks.filter((block) => selector.mode === "unknown"
        ? !validation_1.STANDARD_OR_TRANSFORMED_FRONT_MATTER_FIELDS.has(block.key)
        : selector.fields.has(block.key));
    return sha256(stableJson(selected));
}
function frontMatterBlocks(lines) {
    const blocks = [];
    for (const line of lines) {
        const match = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):(?:\s|$)/);
        if (match !== null) {
            blocks.push({ key: match[1], lines: [line] });
        }
        else if (blocks.length > 0) {
            blocks[blocks.length - 1].lines.push(line);
        }
    }
    return blocks;
}
function resolveCanonicalRepositoryRoot(repositoryRootInput) {
    const lexicalRoot = node_path_1.default.resolve(repositoryRootInput);
    requireDirectory(lexicalRoot, "Repository root");
    const repositoryRoot = node_fs_1.default.realpathSync(lexicalRoot);
    if (lexicalRoot !== repositoryRoot) {
        throw new contracts_1.RepositoryMigrationRehearsalError("invalid-repository", "Repository root aliases and symlinked repository paths are not accepted for rehearsal ownership.");
    }
    requireDirectory(node_path_1.default.join(repositoryRoot, ".git"), "Repository .git directory");
    requireFile(node_path_1.default.join(repositoryRoot, ".day-shift", "config.toml"), "Day Shift configuration");
    return repositoryRoot;
}
function resolveCanonicalSources(repositoryRoot) {
    const sources = {
        planning: node_path_1.default.join(repositoryRoot, ".day-shift", "planning"),
        specs: node_path_1.default.join(repositoryRoot, ".day-shift", "specs"),
        specIndex: node_path_1.default.join(repositoryRoot, ".day-shift", "specs", "index.json"),
        config: node_path_1.default.join(repositoryRoot, ".day-shift", "config.toml")
    };
    requireDirectory(sources.planning, "Canonical planning root");
    requireDirectory(sources.specs, "Canonical specification root");
    requireFile(sources.specIndex, "Canonical specification index");
    requireFile(sources.config, "Canonical configuration");
    for (const [label, sourcePath] of Object.entries(sources)) {
        rejectSymlinkPath(repositoryRoot, sourcePath, `Canonical ${label}`);
    }
    return sources;
}
function resolveExactRehearsalTarget(repositoryRoot, requestedTarget) {
    if (requestedTarget !== contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", `Rehearsal target must be exactly '${contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT}'.`);
    }
    const absolutePath = node_path_1.default.resolve(repositoryRoot, ...requestedTarget.split("/"));
    const expectedPath = node_path_1.default.join(repositoryRoot, ...contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT.split("/"));
    if (absolutePath !== expectedPath || !absolutePath.startsWith(`${repositoryRoot}${node_path_1.default.sep}`)) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", "Rehearsal target escapes or aliases the exact managed rehearsal boundary.");
    }
    rejectSymlinkAncestors(repositoryRoot, absolutePath);
    for (const forbidden of [
        node_path_1.default.join(repositoryRoot, ".day-shift", "planning"),
        node_path_1.default.join(repositoryRoot, ".day-shift", "specs"),
        node_path_1.default.join(repositoryRoot, ".day-shift", "config.toml")
    ]) {
        if (absolutePath === forbidden || forbidden.startsWith(`${absolutePath}${node_path_1.default.sep}`)) {
            throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", "Rehearsal target overlaps canonical planning, specification, or configuration state.");
        }
    }
    return {
        relativePath: contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT,
        absolutePath
    };
}
function removeOwnedTargetIfPresent(repositoryRoot, target) {
    if (!node_fs_1.default.existsSync(target.absolutePath))
        return;
    const stat = node_fs_1.default.lstatSync(target.absolutePath);
    if (stat.isSymbolicLink() || !stat.isDirectory()) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", "Existing rehearsal target is not a real directory; cleanup was rejected.");
    }
    const realTarget = node_fs_1.default.realpathSync(target.absolutePath);
    if (realTarget !== target.absolutePath) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", "Existing rehearsal target realpath does not match the exact managed target.");
    }
    const marker = parseOwnerMarker(node_path_1.default.join(target.absolutePath, ...contracts_1.REPOSITORY_MIGRATION_REHEARSAL_MARKER.split("/")));
    const expected = ownerMarker(repositoryRoot);
    if (stableJson(marker) !== stableJson(expected)) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unowned-target", "Existing rehearsal target is not owned by this exact repository and cannot be removed.");
    }
    node_fs_1.default.rmSync(target.absolutePath, { recursive: true, force: false });
}
function writeOwnerMarker(repositoryRoot, target) {
    const markerPath = node_path_1.default.join(target.absolutePath, ...contracts_1.REPOSITORY_MIGRATION_REHEARSAL_MARKER.split("/"));
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(markerPath), { recursive: true, mode: 0o700 });
    node_fs_1.default.writeFileSync(markerPath, `${JSON.stringify(ownerMarker(repositoryRoot), null, 2)}\n`, {
        mode: 0o600
    });
}
function ownerMarker(repositoryRoot) {
    return {
        protocolVersion: contracts_1.REPOSITORY_MIGRATION_REHEARSAL_PROTOCOL_VERSION,
        kind: "day-shift-repository-migration-rehearsal",
        targetPath: contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT,
        disposable: true,
        repositoryFingerprint: sha256(repositoryRoot)
    };
}
function parseOwnerMarker(markerPath) {
    if (!node_fs_1.default.existsSync(markerPath) || !node_fs_1.default.statSync(markerPath).isFile()) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unowned-target", `Existing rehearsal target is missing '${contracts_1.REPOSITORY_MIGRATION_REHEARSAL_MARKER}'.`);
    }
    try {
        const parsed = JSON.parse(node_fs_1.default.readFileSync(markerPath, "utf8"));
        if (parsed.protocolVersion !== contracts_1.REPOSITORY_MIGRATION_REHEARSAL_PROTOCOL_VERSION
            || parsed.kind !== "day-shift-repository-migration-rehearsal"
            || parsed.targetPath !== contracts_1.REPOSITORY_MIGRATION_REHEARSAL_ROOT
            || parsed.disposable !== true
            || !/^sha256:[a-f0-9]{64}$/.test(parsed.repositoryFingerprint)) {
            throw new Error("invalid marker fields");
        }
        return parsed;
    }
    catch (error) {
        if (error instanceof contracts_1.RepositoryMigrationRehearsalError)
            throw error;
        throw new contracts_1.RepositoryMigrationRehearsalError("unowned-target", "Existing rehearsal ownership marker is malformed.");
    }
}
function copyTreePreservingBytesAndModes(sourceRoot, destinationRoot) {
    requireDirectory(sourceRoot, "Copy source");
    rejectSymlinkPath(node_path_1.default.dirname(sourceRoot), sourceRoot, "Copy source");
    node_fs_1.default.mkdirSync(destinationRoot, { recursive: true });
    const directories = walkDirectories(sourceRoot);
    for (const sourceDirectory of directories) {
        const relative = node_path_1.default.relative(sourceRoot, sourceDirectory);
        const destination = node_path_1.default.join(destinationRoot, relative);
        const sourceStat = node_fs_1.default.statSync(sourceDirectory);
        node_fs_1.default.mkdirSync(destination, { recursive: true });
        node_fs_1.default.chmodSync(destination, sourceStat.mode & 0o777);
    }
    for (const sourceFile of walkFiles(sourceRoot)) {
        const destination = node_path_1.default.join(destinationRoot, node_path_1.default.relative(sourceRoot, sourceFile));
        copyFilePreservingBytesAndMode(sourceFile, destination);
    }
}
function copyFilePreservingBytesAndMode(sourcePath, destinationPath) {
    const sourceStat = node_fs_1.default.lstatSync(sourcePath);
    if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-source", `Rehearsal copy source '${toPosix(sourcePath)}' is not a real file.`);
    }
    node_fs_1.default.mkdirSync(node_path_1.default.dirname(destinationPath), { recursive: true });
    node_fs_1.default.copyFileSync(sourcePath, destinationPath);
    node_fs_1.default.chmodSync(destinationPath, sourceStat.mode & 0o777);
}
function walkDirectories(root) {
    const results = [root];
    const visit = (directory) => {
        for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
            const absolutePath = node_path_1.default.join(directory, entry.name);
            const stat = node_fs_1.default.lstatSync(absolutePath);
            if (stat.isSymbolicLink()) {
                throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-source", `Symlink '${toPosix(absolutePath)}' is not allowed in rehearsal source content.`);
            }
            if (entry.isDirectory()) {
                results.push(absolutePath);
                visit(absolutePath);
            }
        }
    };
    visit(root);
    return results.sort();
}
function walkFiles(root) {
    const results = [];
    const visit = (directory) => {
        for (const entry of node_fs_1.default.readdirSync(directory, { withFileTypes: true })) {
            const absolutePath = node_path_1.default.join(directory, entry.name);
            const stat = node_fs_1.default.lstatSync(absolutePath);
            if (stat.isSymbolicLink()) {
                throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-source", `Symlink '${toPosix(absolutePath)}' is not allowed in rehearsal source content.`);
            }
            if (entry.isDirectory())
                visit(absolutePath);
            if (entry.isFile())
                results.push(absolutePath);
        }
    };
    visit(root);
    return results.sort();
}
function collectReferencedSpecifications(manifest) {
    return [...new Set(manifest.entries.flatMap((entry) => entry.identity?.specificationRefs.filter((ref) => ref.startsWith(".day-shift/specs/") && ref !== cli_constants_1.CLI_SPEC_INDEX_PATH) ?? []))].sort();
}
function validateReferencedSpecifications(repositoryRoot, references) {
    for (const reference of references) {
        if (node_path_1.default.posix.normalize(reference) !== reference
            || reference.includes("..")
            || !reference.startsWith(".day-shift/specs/")) {
            throw new contracts_1.RepositoryMigrationRehearsalError("unsupported-reference", `Specification reference '${reference}' is outside the canonical specification root.`);
        }
        const absolutePath = node_path_1.default.join(repositoryRoot, ...reference.split("/"));
        requireFile(absolutePath, `Referenced specification '${reference}'`);
        rejectSymlinkPath(repositoryRoot, absolutePath, `Referenced specification '${reference}'`);
    }
}
function readManifestIdentity(markdownText) {
    const parsed = (0, planning_document_utils_1.parseMarkdownFrontMatter)(markdownText);
    if (parsed.frontMatterLines.length === 0)
        return null;
    const lines = parsed.frontMatterLines;
    const specificationIds = [];
    const specificationRefs = [];
    for (const line of lines) {
        const idMatch = line.match(/^\s{4}-\s+id:\s*(.+?)\s*$/);
        if (idMatch !== null)
            specificationIds.push(unquote(idMatch[1]));
        const refMatch = line.match(/^\s{6}ref:\s*(.+?)\s*$/);
        if (refMatch !== null)
            specificationRefs.push(unquote(refMatch[1]));
    }
    return {
        artifactType: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "artifact_type")),
        id: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "id")),
        workId: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "work_id")),
        task: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "task")),
        status: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "status")),
        slice: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "slice")),
        phase: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "phase")),
        milestone: nullable((0, planning_document_utils_1.readTopLevelField)(lines, "milestone")),
        specificationIds: [...new Set(specificationIds)].sort(),
        specificationRefs: [...new Set(specificationRefs)].sort()
    };
}
function authoredBody(markdownText) {
    if (!markdownText.startsWith("---"))
        return markdownText;
    const closing = markdownText.indexOf("\n---", 3);
    if (closing < 0)
        return markdownText;
    const bodyStart = markdownText.indexOf("\n", closing + 4);
    return bodyStart < 0 ? "" : markdownText.slice(bodyStart + 1);
}
function digestTree(root) {
    requireDirectory(root, `Canonical tree '${toPosix(root)}'`);
    rejectSymlinkPath(node_path_1.default.dirname(root), root, `Canonical tree '${toPosix(root)}'`);
    const entries = walkFiles(root).map((absolutePath) => {
        const stat = node_fs_1.default.statSync(absolutePath);
        return {
            path: toPosix(node_path_1.default.relative(root, absolutePath)),
            mode: modeToken(stat.mode),
            contentDigest: sha256(node_fs_1.default.readFileSync(absolutePath))
        };
    });
    return sha256(stableJson(entries));
}
function rejectSymlinkAncestors(repositoryRoot, targetPath) {
    const relativeParts = node_path_1.default.relative(repositoryRoot, targetPath).split(node_path_1.default.sep);
    let current = repositoryRoot;
    for (const part of relativeParts) {
        current = node_path_1.default.join(current, part);
        if (!node_fs_1.default.existsSync(current))
            continue;
        if (node_fs_1.default.lstatSync(current).isSymbolicLink()) {
            throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-target", "Rehearsal target contains a symlinked ancestor.");
        }
    }
}
function rejectSymlinkPath(repositoryRoot, targetPath, label) {
    const relative = node_path_1.default.relative(repositoryRoot, targetPath);
    if (relative.startsWith("..") || node_path_1.default.isAbsolute(relative)) {
        throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-source", `${label} escapes its owning repository root.`);
    }
    let current = repositoryRoot;
    for (const part of relative.split(node_path_1.default.sep).filter(Boolean)) {
        current = node_path_1.default.join(current, part);
        if (node_fs_1.default.lstatSync(current).isSymbolicLink()) {
            throw new contracts_1.RepositoryMigrationRehearsalError("unsafe-source", `${label} contains a symlinked path component.`);
        }
    }
}
function readVolatileEvidence(repositoryRoot) {
    const revision = (0, node_child_process_1.spawnSync)("git", ["rev-parse", "HEAD"], {
        cwd: repositoryRoot,
        encoding: "utf8"
    });
    const dirty = (0, node_child_process_1.spawnSync)("git", ["status", "--porcelain=v1"], {
        cwd: repositoryRoot,
        encoding: "utf8"
    });
    const buildManifestPath = node_path_1.default.join(repositoryRoot, "dist", ".day-shift-build-input-manifest.json");
    let migrationBuildIdentity = null;
    if (node_fs_1.default.existsSync(buildManifestPath)) {
        try {
            const parsed = JSON.parse(node_fs_1.default.readFileSync(buildManifestPath, "utf8"));
            if (typeof parsed.aggregateDigest === "string") {
                migrationBuildIdentity = parsed.aggregateDigest;
            }
        }
        catch {
            migrationBuildIdentity = null;
        }
    }
    return {
        repositoryRevision: revision.status === 0 ? revision.stdout.trim() : null,
        repositoryDirty: dirty.status === 0 ? dirty.stdout.length > 0 : null,
        migrationBuildIdentity
    };
}
function requireDirectory(targetPath, label) {
    if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.statSync(targetPath).isDirectory()) {
        throw new contracts_1.RepositoryMigrationRehearsalError("invalid-repository", `${label} '${toPosix(targetPath)}' is missing or is not a directory.`);
    }
}
function requireFile(targetPath, label) {
    if (!node_fs_1.default.existsSync(targetPath) || !node_fs_1.default.statSync(targetPath).isFile()) {
        throw new contracts_1.RepositoryMigrationRehearsalError("invalid-repository", `${label} '${toPosix(targetPath)}' is missing or is not a file.`);
    }
}
function modeToken(mode) {
    return `0o${(mode & 0o777).toString(8).padStart(3, "0")}`;
}
function nullable(value) {
    return value === undefined || value.length === 0 ? null : value;
}
function unquote(value) {
    const trimmed = value.trim();
    if ((trimmed.startsWith("\"") && trimmed.endsWith("\""))
        || (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
        return trimmed.slice(1, -1);
    }
    return trimmed;
}
function sha256(value) {
    return `sha256:${(0, node_crypto_1.createHash)("sha256").update(value).digest("hex")}`;
}
function stableJson(value) {
    return JSON.stringify(stableValue(value));
}
function stableValue(value) {
    if (Array.isArray(value))
        return value.map(stableValue);
    if (value !== null && typeof value === "object") {
        return Object.fromEntries(Object.entries(value)
            .sort(([left], [right]) => left.localeCompare(right))
            .map(([key, entry]) => [key, stableValue(entry)]));
    }
    return value;
}
function toPosix(value) {
    return value.split(node_path_1.default.sep).join("/");
}
