// engine/type-safety-gate.ts — SPEC-968: Deterministic TypeScript anti-pattern scanner
import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function getChangedTsFiles(projectPath) {
    let names;
    // Try origin/main...HEAD (typical feature branch)
    try {
        names = execSync('git diff --name-only --diff-filter=ACM origin/main...HEAD', {
            cwd: projectPath,
            encoding: 'utf-8',
            stdio: ['pipe', 'pipe', 'pipe'],
        })
            .trim()
            .split('\n')
            .filter(Boolean);
    }
    catch {
        // Fallback: last commit
        try {
            names = execSync('git diff --name-only --diff-filter=ACM HEAD~1..HEAD', {
                cwd: projectPath,
                encoding: 'utf-8',
                stdio: ['pipe', 'pipe', 'pipe'],
            })
                .trim()
                .split('\n')
                .filter(Boolean);
        }
        catch {
            // No git history — return empty
            return [];
        }
    }
    return names.filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && (f.startsWith('src/') || f.startsWith('tests/')));
}
function stripStrings(line) {
    // Naïve string-literal stripper — good enough for deterministic scanning
    return line
        .replace(/`(?:[^`\\]|\\.)*`/g, '``')
        .replace(/"(?:[^"\\]|\\.)*"/g, '""')
        .replace(/'(?:[^'\\]|\\.)*'/g, "''");
}
function codePart(line) {
    const idx = line.indexOf('//');
    return idx >= 0 ? line.slice(0, idx) : line;
}
function countAnyInLine(line) {
    const code = stripStrings(codePart(line));
    // Match `any` as a standalone type token
    return /\bany\b/.test(code);
}
function isUnjustifiedTsDirective(line) {
    const trimmed = line.trim();
    const ignoreMatch = /\/\/\s*@ts-ignore(?:\s+(.*))?$/.exec(trimmed);
    if (ignoreMatch) {
        const justification = (ignoreMatch[1] ?? '').trim();
        return justification.length > 0 ? null : 'ts-ignore';
    }
    const expectMatch = /\/\/\s*@ts-expect-error(?:\s+(.*))?$/.exec(trimmed);
    if (expectMatch) {
        const justification = (expectMatch[1] ?? '').trim();
        return justification.length > 0 ? null : 'ts-expect-error';
    }
    return null;
}
function extractNoImplicitAnyViolations(line) {
    const violations = [];
    // 1. Named functions: function foo(a, b)
    const namedFn = /function\s+\w+\s*\(([^)]*)\)/.exec(line);
    if (namedFn?.[1] !== undefined) {
        violations.push(...paramsWithoutTypes(namedFn[1]));
    }
    // 2. Arrow functions with parens: (a, b) =>
    const arrowParens = /\(([^)]*)\)\s*=>/.exec(line);
    if (arrowParens?.[1] !== undefined) {
        violations.push(...paramsWithoutTypes(arrowParens[1]));
    }
    // 3. Arrow functions with single bare param: x =>
    //    Avoid matching logical expressions like `a && b =>` by requiring start-of-expression
    const bareArrow = /(?:^|[^\w)])([a-zA-Z_$][\w$]*)\s*=>/.exec(line);
    if (bareArrow?.[1] !== undefined) {
        const param = bareArrow[1];
        if (param !== 'return' && param !== 'throw' && param !== 'yield') {
            violations.push(param);
        }
    }
    // 4. Methods / object shorthand: method(a, b) {
    // Skip if we already matched a named function to avoid double-counting
    if (violations.length > 0) {
        return violations;
    }
    const methodFn = /\b\w+\s*\(([^)]*)\)\s*\{/.exec(line);
    if (methodFn?.[1] !== undefined) {
        // Distinguish from if/while/for — those have keywords before the paren
        const before = line.slice(0, methodFn.index).trim();
        const isControlFlow = /\b(if|while|for|switch|catch)\s*$/.test(before);
        if (!isControlFlow) {
            violations.push(...paramsWithoutTypes(methodFn[1]));
        }
    }
    return violations;
}
function paramsWithoutTypes(params) {
    const result = [];
    // Very simple comma split — does not handle nested generics, but covers 95 % of cases
    const parts = params.split(',');
    for (const part of parts) {
        const p = part.trim();
        if (!p) {
            continue;
        }
        if (p.startsWith('...')) {
            // rest param — check if typed
            if (!p.includes(':')) {
                result.push(p);
            }
            continue;
        }
        if (p.includes(':')) {
            continue; // already typed
        }
        if (p.includes('=')) {
            continue; // default value with inferred type
        }
        // Destructured params without explicit type are also noImplicitAny violations
        result.push(p.length > 20 ? p.slice(0, 20) + '...' : p);
    }
    return result;
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export function scanTypeSafety(projectPath, waiver) {
    const changedFiles = getChangedTsFiles(projectPath);
    const violations = [];
    let anyCount = 0;
    let tsIgnoreCount = 0;
    let noImplicitAnyCount = 0;
    const waivers = new Set(waiver ?? []);
    for (const relativePath of changedFiles) {
        if (waivers.has(relativePath)) {
            continue;
        }
        const absolutePath = join(projectPath, relativePath);
        let content;
        try {
            content = readFileSync(absolutePath, 'utf-8');
        }
        catch {
            continue;
        }
        const lines = content.split('\n');
        let inBlockComment = false;
        for (let i = 0; i < lines.length; i++) {
            const rawLine = lines[i];
            if (rawLine === undefined) {
                continue;
            }
            const lineNum = i + 1;
            // Block comment tracking
            if (inBlockComment) {
                if (rawLine.includes('*/')) {
                    inBlockComment = false;
                }
                continue;
            }
            if (rawLine.includes('/*')) {
                if (!rawLine.includes('*/')) {
                    inBlockComment = true;
                }
                continue;
            }
            const trimmed = rawLine.trim();
            if (trimmed.startsWith('//')) {
                // Still check for ts-directives on comment lines
                const directive = isUnjustifiedTsDirective(rawLine);
                if (directive) {
                    violations.push({
                        file: relativePath,
                        line: lineNum,
                        type: directive,
                        message: `Unjustified // @${directive}`,
                    });
                    tsIgnoreCount++;
                }
                continue;
            }
            // any detection
            if (countAnyInLine(rawLine)) {
                violations.push({
                    file: relativePath,
                    line: lineNum,
                    type: 'any',
                    message: 'Use of `any` type detected',
                });
                anyCount++;
            }
            // noImplicitAny detection
            const implicitParams = extractNoImplicitAnyViolations(rawLine);
            for (const param of implicitParams) {
                violations.push({
                    file: relativePath,
                    line: lineNum,
                    type: 'noImplicitAny',
                    message: `Parameter \`${param}\` lacks explicit type annotation`,
                });
                noImplicitAnyCount++;
            }
        }
    }
    // Score: start at 100, penalise
    let score = 100;
    score -= anyCount * 10;
    score -= tsIgnoreCount * 10;
    score -= noImplicitAnyCount * 5;
    score = Math.max(0, score);
    const suggestions = [];
    if (anyCount > 0) {
        suggestions.push('Replace `any` with `unknown` when the shape of data is not known at compile time.', 'Consider using branded types (e.g., `type UserId = string & { __brand: "UserId" }`) for primitive values that carry semantic meaning.', 'Define proper error types instead of `any` in catch clauses (e.g., `error: unknown`).');
    }
    if (tsIgnoreCount > 0) {
        suggestions.push('Add a justification comment after `// @ts-ignore` or `// @ts-expect-error` explaining why the directive is necessary.', 'Prefer `// @ts-expect-error` with a descriptive reason over `// @ts-ignore`.');
    }
    if (noImplicitAnyCount > 0) {
        suggestions.push('Enable `noImplicitAny: true` in tsconfig.json to enforce explicit parameter types.', 'Add type annotations to all function parameters, even in test files.');
    }
    if (violations.length === 0) {
        suggestions.push('No type-safety violations detected. Great job!');
    }
    const passed = anyCount === 0 && tsIgnoreCount === 0;
    return {
        score,
        anyCount,
        tsIgnoreCount,
        noImplicitAnyCount,
        violations,
        suggestions,
        passed,
    };
}
//# sourceMappingURL=type-safety-gate.js.map