"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, options.includeBodyReferences === true);
    const proposals = options.preview.proposals;
    const changedArtifactPaths = [...new Set(proposals.map((proposal) => proposal.sourceArtifactPath))];
    const plannedMutations = planArtifactMutations(options.repositoryRoot, proposals, options.includeBodyReferences === true);
    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, includeBodyReferences) {
    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) &&
            !(includeBodyReferences && isSupportedBodyReferenceProposal(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) {
        if (isSupportedBodyReferenceProposal(proposal)) {
            continue;
        }
        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 isSupportedBodyReferenceProposal(proposal) {
    return (proposal.sourceArtifactPath.endsWith('.md') &&
        proposal.traceField.startsWith('body.') &&
        proposal.artifactFamily !== 'spec_index_entry');
}
function planArtifactMutations(repositoryRoot, proposals, includeBodyReferences) {
    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];
        let nextBody = parsed.body;
        for (const proposal of artifactProposals.filter(isSupportedFrontMatterProposal)) {
            applyProposalToFrontMatter(nextFrontMatterLines, proposal);
        }
        if (includeBodyReferences) {
            nextBody = applyProposalsToBody(nextBody, artifactProposals.filter(isSupportedBodyReferenceProposal));
        }
        return {
            path: artifactPath,
            content: ['---', ...nextFrontMatterLines, '---', nextBody].join('\n'),
        };
    });
}
function applyProposalsToBody(body, proposals) {
    const groups = new Map();
    for (const proposal of proposals) {
        const key = `${proposal.traceField}\0${proposal.oldTargetPath}\0${proposal.newTargetPath}`;
        const current = groups.get(key) ?? [];
        current.push(proposal);
        groups.set(key, current);
    }
    let lines = body.split(/\r?\n/);
    for (const group of groups.values()) {
        const proposal = group[0];
        const sectionName = proposal.traceField.slice('body.'.length);
        const sectionRange = findMarkdownSection(lines, sectionName, proposal);
        const matches = findBodyReferenceMatches(lines, sectionRange, proposal.oldTargetPath);
        if (matches.length === 0) {
            throw new RangeError(`Trace-link disposition apply could not find the parsed '${proposal.traceField}' target in '${proposal.sourceArtifactPath}'.`);
        }
        for (const match of [...matches].sort((left, right) => right.line - left.line || right.column - left.column)) {
            lines[match.line] = replaceAt(lines[match.line], match.column, proposal.oldTargetPath.length, proposal.newTargetPath);
        }
    }
    return lines.join('\n');
}
function findMarkdownSection(lines, sectionName, proposal) {
    const heading = `## ${sectionName}`;
    const startHeading = lines.findIndex((line) => line.trim() === heading);
    if (startHeading === -1) {
        throw new RangeError(`Trace-link disposition apply could not find '${proposal.traceField}' in '${proposal.sourceArtifactPath}'.`);
    }
    let end = lines.length;
    for (let index = startHeading + 1; index < lines.length; index += 1) {
        if (/^##\s+\S/u.test(lines[index])) {
            end = index;
            break;
        }
    }
    return { start: startHeading + 1, end };
}
function findBodyReferenceMatches(lines, range, oldTargetPath) {
    const matches = [];
    let fenceMarker = null;
    for (let lineIndex = range.start; lineIndex < range.end; lineIndex += 1) {
        const line = lines[lineIndex];
        const trimmed = line.trimStart();
        const marker = trimmed.startsWith('```') ? '```' : trimmed.startsWith('~~~') ? '~~~' : null;
        if (marker !== null) {
            fenceMarker = fenceMarker === null ? marker : fenceMarker === marker ? null : fenceMarker;
            continue;
        }
        if (fenceMarker !== null) {
            continue;
        }
        let cursor = 0;
        while (cursor <= line.length - oldTargetPath.length) {
            const column = line.indexOf(oldTargetPath, cursor);
            if (column === -1) {
                break;
            }
            matches.push({ line: lineIndex, column });
            cursor = column + oldTargetPath.length;
        }
    }
    return matches;
}
function replaceAt(value, start, length, replacement) {
    return `${value.slice(0, start)}${replacement}${value.slice(start + length)}`;
}
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, '\\$&');
}
