"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyTraceLinkDispositionPreviewMutations = applyTraceLinkDispositionPreviewMutations;
const node_fs_1 = __importDefault(require("node:fs"));
const paths_1 = require("../paths");
const apply_evidence_1 = require("./apply-evidence");
function applyTraceLinkDispositionPreviewMutations(options) {
    assertNoBlockingPreviewEvidence(options.preview);
    const proposals = options.preview.proposals;
    const changedArtifactPaths = [...new Set(proposals.map((proposal) => proposal.sourceArtifactPath))];
    const plannedMutations = planArtifactMutations(options.repositoryRoot, proposals);
    for (const mutation of plannedMutations) {
        node_fs_1.default.writeFileSync((0, paths_1.resolveRepositoryRelativePath)(options.repositoryRoot, mutation.path), mutation.content, "utf8");
    }
    const selectedDispositionIds = new Set(proposals.map((proposal) => proposal.dispositionId));
    const skippedPaths = uniqueEvidencePaths(options.preview.skipped);
    const conflictPaths = uniqueEvidencePaths(options.preview.conflicts);
    const unresolvedPaths = uniqueEvidencePaths(options.preview.unresolved);
    const dispositions = options.storageDocument.dispositions.map((disposition) => {
        if (!selectedDispositionIds.has(disposition.id)) {
            return disposition;
        }
        return (0, apply_evidence_1.attachTraceLinkDispositionApplyEvidence)(disposition, (0, apply_evidence_1.createTraceLinkDispositionApplyEvidence)({
            lastAppliedAt: options.appliedAt,
            dispositionRevision: (0, apply_evidence_1.createTraceLinkDispositionRevision)(disposition),
            changedPaths: changedArtifactPaths,
            skippedPaths,
            conflictPaths,
            unresolvedPaths
        }));
    });
    return {
        changedArtifactPaths,
        skippedPaths,
        conflictPaths,
        unresolvedPaths,
        dispositions
    };
}
function assertNoBlockingPreviewEvidence(preview) {
    if (preview.conflicts.length > 0 || preview.unresolved.length > 0) {
        throw new RangeError("Trace-link disposition apply cannot write while preview has conflict or unresolved evidence.");
    }
    for (const proposal of preview.proposals) {
        if (!isSupportedFrontMatterProposal(proposal)) {
            throw new RangeError(`Trace-link disposition apply does not support rewriting '${proposal.traceField}' in '${proposal.sourceArtifactPath}'.`);
        }
    }
    const seen = new Set();
    for (const proposal of preview.proposals) {
        const key = `${proposal.sourceArtifactPath}\0${proposal.traceField}\0${proposal.oldTargetPath}`;
        if (seen.has(key)) {
            throw new RangeError(`Trace-link disposition apply found multiple matching values for '${proposal.traceField}' in '${proposal.sourceArtifactPath}'.`);
        }
        seen.add(key);
    }
}
function isSupportedFrontMatterProposal(proposal) {
    return proposal.sourceArtifactPath.endsWith(".md") &&
        !proposal.traceField.startsWith("body.") &&
        proposal.artifactFamily !== "spec_index_entry";
}
function planArtifactMutations(repositoryRoot, proposals) {
    const groups = new Map();
    for (const proposal of proposals) {
        const current = groups.get(proposal.sourceArtifactPath) ?? [];
        current.push(proposal);
        groups.set(proposal.sourceArtifactPath, current);
    }
    return [...groups.entries()].map(([artifactPath, artifactProposals]) => {
        const absolutePath = (0, paths_1.resolveRepositoryRelativePath)(repositoryRoot, artifactPath);
        const originalContent = node_fs_1.default.readFileSync(absolutePath, "utf8");
        const parsed = parseMarkdownFrontMatter(originalContent, artifactPath);
        const nextFrontMatterLines = [...parsed.frontMatterLines];
        for (const proposal of artifactProposals) {
            applyProposalToFrontMatter(nextFrontMatterLines, proposal);
        }
        return {
            path: artifactPath,
            content: [
                "---",
                ...nextFrontMatterLines,
                "---",
                parsed.body
            ].join("\n")
        };
    });
}
function applyProposalToFrontMatter(frontMatterLines, proposal) {
    if (proposal.traceField === "upstream.specs") {
        replaceUpstreamSpecsRef(frontMatterLines, proposal);
        return;
    }
    const parts = proposal.traceField.split(".");
    if (parts.length === 2 && (parts[0] === "upstream" || parts[0] === "source")) {
        replaceNestedScalar(frontMatterLines, parts[0], parts[1], proposal);
        return;
    }
    if (parts.length === 1) {
        replaceTopLevelScalar(frontMatterLines, parts[0], proposal);
        return;
    }
    throw new RangeError(`Trace-link disposition apply does not support trace field '${proposal.traceField}'.`);
}
function replaceTopLevelScalar(frontMatterLines, key, proposal) {
    const matches = findMatchingScalarLines(frontMatterLines, new RegExp(`^(${escapeRegExp(key)}:\\s*)(.*)$`), proposal.oldTargetPath);
    replaceOnlyMatch(frontMatterLines, matches, proposal);
}
function replaceNestedScalar(frontMatterLines, parentKey, childKey, proposal) {
    const parentRange = findIndentedBlock(frontMatterLines, 0, parentKey);
    if (parentRange === null) {
        throw new RangeError(`Trace-link disposition apply could not find '${parentKey}' in '${proposal.sourceArtifactPath}'.`);
    }
    const matches = findMatchingScalarLines(frontMatterLines, new RegExp(`^(\\s{2}${escapeRegExp(childKey)}:\\s*)(.*)$`), proposal.oldTargetPath, parentRange);
    replaceOnlyMatch(frontMatterLines, matches, proposal);
}
function replaceUpstreamSpecsRef(frontMatterLines, proposal) {
    const upstreamRange = findIndentedBlock(frontMatterLines, 0, "upstream");
    if (upstreamRange === null) {
        throw new RangeError(`Trace-link disposition apply could not find 'upstream' in '${proposal.sourceArtifactPath}'.`);
    }
    const specsRange = findIndentedBlock(frontMatterLines, 2, "specs", upstreamRange);
    if (specsRange === null) {
        throw new RangeError(`Trace-link disposition apply could not find 'upstream.specs' in '${proposal.sourceArtifactPath}'.`);
    }
    const matches = findMatchingScalarLines(frontMatterLines, /^(\s{6}ref:\s*)(.*)$/, proposal.oldTargetPath, specsRange);
    replaceOnlyMatch(frontMatterLines, matches, proposal);
}
function findMatchingScalarLines(frontMatterLines, pattern, oldTargetPath, range) {
    const start = range?.start ?? 0;
    const end = range?.end ?? frontMatterLines.length;
    const matches = [];
    for (let index = start; index < end; index += 1) {
        const match = frontMatterLines[index].match(pattern);
        if (match === null) {
            continue;
        }
        if (parseYamlScalar(match[2]) === oldTargetPath) {
            matches.push({
                index,
                prefix: match[1],
                rawValue: match[2]
            });
        }
    }
    return matches;
}
function replaceOnlyMatch(frontMatterLines, matches, proposal) {
    if (matches.length !== 1) {
        throw new RangeError(`Trace-link disposition apply expected one '${proposal.traceField}' match in '${proposal.sourceArtifactPath}', found ${matches.length}.`);
    }
    const match = matches[0];
    frontMatterLines[match.index] = `${match.prefix}${formatYamlScalarLike(match.rawValue, proposal.newTargetPath)}`;
}
function findIndentedBlock(frontMatterLines, indent, key, range) {
    const start = range?.start ?? 0;
    const end = range?.end ?? frontMatterLines.length;
    const pattern = new RegExp(`^\\s{${indent}}${escapeRegExp(key)}:\\s*$`);
    for (let index = start; index < end; index += 1) {
        if (!pattern.test(frontMatterLines[index])) {
            continue;
        }
        let blockEnd = end;
        for (let cursor = index + 1; cursor < end; cursor += 1) {
            const line = frontMatterLines[cursor];
            if (line.trim() === "") {
                continue;
            }
            const lineIndent = line.match(/^ */)?.[0].length ?? 0;
            if (lineIndent <= indent) {
                blockEnd = cursor;
                break;
            }
        }
        return { start: index + 1, end: blockEnd };
    }
    return null;
}
function parseMarkdownFrontMatter(content, artifactPath) {
    const lines = content.split(/\r?\n/);
    if (lines[0] !== "---") {
        throw new RangeError(`Trace-link disposition apply requires YAML front matter in '${artifactPath}'.`);
    }
    const closingIndex = lines.indexOf("---", 1);
    if (closingIndex === -1) {
        throw new RangeError(`Trace-link disposition apply requires closed YAML front matter in '${artifactPath}'.`);
    }
    return {
        frontMatterLines: lines.slice(1, closingIndex),
        body: lines.slice(closingIndex + 1).join("\n")
    };
}
function parseYamlScalar(rawValue) {
    const trimmed = rawValue.trim();
    if ((trimmed.startsWith("\"") && trimmed.endsWith("\"")) ||
        (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
        return trimmed.slice(1, -1);
    }
    return trimmed;
}
function formatYamlScalarLike(rawValue, nextValue) {
    const trimmed = rawValue.trim();
    if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
        return `"${nextValue}"`;
    }
    if (trimmed.startsWith("'") && trimmed.endsWith("'")) {
        return `'${nextValue}'`;
    }
    return nextValue;
}
function uniqueEvidencePaths(evidence) {
    const paths = evidence.map((item) => item.sourceArtifactPath ?? item.dispositionTargetPath ?? item.dispositionId)
        .filter((value) => value !== undefined);
    return [...new Set(paths)];
}
function escapeRegExp(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
