import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { getFrontmatterBlock } from '../shared/frontmatter.js';
import YAML from 'yaml';
const SEVERITIES = [
    'critical',
    'important',
    'medium',
    'minor',
];
const FRONTMATTER_COUNT_KEYS = {
    critical: ['oat_review_critical_count', 'critical'],
    important: ['oat_review_important_count', 'important'],
    medium: ['oat_review_medium_count', 'medium'],
    minor: ['oat_review_minor_count', 'minor'],
};
function artifactContentSignature(content) {
    return createHash('sha256').update(content).digest('hex');
}
async function assertArtifactContentCurrent(artifactPath, expectedContent) {
    let currentContent;
    try {
        currentContent = await readFile(artifactPath, 'utf8');
    }
    catch (error) {
        const detail = error instanceof Error ? error.message : String(error);
        throw new Error(`Unable to verify review artifact snapshot at ${artifactPath}: ${detail}`, { cause: error });
    }
    if (artifactContentSignature(currentContent) !==
        artifactContentSignature(expectedContent)) {
        throw new Error(`Review artifact at ${artifactPath} changed after gate correlation; rerun the gate so correlation and verdict evaluation use one immutable snapshot.`);
    }
}
function normalizeReviewType(value) {
    return value === 'code' || value === 'artifact' ? value : 'unknown';
}
function stringOrNull(value) {
    return typeof value === 'string' && value.trim() ? value.trim() : null;
}
function readGateInvocation(frontmatter) {
    const keys = [
        'oat_gate_run_id',
        'oat_gate_target',
        'oat_gate_runtime',
        'oat_invocation_model',
        'oat_invocation_reasoning_effort',
        'oat_invocation_source',
    ];
    if (!keys.some((key) => key in frontmatter)) {
        return undefined;
    }
    return {
        runId: stringOrNull(frontmatter['oat_gate_run_id']),
        targetId: stringOrNull(frontmatter['oat_gate_target']),
        runtime: stringOrNull(frontmatter['oat_gate_runtime']),
        model: stringOrNull(frontmatter['oat_invocation_model']),
        reasoningEffort: stringOrNull(frontmatter['oat_invocation_reasoning_effort']),
        source: stringOrNull(frontmatter['oat_invocation_source']),
    };
}
function parseCountValue(value) {
    const numberValue = typeof value === 'number'
        ? value
        : typeof value === 'string'
            ? Number(value.trim())
            : Number.NaN;
    if (!Number.isInteger(numberValue) || numberValue < 0) {
        return null;
    }
    return numberValue;
}
function readFrontmatterCounts(frontmatter) {
    const nestedCounts = typeof frontmatter['oat_review_counts'] === 'object' &&
        frontmatter['oat_review_counts'] !== null &&
        !Array.isArray(frontmatter['oat_review_counts'])
        ? frontmatter['oat_review_counts']
        : null;
    const counts = {
        critical: 0,
        important: 0,
        medium: 0,
        minor: 0,
    };
    for (const severity of SEVERITIES) {
        const candidateValues = [
            ...FRONTMATTER_COUNT_KEYS[severity].map((key) => frontmatter[key]),
            ...(nestedCounts
                ? FRONTMATTER_COUNT_KEYS[severity].map((key) => nestedCounts[key])
                : []),
        ].filter((value) => value !== undefined && value !== null);
        if (candidateValues.length === 0) {
            return null;
        }
        const parsedCount = candidateValues.reduce((parsed, value) => {
            if (parsed !== null) {
                return parsed;
            }
            return parseCountValue(value);
        }, null);
        if (parsedCount === null) {
            return null;
        }
        counts[severity] = parsedCount;
    }
    return counts;
}
function sectionContentIsEmpty(content) {
    const cleaned = content
        .split('\n')
        .map((line) => line.trim())
        .filter((line) => line.length > 0 && !/^<!--.*-->$/.test(line))
        .join('\n')
        .trim();
    return cleaned.length === 0 || /^none\.?$/i.test(cleaned);
}
function countFindingsInSection(content) {
    if (sectionContentIsEmpty(content)) {
        return 0;
    }
    return content
        .split('\n')
        .filter((line) => /^([-*+]\s+\S|\d+\.\s+\S)/.test(line)).length;
}
function normalizeHeading(value) {
    const normalized = value.trim().toLowerCase();
    if (SEVERITIES.includes(normalized)) {
        return normalized;
    }
    return null;
}
export function severityDisplayName(severity) {
    return severity.charAt(0).toUpperCase() + severity.slice(1);
}
function markdownLines(content) {
    const lines = [];
    let start = 0;
    while (start < content.length) {
        const newlineIndex = content.indexOf('\n', start);
        const end = newlineIndex === -1 ? content.length : newlineIndex;
        lines.push({
            text: content.slice(start, end),
            start,
        });
        start = newlineIndex === -1 ? content.length : newlineIndex + 1;
    }
    return lines;
}
function fenceMarker(line) {
    const match = line.match(/^\s*(`{3,}|~{3,})/);
    if (!match?.[1]) {
        return null;
    }
    return {
        marker: match[1].startsWith('`') ? '`' : '~',
        length: match[1].length,
    };
}
function linesOutsideFences(content) {
    const outsideFenceLines = [];
    let activeFence = null;
    for (const line of markdownLines(content)) {
        const marker = fenceMarker(line.text);
        if (marker) {
            if (!activeFence) {
                activeFence = marker;
            }
            else if (marker.marker === activeFence.marker &&
                marker.length >= activeFence.length) {
                activeFence = null;
            }
            continue;
        }
        if (!activeFence) {
            outsideFenceLines.push(line);
        }
    }
    return outsideFenceLines;
}
function findFindingsSection(content) {
    const lines = linesOutsideFences(content);
    const findingsHeading = lines.find((line) => /^##\s+Findings\s*#*\s*$/i.test(line.text));
    if (!findingsHeading) {
        return null;
    }
    const start = findingsHeading.start + findingsHeading.text.length;
    const nextPeerHeading = lines.find((line) => line.start > findingsHeading.start &&
        /^#{1,2}\s+.+?\s*#*\s*$/.test(line.text));
    const end = nextPeerHeading?.start ?? content.length;
    return {
        start,
        end,
        content: content.slice(start, end),
    };
}
function findSeverityHeadings(content) {
    const findingsSection = findFindingsSection(content);
    const scanContent = findingsSection?.content ?? content;
    const offset = findingsSection?.start ?? 0;
    return linesOutsideFences(scanContent)
        .map((line) => {
        const match = line.text.match(/^#{3,6}\s+(.+?)\s*#*\s*$/);
        return {
            severity: normalizeHeading(match?.[1] ?? ''),
            index: offset + line.start,
            headingLength: line.text.length,
        };
    })
        .filter((heading) => heading.severity !== null);
}
function missingSeverityHeadings(severityHeadings) {
    const seenSeverities = new Set(severityHeadings.map((heading) => heading.severity));
    return SEVERITIES.filter((severity) => !seenSeverities.has(severity));
}
function parseFindingsSummaryCounts(content) {
    const match = content.match(/^Findings:\s*(\d+)\s+critical,\s*(\d+)\s+important,\s*(\d+)\s+medium,\s*(\d+)\s+minor\s*$/im);
    if (!match) {
        return null;
    }
    return {
        critical: Number.parseInt(match[1] ?? '0', 10),
        important: Number.parseInt(match[2] ?? '0', 10),
        medium: Number.parseInt(match[3] ?? '0', 10),
        minor: Number.parseInt(match[4] ?? '0', 10),
    };
}
function parseFindingsSectionCounts(content, artifactPath) {
    const findingsSection = findFindingsSection(content);
    const severityHeadings = findSeverityHeadings(content);
    if (severityHeadings.length === 0) {
        return null;
    }
    const missingSeverities = missingSeverityHeadings(severityHeadings);
    if (missingSeverities.length > 0) {
        throw new Error(`Review artifact at ${artifactPath} has an incomplete Findings section; expected headings for Critical, Important, Medium, and Minor. Missing: ${missingSeverities.join(', ')}.`);
    }
    const counts = {
        critical: 0,
        important: 0,
        medium: 0,
        minor: 0,
    };
    for (const [offset, heading] of severityHeadings.entries()) {
        const nextHeading = severityHeadings[offset + 1];
        const sectionStart = heading.index + heading.headingLength;
        const sectionEnd = nextHeading?.index ?? findingsSection?.end ?? content.length;
        counts[heading.severity] = countFindingsInSection(content.slice(sectionStart, sectionEnd));
    }
    return counts;
}
function parseFrontmatterObject(frontmatter, artifactPath) {
    try {
        const parsed = YAML.parse(frontmatter);
        if (typeof parsed === 'object' &&
            parsed !== null &&
            !Array.isArray(parsed)) {
            return parsed;
        }
        throw new Error('frontmatter must be a YAML object');
    }
    catch (error) {
        const detail = error instanceof Error ? error.message : String(error);
        throw new Error(`Unable to parse review artifact frontmatter at ${artifactPath}: ${detail}`, { cause: error });
    }
}
function hasBlockingFindings(counts) {
    return counts.critical > 0 || counts.important > 0;
}
function insertionTextForSeverity(severity) {
    return `\n### ${severityDisplayName(severity)}\n\nNone\n`;
}
function insertionPointForMissingSeverity(content, missingSeverity) {
    const findingsSection = findFindingsSection(content);
    const sectionEnd = findingsSection?.end ?? content.length;
    const severityHeadings = findSeverityHeadings(content);
    const missingSeverityRank = SEVERITIES.indexOf(missingSeverity);
    const nextHeading = severityHeadings.find((heading) => SEVERITIES.indexOf(heading.severity) > missingSeverityRank);
    return nextHeading?.index ?? sectionEnd;
}
function insertMissingSeveritySection(content, severity) {
    const insertionPoint = insertionPointForMissingSeverity(content, severity);
    const insertion = insertionTextForSeverity(severity);
    const suffix = content.slice(insertionPoint);
    const needsTrailingBlank = suffix.length > 0 ? '\n' : '';
    return `${content.slice(0, insertionPoint)}${insertion}${needsTrailingBlank}${suffix}`;
}
async function normalizeMissingEmptySeveritySections(artifactPath, content, counts, persist) {
    if (!findFindingsSection(content)) {
        throw new Error(`Review artifact at ${artifactPath} does not contain a ## Findings section, so missing severity headings cannot be safely normalized.`);
    }
    let normalizedContent = content;
    const insertedSeverities = [];
    for (const severity of missingSeverityHeadings(findSeverityHeadings(content))) {
        if (counts[severity] !== 0) {
            throw new Error(`Review artifact at ${artifactPath} is missing the ${severityDisplayName(severity)} Findings section, but structured counts report ${counts[severity]} ${severity} finding(s). This cannot be safely normalized.`);
        }
        normalizedContent = insertMissingSeveritySection(normalizedContent, severity);
        insertedSeverities.push(severity);
    }
    if (insertedSeverities.length > 0 && persist) {
        await assertArtifactContentCurrent(artifactPath, content);
        await writeFile(artifactPath, normalizedContent, 'utf8');
        await assertArtifactContentCurrent(artifactPath, normalizedContent);
    }
    return {
        content: normalizedContent,
        insertedSeverities,
        persisted: insertedSeverities.length > 0 && persist,
    };
}
function resolveCounts(content, frontmatter) {
    const frontmatterCounts = readFrontmatterCounts(frontmatter);
    if (frontmatterCounts) {
        return frontmatterCounts;
    }
    const summaryCounts = parseFindingsSummaryCounts(content);
    if (summaryCounts) {
        return summaryCounts;
    }
    return null;
}
export async function parseReviewGateVerdict(artifactPath, options = {}) {
    let content;
    if (options.artifactSnapshot) {
        content = options.artifactSnapshot.content;
        if (artifactContentSignature(content) !== options.artifactSnapshot.signature) {
            throw new Error(`Review artifact snapshot signature at ${artifactPath} does not match its content.`);
        }
        await assertArtifactContentCurrent(artifactPath, content);
    }
    else {
        try {
            content = await readFile(artifactPath, 'utf8');
        }
        catch (error) {
            const detail = error instanceof Error ? error.message : String(error);
            throw new Error(`Unable to read review artifact at ${artifactPath}: ${detail}`, { cause: error });
        }
    }
    const frontmatterBlock = getFrontmatterBlock(content);
    const frontmatter = frontmatterBlock
        ? parseFrontmatterObject(frontmatterBlock, artifactPath)
        : {};
    let counts = resolveCounts(content, frontmatter);
    let insertedSeverities = [];
    let normalizationPersisted = false;
    if (counts && options.normalizeMissingEmptySeveritySections) {
        const normalized = await normalizeMissingEmptySeveritySections(artifactPath, content, counts, !options.artifactSnapshot);
        content = normalized.content;
        insertedSeverities = normalized.insertedSeverities;
        normalizationPersisted = normalized.persisted;
    }
    if (!counts) {
        counts = parseFindingsSectionCounts(content, artifactPath);
    }
    if (!counts) {
        throw new Error(`Review artifact at ${artifactPath} does not contain recognizable review findings or explicit verdict counts.`);
    }
    const gateInvocation = readGateInvocation(frontmatter);
    if (options.artifactSnapshot) {
        await assertArtifactContentCurrent(artifactPath, options.artifactSnapshot.content);
    }
    return {
        artifactPath,
        reviewType: normalizeReviewType(frontmatter['oat_review_type']),
        scope: stringOrNull(frontmatter['oat_review_scope']),
        invocation: stringOrNull(frontmatter['oat_review_invocation']),
        project: stringOrNull(frontmatter['oat_project']),
        ...(gateInvocation ? { gateInvocation } : {}),
        counts,
        blocking: hasBlockingFindings(counts),
        ...(insertedSeverities.length > 0
            ? {
                normalization: {
                    insertedSeverities,
                    persisted: normalizationPersisted,
                },
            }
            : {}),
    };
}
