"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLinkScopeRevisionEvidence = createLinkScopeRevisionEvidence;
exports.createRevisionBoundLinkGateEvidence = createRevisionBoundLinkGateEvidence;
exports.inspectMarkdownLinkMaterialization = inspectMarkdownLinkMaterialization;
exports.collectResolvedMarkdownLinkMaterializationCandidates = collectResolvedMarkdownLinkMaterializationCandidates;
exports.materializeMarkdownLinks = materializeMarkdownLinks;
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const validation_links_discovery_1 = require("./validation-links-discovery");
const validation_links_resolution_1 = require("./validation-links-resolution");
function createLinkScopeRevisionEvidence(options) {
    const artifactInputKind = classifyLinkScopeArtifactInputKind(options.repositoryRoot, options.artifactPath);
    const scope = options.artifactPath === null ? "repository" : "artifact";
    if (options.artifactPath !== null && !scopeArtifactExists(options.repositoryRoot, options.artifactPath)) {
        return {
            scope,
            artifactPath: options.artifactPath,
            artifactInputKind,
            kind: "missing",
            revision: null,
            observed: false,
            observedArtifactCount: 0
        };
    }
    if (artifactInputKind === "file" && options.artifactPath !== null) {
        const absolutePath = node_path_1.default.resolve(options.repositoryRoot, options.artifactPath);
        return {
            scope,
            artifactPath: options.artifactPath,
            artifactInputKind,
            kind: "content-sha256",
            revision: sha256(node_fs_1.default.readFileSync(absolutePath)),
            observed: true,
            observedArtifactCount: 1
        };
    }
    const observations = [...new Set(options.discovery.artifacts.map((artifact) => artifact.path))]
        .sort((left, right) => left.localeCompare(right))
        .map((artifactPath) => createScopeArtifactRevisionObservation(options.repositoryRoot, artifactPath));
    return {
        scope,
        artifactPath: options.artifactPath,
        artifactInputKind,
        kind: "scope-sha256",
        revision: sha256(JSON.stringify(observations)),
        observed: true,
        observedArtifactCount: observations.length
    };
}
function createRevisionBoundLinkGateEvidence(options) {
    const selectedRevisionEvidence = createLinkScopeRevisionEvidence({
        repositoryRoot: options.repositoryRoot,
        artifactPath: options.artifactPath,
        discovery: options.discovery
    });
    const findings = options.findings.map((finding) => ({
        code: finding.code,
        severity: finding.severity,
        message: finding.message,
        path: finding.path ?? null,
        recovery: finding.recovery ?? null
    }));
    const blockingFindingCount = findings.filter((finding) => finding.severity === "error").length;
    const advisoryFindingCount = findings.length - blockingFindingCount;
    const findingState = blockingFindingCount > 0
        ? "blocked"
        : advisoryFindingCount > 0
            ? "advisory"
            : "clean";
    const checkedRelationshipCount = options.resolution.summary.valueResolutionCount +
        options.resolution.summary.hierarchyCheckCount +
        options.resolution.summary.pairingCheckCount;
    const findingDigest = sha256(JSON.stringify(findings));
    const inspectionDigest = sha256(JSON.stringify({
        discoverySummary: options.discovery.summary,
        resolution: options.resolution,
        candidateCount: options.candidateCount,
        previewChangedPaths: [...options.previewChangedPaths],
        findingDigest
    }));
    const repositoryIdentity = sha256(node_fs_1.default.realpathSync(options.repositoryRoot));
    const evidenceId = `link-validation-evidence-${(0, node_crypto_1.createHash)("sha256").update(JSON.stringify({
        repositoryIdentity,
        scope: "planning",
        artifact: options.artifactPath,
        artifactInputKind: options.artifactInputKind,
        selectedRevision: selectedRevisionEvidence.revision,
        selectedRevisionKind: selectedRevisionEvidence.kind,
        findingState,
        findingDigest,
        inspectionDigest
    })).digest("hex")}`;
    const requiredRelationshipCount = options.resolution.summary.requiredTargetCount;
    const unresolvedRequiredRelationshipCount = options.resolution.summary.unresolvedRequiredTargetCount;
    const plannedTargetAdvisoryCount = options.resolution.summary.plannedTargetCount;
    const reusableAsLifecycleLinkGate = blockingFindingCount === 0 &&
        unresolvedRequiredRelationshipCount === 0 &&
        options.candidateCount === 0 &&
        selectedRevisionEvidence.observed &&
        selectedRevisionEvidence.revision !== null;
    return {
        schemaVersion: 1,
        evidenceId,
        scope: "planning",
        artifact: options.artifactPath,
        artifactInputKind: options.artifactInputKind,
        selectedRevisionEvidence,
        checkedArtifactCount: options.discovery.summary.artifactCount,
        checkedRelationshipCount,
        candidateCount: options.candidateCount,
        previewChangedPaths: [...options.previewChangedPaths],
        findingCount: findings.length,
        blockingFindingCount,
        advisoryFindingCount,
        requiredRelationshipCount,
        unresolvedRequiredRelationshipCount,
        plannedTargetAdvisoryCount,
        findingState,
        findingDigest,
        inspectionDigest,
        findings,
        reusableAsLifecycleLinkGate,
        reuseBoundary: "Reuse only for the same repository, planning scope, selected artifact or directory, revision digest, and finding state. Any mismatch requires a fresh link materialize dry-run."
    };
}
function inspectMarkdownLinkMaterialization(options) {
    const discovery = (0, validation_links_discovery_1.discoverValidationLinksArtifacts)({
        repositoryRoot: options.repositoryRoot,
        artifactPath: options.artifactPath
    });
    const resolution = (0, validation_links_resolution_1.evaluateValidationLinksResolution)(discovery);
    const candidates = collectResolvedMarkdownLinkMaterializationCandidates(discovery, resolution);
    const markdownText = node_fs_1.default.readFileSync(node_path_1.default.join(options.repositoryRoot, options.artifactPath), "utf8");
    const result = materializeMarkdownLinks({ markdownText, candidates });
    return {
        artifactPath: options.artifactPath,
        candidateCount: result.replacements.length,
        candidates,
        changed: result.changed,
        previewMarkdownText: result.markdownText,
        replacements: result.replacements,
        skippedCandidates: result.skippedCandidates
    };
}
function collectResolvedMarkdownLinkMaterializationCandidates(discovery, resolution) {
    const normalizedByKey = new Map();
    for (const evidence of resolution.valueResolutions) {
        if (evidence.valueShape === "body-reference-list"
            && evidence.materializationPolicy === "markdown-link"
            && evidence.resolution.status === "resolved-local-target"
            && evidence.resolution.normalizedPath !== undefined) {
            normalizedByKey.set(materializationCandidateKey(evidence.sourceArtifactPath, evidence.field, evidence.originalValue), evidence.resolution.normalizedPath);
        }
    }
    return discovery.artifacts.flatMap((artifact) => artifact.traceFields.flatMap((field) => {
        if (field.valueShape !== "body-reference-list"
            || field.materializationPolicy !== "markdown-link"
            || field.bodyReferenceOccurrences === undefined) {
            return [];
        }
        return field.bodyReferenceOccurrences.flatMap((occurrence) => {
            if (isExistingMarkdownLinkOccurrence(occurrence)) {
                return [];
            }
            const normalizedPath = normalizedByKey.get(materializationCandidateKey(artifact.path, field.field, occurrence.value));
            if (normalizedPath === undefined)
                return [];
            return [{
                    sourceArtifactPath: artifact.path,
                    field: field.field,
                    value: occurrence.value,
                    normalizedPath,
                    occurrence
                }];
        });
    }));
}
function materializeMarkdownLinks(options) {
    const replacements = [];
    const skippedCandidates = [];
    const occupiedRanges = [];
    const fencedCodeRanges = collectFencedCodeRanges(options.markdownText);
    const frontMatterRange = collectFrontMatterRange(options.markdownText);
    for (const candidate of [...options.candidates].sort(compareCandidates)) {
        const occurrence = candidate.occurrence;
        const startOffset = occurrence.startOffset;
        const endOffset = occurrence.endOffset;
        const rawText = options.markdownText.slice(startOffset, endOffset);
        if (isPlainPathOnlyField(candidate.field)) {
            skippedCandidates.push(createSkippedCandidate(candidate, "non-plain-reference", "Field requires plain repository-relative path bullets and is not materialized to Markdown links."));
            continue;
        }
        if (!isMaterializableReferenceFormat(candidate)) {
            skippedCandidates.push(createSkippedCandidate(candidate, "non-plain-reference", "Reference is not a plain body reference."));
            continue;
        }
        if (!sourceTextMatchesCandidate({ candidate, rawText })) {
            skippedCandidates.push(createSkippedCandidate(candidate, "source-mismatch", "Source text no longer matches the candidate evidence."));
            continue;
        }
        if (frontMatterRange !== null && rangesOverlap(occurrence, frontMatterRange)) {
            skippedCandidates.push(createSkippedCandidate(candidate, "front-matter", "Reference is inside YAML front matter."));
            continue;
        }
        if (fencedCodeRanges.some((range) => rangesOverlap(occurrence, range))) {
            skippedCandidates.push(createSkippedCandidate(candidate, "fenced-code", "Reference is inside a fenced code block."));
            continue;
        }
        if (occurrence.format === "plain" && isInsideInlineCode(options.markdownText, startOffset)) {
            skippedCandidates.push(createSkippedCandidate(candidate, "inline-code", "Reference is inside inline code."));
            continue;
        }
        if (isExistingMarkdownLinkDestination(options.markdownText, startOffset)) {
            skippedCandidates.push(createSkippedCandidate(candidate, "existing-link", "Reference is already part of a Markdown link."));
            continue;
        }
        if (occupiedRanges.some((range) => rangesOverlap(occurrence, range))) {
            skippedCandidates.push(createSkippedCandidate(candidate, "overlapping-candidate", "Candidate overlaps another accepted replacement."));
            continue;
        }
        const linkDestination = toMarkdownLinkDestination(candidate.sourceArtifactPath, candidate.normalizedPath);
        const linkText = toMarkdownLinkText(candidate);
        const replacementText = `[${linkText}](${linkDestination})`;
        replacements.push({
            sourceArtifactPath: candidate.sourceArtifactPath,
            field: candidate.field,
            value: candidate.value,
            normalizedPath: candidate.normalizedPath,
            linkDestination,
            startOffset,
            endOffset,
            replacementText
        });
        occupiedRanges.push({ startOffset, endOffset });
    }
    const markdownText = applyReplacements(options.markdownText, replacements);
    return {
        changed: replacements.length > 0,
        markdownText,
        replacements,
        skippedCandidates
    };
}
function toMarkdownLinkDestination(sourceArtifactPath, normalizedPath) {
    const sourceDirectory = node_path_1.default.posix.dirname(sourceArtifactPath);
    const relativePath = node_path_1.default.posix.relative(sourceDirectory, normalizedPath);
    return relativePath === "" ? node_path_1.default.posix.basename(normalizedPath) : relativePath;
}
function toMarkdownLinkText(candidate) {
    if (isDeclaredChildLinkField(candidate.field)) {
        return candidate.value;
    }
    if ((candidate.field === "body.Completed Task Summaries" ||
        candidate.field === "body.Acceptance Criteria Review" ||
        candidate.field === "body.Validation Review") &&
        candidate.normalizedPath.endsWith("/implementation-summary.md")) {
        const taskSlug = node_path_1.default.posix.basename(node_path_1.default.posix.dirname(candidate.normalizedPath));
        const readableTaskName = taskSlug
            .replace(/^\d+-/, "")
            .split("-")
            .filter((part) => part.length > 0)
            .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
            .join(" ");
        return readableTaskName === "" ? "Implementation Summary" : `${readableTaskName} Summary`;
    }
    return candidate.normalizedPath;
}
function isMaterializableReferenceFormat(candidate) {
    return candidate.occurrence.format === "plain" ||
        (candidate.occurrence.format === "backticked" &&
            isDeclaredChildLinkField(candidate.field));
}
function isPlainPathOnlyField(field) {
    return field === "body.Dependencies" ||
        field === "body.Implementation Target Paths" ||
        field === "body.Changed Paths";
}
function isDeclaredChildLinkField(field) {
    return field === "body.Phases" ||
        field === "body.Milestones" ||
        field === "body.Tasks" ||
        field === "body.Task Plan";
}
function sourceTextMatchesCandidate(options) {
    const occurrence = options.candidate.occurrence;
    if (options.rawText !== occurrence.rawText) {
        return false;
    }
    if (occurrence.format === "plain") {
        return options.rawText === options.candidate.value;
    }
    return occurrence.value === options.candidate.value;
}
function compareCandidates(left, right) {
    return left.occurrence.startOffset - right.occurrence.startOffset ||
        left.occurrence.endOffset - right.occurrence.endOffset;
}
function createSkippedCandidate(candidate, reason, message) {
    return {
        sourceArtifactPath: candidate.sourceArtifactPath,
        field: candidate.field,
        value: candidate.value,
        normalizedPath: candidate.normalizedPath,
        startOffset: candidate.occurrence.startOffset,
        endOffset: candidate.occurrence.endOffset,
        reason,
        message
    };
}
function applyReplacements(markdownText, replacements) {
    let nextText = markdownText;
    for (const replacement of [...replacements].sort((left, right) => right.startOffset - left.startOffset)) {
        nextText = `${nextText.slice(0, replacement.startOffset)}${replacement.replacementText}${nextText.slice(replacement.endOffset)}`;
    }
    return nextText;
}
function collectFrontMatterRange(markdownText) {
    if (!markdownText.startsWith("---")) {
        return null;
    }
    const closingMatch = /\r?\n---\r?\n/.exec(markdownText.slice(3));
    if (closingMatch === null) {
        return null;
    }
    return {
        startOffset: 0,
        endOffset: 3 + closingMatch.index + closingMatch[0].length
    };
}
function collectFencedCodeRanges(markdownText) {
    const ranges = [];
    const lines = markdownText.split(/\r?\n/);
    const newlineMatches = markdownText.match(/\r?\n/g) ?? [];
    let offset = 0;
    let openFenceStart = null;
    for (let index = 0; index < lines.length; index += 1) {
        if (/^\s*```/.test(lines[index])) {
            if (openFenceStart === null) {
                openFenceStart = offset;
            }
            else {
                ranges.push({
                    startOffset: openFenceStart,
                    endOffset: offset + lines[index].length
                });
                openFenceStart = null;
            }
        }
        offset += lines[index].length + (newlineMatches[index]?.length ?? 0);
    }
    if (openFenceStart !== null) {
        ranges.push({
            startOffset: openFenceStart,
            endOffset: markdownText.length
        });
    }
    return ranges;
}
function isInsideInlineCode(markdownText, offset) {
    const lineStart = markdownText.lastIndexOf("\n", offset - 1) + 1;
    const lineEndIndex = markdownText.indexOf("\n", offset);
    const lineEnd = lineEndIndex === -1 ? markdownText.length : lineEndIndex;
    const before = markdownText.slice(lineStart, offset);
    const line = markdownText.slice(lineStart, lineEnd);
    const backticksBefore = [...before].filter((character) => character === "`").length;
    const backticksInLine = [...line].filter((character) => character === "`").length;
    return backticksInLine >= 2 && backticksBefore % 2 === 1;
}
function isExistingMarkdownLinkDestination(markdownText, offset) {
    if (markdownText.slice(Math.max(0, offset - 2), offset) !== "](") {
        return false;
    }
    const closeParen = markdownText.indexOf(")", offset);
    return closeParen !== -1;
}
function rangesOverlap(left, right) {
    return left.startOffset < right.endOffset && right.startOffset < left.endOffset;
}
function isExistingMarkdownLinkOccurrence(occurrence) {
    return /^\[[^\]\n]*\]\([^)]+\)$/u.test(occurrence.rawText);
}
function materializationCandidateKey(sourceArtifactPath, field, value) {
    return `${sourceArtifactPath}\u0000${field}\u0000${value}`;
}
function classifyLinkScopeArtifactInputKind(repositoryRoot, artifactPath) {
    if (artifactPath === null) {
        return "repository";
    }
    const absolutePath = node_path_1.default.resolve(repositoryRoot, artifactPath);
    try {
        return node_fs_1.default.existsSync(absolutePath) && node_fs_1.default.statSync(absolutePath).isDirectory()
            ? "directory"
            : "file";
    }
    catch {
        return "file";
    }
}
function scopeArtifactExists(repositoryRoot, artifactPath) {
    try {
        return node_fs_1.default.existsSync(node_path_1.default.resolve(repositoryRoot, artifactPath));
    }
    catch {
        return false;
    }
}
function createScopeArtifactRevisionObservation(repositoryRoot, artifactPath) {
    const absolutePath = node_path_1.default.resolve(repositoryRoot, artifactPath);
    try {
        if (!node_fs_1.default.existsSync(absolutePath)) {
            return { path: artifactPath, kind: "missing", revision: null };
        }
        if (!node_fs_1.default.statSync(absolutePath).isFile()) {
            return { path: artifactPath, kind: "other", revision: null };
        }
        return {
            path: artifactPath,
            kind: "file",
            revision: sha256(node_fs_1.default.readFileSync(absolutePath))
        };
    }
    catch {
        return { path: artifactPath, kind: "missing", revision: null };
    }
}
function sha256(value) {
    return `sha256:${(0, node_crypto_1.createHash)("sha256").update(value).digest("hex")}`;
}
