// Planu — Validator: deep code checker
// Performs real filesystem/grep analysis for spec criteria that go beyond file-name matching.
import { readFile, stat } from 'node:fs/promises';
import { join, relative } from 'node:path';
import { glob } from 'glob';
const DEFAULT_IGNORE = ['node_modules/**', 'dist/**', 'build/**', '.git/**', 'coverage/**'];
// ─── Keyword classification ──────────────────────────────────────────────────
/**
 * Classify criterion text into a check type using keyword heuristics.
 * Returns the type and optional config derived from the criterion.
 */
export function classifyCriterion(text) {
    const lower = text.toLowerCase();
    // grep_absent: look for patterns that MUST NOT exist
    if (lower.includes('no hardcoded') ||
        lower.includes('no literal') ||
        lower.includes('no magic') ||
        lower.includes('no raw') ||
        lower.includes('remove all')) {
        return { type: 'grep_absent', config: buildAbsentConfig(text) };
    }
    // metric_max: file-size / line-count thresholds
    if (lower.includes('max lines') ||
        lower.includes('no file exceeds') ||
        lower.includes('under ') ||
        /\d+\s+lines/.exec(lower)) {
        return { type: 'metric_max', config: buildMetricConfig(text) };
    }
    // file_exists: a specific file must be present
    if (lower.includes('file exists') || lower.includes('must have file')) {
        return { type: 'file_exists', config: buildFileExistsConfig(text) };
    }
    // coverage: every source file must have a companion test
    if (lower.includes('test coverage') || /all .+ have tests/.exec(lower)) {
        return { type: 'coverage', config: buildCoverageConfig(text) };
    }
    // grep_present: a pattern MUST exist somewhere
    if (/all .+ have/.exec(lower) ||
        /every .+ includes?/.exec(lower) ||
        lower.includes('must contain')) {
        return { type: 'grep_present', config: buildPresentConfig(text) };
    }
    return { type: 'manual' };
}
// ─── Config builders ──────────────────────────────────────────────────────────
function buildAbsentConfig(text) {
    // Detect known patterns by keyword
    const lower = text.toLowerCase();
    if (lower.includes('spanish') || lower.includes('español') || lower.includes('i18n')) {
        return { pattern: '[áéíóúñ¿¡]', scope: 'src/**/*.{ts,tsx}', expected: 0 };
    }
    if (lower.includes('console.log')) {
        return { pattern: 'console\\.(log|debug|info)\\(', scope: 'src/**/*.{ts,tsx}', expected: 0 };
    }
    if (lower.includes('magic number') || lower.includes('magic string')) {
        return {
            pattern: '(?<![a-zA-Z])\\d{3,}(?![a-zA-Z0-9])',
            scope: 'src/**/*.{ts,tsx}',
            expected: 0,
        };
    }
    // Generic: try to extract a quoted term or word after "no"
    const quotedMatch = /"([^"]+)"|'([^']+)'/.exec(text);
    if (quotedMatch) {
        const term = quotedMatch[1] ?? quotedMatch[2] ?? '';
        return { pattern: term, scope: 'src/**/*.{ts,tsx,js,jsx}', expected: 0 };
    }
    const noMatch = /no\s+(\w+)/i.exec(text);
    const term = noMatch ? noMatch[1] : 'TODO';
    return { pattern: term ?? 'TODO', scope: 'src/**/*.{ts,tsx,js,jsx}', expected: 0 };
}
function buildPresentConfig(text) {
    const lower = text.toLowerCase();
    if (lower.includes('error handling') || lower.includes('try') || lower.includes('catch')) {
        return { pattern: 'try\\s*\\{|catch\\s*\\(', scope: 'src/**/*.{ts,tsx}', expected: 1 };
    }
    if (lower.includes('test') || lower.includes('spec')) {
        return { pattern: 'describe\\(|it\\(|test\\(', scope: 'tests/**/*.test.ts', expected: 1 };
    }
    // Generic: extract quoted term
    const quotedMatch = /"([^"]+)"|'([^']+)'/.exec(text);
    const term = quotedMatch ? (quotedMatch[1] ?? quotedMatch[2] ?? 'TODO') : 'export';
    return { pattern: term, scope: 'src/**/*.{ts,tsx,js,jsx}', expected: 1 };
}
function buildMetricConfig(text) {
    const numMatch = /(\d+)\s+lines?/i.exec(text);
    const threshold = numMatch ? parseInt(numMatch[1] ?? '400', 10) : 400;
    return { pattern: String(threshold), scope: 'src/**/*.{ts,tsx}', expected: threshold };
}
function buildFileExistsConfig(text) {
    const pathMatch = /["']([^"']+\.\w+)["']/.exec(text);
    const filePath = pathMatch ? (pathMatch[1] ?? '') : '';
    return { pattern: filePath, scope: filePath, expected: 1 };
}
function buildCoverageConfig(_text) {
    return { pattern: 'src/**/*.ts', scope: 'tests/**/*.test.ts', expected: 1 };
}
// ─── Utilities ────────────────────────────────────────────────────────────────
const MAX_FILES_PER_CHECK = 300;
async function resolveFiles(scopeGlob, projectPath, exclude) {
    const ignore = [...DEFAULT_IGNORE];
    if (exclude) {
        ignore.push(exclude);
    }
    try {
        const files = await glob(scopeGlob, {
            cwd: projectPath,
            nodir: true,
            ignore,
            maxDepth: 8,
        });
        return files.slice(0, MAX_FILES_PER_CHECK);
    }
    catch {
        return [];
    }
}
// ─── Checkers ─────────────────────────────────────────────────────────────────
import { isNativeActive, fastFindPatterns } from '../core-bridge.js';
function buildRegex(pattern) {
    try {
        return new RegExp(pattern, 'g');
    }
    catch {
        try {
            const escaped = RegExp.escape(pattern);
            return new RegExp(escaped, 'g');
        }
        catch {
            return null;
        }
    }
}
/**
 * Search for a regex pattern in files matching the scope glob.
 * For grep_absent: passed when matchCount === 0.
 * For grep_present: passed when matchCount >= expected.
 */
export async function grepCheck(config, projectPath, checkType) {
    const evidence = [];
    if (isNativeActive()) {
        const results = fastFindPatterns(projectPath, [config.pattern], [], config.exclude ? [config.exclude] : []);
        evidence.push(...results.map((r) => `${relative(projectPath, r.path)}:${r.line}`));
    }
    else {
        const files = await resolveFiles(config.scope, projectPath, config.exclude);
        const regex = buildRegex(config.pattern);
        if (regex) {
            await Promise.all(files.map(async (file) => {
                const fullPath = join(projectPath, file);
                try {
                    const content = await readFile(fullPath, 'utf-8');
                    const lines = content.split('\n');
                    for (let i = 0; i < lines.length; i++) {
                        const line = lines[i];
                        if (line !== undefined && regex.test(line)) {
                            evidence.push(`${file}:${i + 1}`);
                            regex.lastIndex = 0;
                        }
                    }
                }
                catch {
                    // Skip unreadable files
                }
            }));
        }
    }
    const matchCount = evidence.length;
    const passed = checkType === 'grep_absent' ? matchCount === 0 : matchCount >= config.expected;
    return {
        criterionText: config.pattern,
        checkType,
        passed,
        evidence,
        matchCount,
        expected: config.expected,
        description: checkType === 'grep_absent'
            ? `Pattern "${config.pattern}" must not appear in ${config.scope}`
            : `Pattern "${config.pattern}" must appear at least ${config.expected} time(s) in ${config.scope}`,
    };
}
/**
 * Check that no file in scope exceeds the line threshold encoded in config.expected.
 */
export async function metricCheck(config, projectPath) {
    const threshold = config.expected;
    const files = await resolveFiles(config.scope, projectPath, config.exclude);
    const evidence = [];
    await Promise.all(files.map(async (file) => {
        const fullPath = join(projectPath, file);
        try {
            const content = await readFile(fullPath, 'utf-8');
            const lineCount = content.split('\n').length;
            if (lineCount > threshold) {
                evidence.push(`${file}:${lineCount} lines (max ${threshold})`);
            }
        }
        catch {
            // Skip
        }
    }));
    return {
        criterionText: `No file exceeds ${threshold} lines`,
        checkType: 'metric_max',
        passed: evidence.length === 0,
        evidence,
        matchCount: evidence.length,
        expected: 0,
        description: `All files in ${config.scope} must have <= ${threshold} lines`,
    };
}
/**
 * Check that each source file has a corresponding test file.
 * sourceGlob: e.g. "src/**\/*.ts"
 * testGlob: e.g. "tests/**\/*.test.ts"
 */
export async function coverageCheck(sourceGlob, testGlob, projectPath) {
    const sourceFiles = await resolveFiles(sourceGlob, projectPath);
    // Cannot verify coverage with no source files — treat as non-authoritative
    if (sourceFiles.length === 0) {
        return {
            criterionText: `All ${sourceGlob} files have tests`,
            checkType: 'coverage',
            passed: false,
            evidence: [],
            matchCount: 0,
            expected: 0,
            description: `No source files found in ${sourceGlob} — coverage cannot be verified`,
        };
    }
    const testFiles = await resolveFiles(testGlob, projectPath);
    // Build a set of base names from test files (strip .test.ts / .spec.ts)
    const testBaseNames = new Set(testFiles.map((f) => {
        const base = f
            .replace(/\.test\.(ts|tsx|js|jsx)$/, '')
            .replace(/\.spec\.(ts|tsx|js|jsx)$/, '');
        return base.split('/').pop() ?? base;
    }));
    const uncovered = [];
    for (const src of sourceFiles) {
        const name = (src.split('/').pop() ?? src).replace(/\.(ts|tsx|js|jsx)$/, '');
        if (!testBaseNames.has(name)) {
            uncovered.push(src);
        }
    }
    return {
        criterionText: `All ${sourceGlob} files have tests`,
        checkType: 'coverage',
        passed: uncovered.length === 0,
        evidence: uncovered,
        matchCount: uncovered.length,
        expected: 0,
        description: `Each source file in ${sourceGlob} must have a corresponding test file`,
    };
}
/**
 * Verify a specific file exists.
 */
export async function fileExistsCheck(config, projectPath) {
    const filePath = config.scope || config.pattern;
    const fullPath = join(projectPath, filePath);
    let exists = false;
    try {
        await stat(fullPath);
        exists = true;
    }
    catch {
        // file does not exist — exists remains false
    }
    return {
        criterionText: `File exists: ${filePath}`,
        checkType: 'file_exists',
        passed: exists,
        evidence: exists ? [filePath] : [],
        matchCount: exists ? 1 : 0,
        expected: 1,
        description: `File ${filePath} must exist in the project`,
    };
}
// ─── Orchestrators ────────────────────────────────────────────────────────────
/**
 * Build a DeepCheckConfig from a VerifyBlock.
 * Maps the VerifyBlock fields to the internal DeepCheckConfig shape.
 */
function configFromVerifyBlock(block) {
    return {
        pattern: block.pattern ?? '',
        scope: block.scope ?? 'src/**/*.{ts,tsx,js,jsx}',
        exclude: block.exclude,
        expected: block.expected ?? (block.type === 'grep_absent' ? 0 : 1),
    };
}
/**
 * Classify a criterion and dispatch to the appropriate checker.
 * When `explicitVerify` is provided the keyword-based classifier is bypassed
 * and the verify block is used directly for the check.
 */
export async function deepCheckCriterion(criterionText, projectPath, explicitVerify) {
    // Use explicit verify block when available — skip keyword inference.
    // VerifyType and DeepCheckType overlap but are not identical, so we compare
    // the type as a string to avoid exhaustiveness errors on union mismatches.
    if (explicitVerify) {
        const config = configFromVerifyBlock(explicitVerify);
        const vtype = explicitVerify.type;
        if (vtype === 'grep_absent') {
            return { ...(await grepCheck(config, projectPath, 'grep_absent')), criterionText };
        }
        if (vtype === 'grep_present') {
            return { ...(await grepCheck(config, projectPath, 'grep_present')), criterionText };
        }
        if (vtype === 'metric_max') {
            return { ...(await metricCheck(config, projectPath)), criterionText };
        }
        if (vtype === 'file_exists') {
            return { ...(await fileExistsCheck(config, projectPath)), criterionText };
        }
        // manual, metric_min, command, and any unknown types fall through to manual result
        return {
            criterionText,
            checkType: 'manual',
            passed: true,
            evidence: [],
            matchCount: 0,
            expected: 0,
            description: explicitVerify.description,
        };
    }
    const { type, config } = classifyCriterion(criterionText);
    if (type === 'manual' || !config) {
        return {
            criterionText,
            checkType: 'manual',
            passed: true, // cannot verify automatically
            evidence: [],
            matchCount: 0,
            expected: 0,
            description: 'Criterion requires manual review — no automated check available',
        };
    }
    switch (type) {
        case 'grep_absent':
            return { ...(await grepCheck(config, projectPath, 'grep_absent')), criterionText };
        case 'grep_present':
            return { ...(await grepCheck(config, projectPath, 'grep_present')), criterionText };
        case 'metric_max':
            return { ...(await metricCheck(config, projectPath)), criterionText };
        case 'file_exists':
            return { ...(await fileExistsCheck(config, projectPath)), criterionText };
        case 'coverage': {
            const result = await coverageCheck('src/**/*.ts', 'tests/**/*.test.ts', projectPath);
            return { ...result, criterionText };
        }
        default:
            return {
                criterionText,
                checkType: 'manual',
                passed: true,
                evidence: [],
                matchCount: 0,
                expected: 0,
                description: 'Criterion requires manual review',
            };
    }
}
/**
 * Run deep checks on all criteria. Returns one result per criterion.
 */
export async function runDeepChecks(criteria, projectPath) {
    return Promise.all(criteria.map(async (criterion) => {
        try {
            return await deepCheckCriterion(criterion, projectPath);
        }
        catch {
            return {
                criterionText: criterion,
                checkType: 'manual',
                passed: false,
                evidence: [],
                matchCount: 0,
                expected: 0,
                description: 'Check could not be executed — criterion may be malformed',
            };
        }
    }));
}
//# sourceMappingURL=deep-code-checker.js.map