import { readFileSync, promises as fs } from 'fs';
import path from 'path';
import { SpecSchema, ChangeSchema } from '../schemas/index.js';
import { MarkdownParser } from '../parsers/markdown-parser.js';
import { ChangeParser } from '../parsers/change-parser.js';
import { MIN_PURPOSE_LENGTH, MAX_REQUIREMENT_TEXT_LENGTH, VALIDATION_MESSAGES } from './constants.js';
import { parseDeltaSpec, normalizeRequirementName } from '../parsers/requirement-blocks.js';
import { findMainSpecStructureIssues } from '../parsers/spec-structure.js';
import { FileSystemUtils } from '../../utils/file-system.js';
export class Validator {
    strictMode;
    constructor(strictMode = false) {
        this.strictMode = strictMode;
    }
    async validateSpec(filePath) {
        const issues = [];
        const specName = this.extractNameFromPath(filePath);
        try {
            const content = readFileSync(filePath, 'utf-8');
            const parser = new MarkdownParser(content);
            const spec = parser.parseSpec(specName);
            const result = SpecSchema.safeParse(spec);
            if (!result.success) {
                issues.push(...this.convertZodErrors(result.error));
            }
            issues.push(...this.applySpecRules(spec, content));
        }
        catch (error) {
            const baseMessage = error instanceof Error ? error.message : 'Unknown error';
            const enriched = this.enrichTopLevelError(specName, baseMessage);
            issues.push({
                level: 'ERROR',
                path: 'file',
                message: enriched,
            });
        }
        return this.createReport(issues);
    }
    /**
     * Validate spec content from a string (used for pre-write validation of rebuilt specs)
     */
    async validateSpecContent(specName, content) {
        const issues = [];
        try {
            const parser = new MarkdownParser(content);
            const spec = parser.parseSpec(specName);
            const result = SpecSchema.safeParse(spec);
            if (!result.success) {
                issues.push(...this.convertZodErrors(result.error));
            }
            issues.push(...this.applySpecRules(spec, content));
        }
        catch (error) {
            const baseMessage = error instanceof Error ? error.message : 'Unknown error';
            const enriched = this.enrichTopLevelError(specName, baseMessage);
            issues.push({ level: 'ERROR', path: 'file', message: enriched });
        }
        return this.createReport(issues);
    }
    async validateChange(filePath) {
        const issues = [];
        const changeName = this.extractNameFromPath(filePath);
        try {
            const content = readFileSync(filePath, 'utf-8');
            const changeDir = path.dirname(filePath);
            const parser = new ChangeParser(content, changeDir);
            const change = await parser.parseChangeWithDeltas(changeName);
            const result = ChangeSchema.safeParse(change);
            if (!result.success) {
                issues.push(...this.convertZodErrors(result.error));
            }
            issues.push(...this.applyChangeRules(change, content));
        }
        catch (error) {
            const baseMessage = error instanceof Error ? error.message : 'Unknown error';
            const enriched = this.enrichTopLevelError(changeName, baseMessage);
            issues.push({
                level: 'ERROR',
                path: 'file',
                message: enriched,
            });
        }
        return this.createReport(issues);
    }
    /**
     * Validate delta-formatted spec files under a change directory.
     * Enforces:
     * - At least one delta across all files
     * - ADDED/MODIFIED: each requirement has SHALL/MUST and at least one scenario
     * - REMOVED: names only; no scenario/description required
     * - RENAMED: pairs well-formed
     * - No duplicates within sections; no cross-section conflicts per spec
     */
    async validateChangeDeltaSpecs(changeDir) {
        const issues = [];
        const specsDir = path.join(changeDir, 'specs');
        let totalDeltas = 0;
        const missingHeaderSpecs = [];
        const emptySectionSpecs = [];
        try {
            const entries = await fs.readdir(specsDir, { withFileTypes: true });
            for (const entry of entries) {
                if (!entry.isDirectory())
                    continue;
                const specName = entry.name;
                const specFile = path.join(specsDir, specName, 'spec.md');
                let content;
                try {
                    content = await fs.readFile(specFile, 'utf-8');
                }
                catch {
                    continue;
                }
                const plan = parseDeltaSpec(content);
                const entryPath = `${specName}/spec.md`;
                const sectionNames = [];
                if (plan.sectionPresence.added)
                    sectionNames.push('## ADDED Requirements');
                if (plan.sectionPresence.modified)
                    sectionNames.push('## MODIFIED Requirements');
                if (plan.sectionPresence.removed)
                    sectionNames.push('## REMOVED Requirements');
                if (plan.sectionPresence.renamed)
                    sectionNames.push('## RENAMED Requirements');
                const hasSections = sectionNames.length > 0;
                const hasEntries = plan.added.length + plan.modified.length + plan.removed.length + plan.renamed.length > 0;
                if (!hasEntries) {
                    if (hasSections)
                        emptySectionSpecs.push({ path: entryPath, sections: sectionNames });
                    else
                        missingHeaderSpecs.push(entryPath);
                }
                const addedNames = new Set();
                const modifiedNames = new Set();
                const removedNames = new Set();
                const renamedFrom = new Set();
                const renamedTo = new Set();
                // Validate ADDED
                for (const block of plan.added) {
                    const key = normalizeRequirementName(block.name);
                    totalDeltas++;
                    if (addedNames.has(key)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in ADDED: "${block.name}"` });
                    }
                    else {
                        addedNames.add(key);
                    }
                    const requirementText = this.extractRequirementText(block.raw);
                    if (!requirementText) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" is missing requirement text` });
                    }
                    else if (!this.containsShallOrMust(requirementText)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('ADDED', block.name) });
                    }
                    const scenarioCount = this.countScenarios(block.raw);
                    if (scenarioCount < 1) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `ADDED "${block.name}" must include at least one scenario` });
                    }
                }
                // Validate MODIFIED
                for (const block of plan.modified) {
                    const key = normalizeRequirementName(block.name);
                    totalDeltas++;
                    if (modifiedNames.has(key)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in MODIFIED: "${block.name}"` });
                    }
                    else {
                        modifiedNames.add(key);
                    }
                    const requirementText = this.extractRequirementText(block.raw);
                    if (!requirementText) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" is missing requirement text` });
                    }
                    else if (!this.containsShallOrMust(requirementText)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: this.buildMissingShallOrMustMessage('MODIFIED', block.name) });
                    }
                    const scenarioCount = this.countScenarios(block.raw);
                    if (scenarioCount < 1) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED "${block.name}" must include at least one scenario` });
                    }
                }
                // Validate REMOVED (names only)
                for (const name of plan.removed) {
                    const key = normalizeRequirementName(name);
                    totalDeltas++;
                    if (removedNames.has(key)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate requirement in REMOVED: "${name}"` });
                    }
                    else {
                        removedNames.add(key);
                    }
                }
                // Validate RENAMED pairs
                for (const { from, to } of plan.renamed) {
                    const fromKey = normalizeRequirementName(from);
                    const toKey = normalizeRequirementName(to);
                    totalDeltas++;
                    if (renamedFrom.has(fromKey)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate FROM in RENAMED: "${from}"` });
                    }
                    else {
                        renamedFrom.add(fromKey);
                    }
                    if (renamedTo.has(toKey)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Duplicate TO in RENAMED: "${to}"` });
                    }
                    else {
                        renamedTo.add(toKey);
                    }
                }
                // Cross-section conflicts (within the same spec file)
                for (const n of modifiedNames) {
                    if (removedNames.has(n)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and REMOVED: "${n}"` });
                    }
                    if (addedNames.has(n)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both MODIFIED and ADDED: "${n}"` });
                    }
                }
                for (const n of addedNames) {
                    if (removedNames.has(n)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `Requirement present in both ADDED and REMOVED: "${n}"` });
                    }
                }
                for (const { from, to } of plan.renamed) {
                    const fromKey = normalizeRequirementName(from);
                    const toKey = normalizeRequirementName(to);
                    if (modifiedNames.has(fromKey)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `MODIFIED references old name from RENAMED. Use new header for "${to}"` });
                    }
                    if (addedNames.has(toKey)) {
                        issues.push({ level: 'ERROR', path: entryPath, message: `RENAMED TO collides with ADDED for "${to}"` });
                    }
                }
            }
        }
        catch {
            // If no specs dir, treat as no deltas
        }
        for (const { path: specPath, sections } of emptySectionSpecs) {
            issues.push({
                level: 'ERROR',
                path: specPath,
                message: `Delta sections ${this.formatSectionList(sections)} were found, but no requirement entries parsed. Ensure each section includes at least one "### Requirement:" block (REMOVED may use bullet list syntax).`,
            });
        }
        for (const path of missingHeaderSpecs) {
            issues.push({
                level: 'ERROR',
                path,
                message: 'No delta sections found. Add headers such as "## ADDED Requirements" or move non-delta notes outside specs/.',
            });
        }
        if (totalDeltas === 0) {
            issues.push({ level: 'ERROR', path: 'file', message: this.enrichTopLevelError('change', VALIDATION_MESSAGES.CHANGE_NO_DELTAS) });
        }
        return this.createReport(issues);
    }
    convertZodErrors(error) {
        return error.issues.map(err => {
            let message = err.message;
            if (message === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
                message = `${message}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
            }
            return {
                level: 'ERROR',
                path: err.path.join('.'),
                message,
            };
        });
    }
    applySpecRules(spec, content) {
        const issues = [];
        for (const structuralIssue of findMainSpecStructureIssues(content)) {
            issues.push({
                level: 'ERROR',
                path: 'file',
                line: structuralIssue.line,
                message: structuralIssue.message,
            });
        }
        if (spec.overview.length < MIN_PURPOSE_LENGTH) {
            issues.push({
                level: 'WARNING',
                path: 'overview',
                message: VALIDATION_MESSAGES.PURPOSE_TOO_BRIEF,
            });
        }
        spec.requirements.forEach((req, index) => {
            if (req.text.length > MAX_REQUIREMENT_TEXT_LENGTH) {
                issues.push({
                    level: 'INFO',
                    path: `requirements[${index}]`,
                    message: VALIDATION_MESSAGES.REQUIREMENT_TOO_LONG,
                });
            }
            if (req.scenarios.length === 0) {
                issues.push({
                    level: 'WARNING',
                    path: `requirements[${index}].scenarios`,
                    message: `${VALIDATION_MESSAGES.REQUIREMENT_NO_SCENARIOS}. ${VALIDATION_MESSAGES.GUIDE_SCENARIO_FORMAT}`,
                });
            }
        });
        return issues;
    }
    applyChangeRules(change, content) {
        const issues = [];
        const MIN_DELTA_DESCRIPTION_LENGTH = 10;
        change.deltas.forEach((delta, index) => {
            if (!delta.description || delta.description.length < MIN_DELTA_DESCRIPTION_LENGTH) {
                issues.push({
                    level: 'WARNING',
                    path: `deltas[${index}].description`,
                    message: VALIDATION_MESSAGES.DELTA_DESCRIPTION_TOO_BRIEF,
                });
            }
            if ((delta.operation === 'ADDED' || delta.operation === 'MODIFIED') &&
                (!delta.requirements || delta.requirements.length === 0)) {
                issues.push({
                    level: 'WARNING',
                    path: `deltas[${index}].requirements`,
                    message: `${delta.operation} ${VALIDATION_MESSAGES.DELTA_MISSING_REQUIREMENTS}`,
                });
            }
        });
        return issues;
    }
    enrichTopLevelError(itemId, baseMessage) {
        const msg = baseMessage.trim();
        if (msg === VALIDATION_MESSAGES.CHANGE_NO_DELTAS) {
            return `${msg}. ${VALIDATION_MESSAGES.GUIDE_NO_DELTAS}`;
        }
        if (msg.includes('Spec must have a Purpose section') || msg.includes('Spec must have a Requirements section')) {
            return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_SPEC_SECTIONS}`;
        }
        if (msg.includes('Change must have a Why section') || msg.includes('Change must have a What Changes section')) {
            return `${msg}. ${VALIDATION_MESSAGES.GUIDE_MISSING_CHANGE_SECTIONS}`;
        }
        return msg;
    }
    extractNameFromPath(filePath) {
        const normalizedPath = FileSystemUtils.toPosixPath(filePath);
        const parts = normalizedPath.split('/');
        // Look for the directory name after 'specs' or 'changes'
        for (let i = parts.length - 1; i >= 0; i--) {
            if (parts[i] === 'specs' || parts[i] === 'changes') {
                if (i < parts.length - 1) {
                    return parts[i + 1];
                }
            }
        }
        // Fallback to filename without extension if not in expected structure
        const fileName = parts[parts.length - 1] ?? '';
        const dotIndex = fileName.lastIndexOf('.');
        return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
    }
    createReport(issues) {
        const errors = issues.filter(i => i.level === 'ERROR').length;
        const warnings = issues.filter(i => i.level === 'WARNING').length;
        const info = issues.filter(i => i.level === 'INFO').length;
        const valid = this.strictMode
            ? errors === 0 && warnings === 0
            : errors === 0;
        return {
            valid,
            issues,
            summary: {
                errors,
                warnings,
                info,
            },
        };
    }
    isValid(report) {
        return report.valid;
    }
    extractRequirementText(blockRaw) {
        const lines = blockRaw.split('\n');
        // Skip header line (index 0)
        let i = 1;
        // Find the first substantial text line, skipping metadata and blank lines
        for (; i < lines.length; i++) {
            const line = lines[i];
            // Stop at scenario headers
            if (/^####\s+/.test(line))
                break;
            const trimmed = line.trim();
            // Skip blank lines
            if (trimmed.length === 0)
                continue;
            // Skip metadata lines (lines starting with ** like **ID**, **Priority**, etc.)
            if (/^\*\*[^*]+\*\*:/.test(trimmed))
                continue;
            // Found first non-metadata, non-blank line - this is the requirement text
            return trimmed;
        }
        // No requirement text found
        return undefined;
    }
    containsShallOrMust(text) {
        return /\b(SHALL|MUST)\b/.test(text);
    }
    /**
     * Build an error message for a requirement block whose body lacks SHALL/MUST.
     *
     * When the SHALL/MUST keyword already appears in the requirement header (e.g.
     * `### Requirement: The system SHALL ...`) the original generic error
     * ("must contain SHALL or MUST") is confusing because the keyword is visibly
     * present in the spec. Per the OpenSpec conventions the keyword has to live
     * on the requirement body line (the line right after the header), so we point
     * the author at that exact fix when the keyword is found in the header only.
     */
    buildMissingShallOrMustMessage(action, blockName) {
        const base = `${action} "${blockName}" must contain SHALL or MUST`;
        if (this.containsShallOrMust(blockName)) {
            return `${base} in the requirement body, not only in the header. Move the SHALL/MUST statement to the line immediately after the "### Requirement: ..." header.`;
        }
        return base;
    }
    countScenarios(blockRaw) {
        const matches = blockRaw.match(/^####\s+/gm);
        return matches ? matches.length : 0;
    }
    formatSectionList(sections) {
        if (sections.length === 0)
            return '';
        if (sections.length === 1)
            return sections[0];
        const head = sections.slice(0, -1);
        const last = sections[sections.length - 1];
        return `${head.join(', ')} and ${last}`;
    }
}
//# sourceMappingURL=validator.js.map