import { execFile } from 'node:child_process';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { promisify } from 'node:util';
import { getLanguageSpec } from './languages.js';
import { collectTreeSitterErrorNodes, parseTreeSitterContent } from './tree-sitter.js';
const execFileAsync = promisify(execFile);
let typescriptModulePromise;
function loadTypescript() {
    typescriptModulePromise ??= import('typescript');
    return typescriptModulePromise;
}
function resolveWithinRepo(repoRoot, file) {
    const root = path.resolve(repoRoot);
    const resolved = path.isAbsolute(file) ? path.resolve(file) : path.resolve(root, file);
    const relativePath = path.relative(root, resolved);
    if (relativePath === '' || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
        return `Resolved path "${file}" is outside repo root "${root}".`;
    }
    return { path: resolved, relativePath: relativePath.split(path.sep).join('/') };
}
function stripBom(text) {
    return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
function languageForExtension(filePath) {
    const extension = path.extname(filePath).toLowerCase();
    switch (extension) {
        case '.ts':
        case '.tsx':
            return 'typescript';
        case '.js':
        case '.jsx':
        case '.mjs':
        case '.cjs':
            return 'javascript';
        case '.vue':
            return 'vue';
        case '.py':
            return 'python';
        default:
            return extension.replace(/^\./, '') || 'unknown';
    }
}
function diagnosticFromError(error, source) {
    const maybeError = error instanceof Error ? error : new Error(String(error));
    const loc = error;
    const start = loc.loc?.start ?? loc.loc;
    const line = typeof start?.line === 'number' ? start.line : undefined;
    const column = typeof start?.column === 'number' ? start.column + 1 : undefined;
    return {
        message: maybeError.message,
        line,
        column,
        source,
        severity: 'error',
    };
}
async function checkTypeScriptLike(filePath, content, language) {
    const ts = await loadTypescript();
    const extension = path.extname(filePath).toLowerCase();
    const scriptKind = extension === '.tsx'
        ? ts.ScriptKind.TSX
        : extension === '.jsx'
            ? ts.ScriptKind.JSX
            : language === 'javascript'
                ? ts.ScriptKind.JS
                : ts.ScriptKind.TS;
    const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
    const parseDiagnostics = sourceFile.parseDiagnostics;
    return parseDiagnostics.map((diagnostic) => {
        const position = diagnostic.start == null ? undefined : sourceFile.getLineAndCharacterOfPosition(diagnostic.start);
        return {
            message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
            line: position ? position.line + 1 : undefined,
            column: position ? position.character + 1 : undefined,
            code: diagnostic.code,
            source: 'typescript',
            severity: 'error',
        };
    });
}
async function checkVue(filePath, content) {
    const compiler = await import('@vue/compiler-sfc');
    const parsed = compiler.parse(content, { filename: filePath });
    const diagnostics = parsed.errors.map(error => diagnosticFromError(error, 'vue'));
    if (diagnostics.length > 0)
        return diagnostics;
    if (!parsed.descriptor.script && !parsed.descriptor.scriptSetup)
        return diagnostics;
    try {
        const compiled = compiler.compileScript(parsed.descriptor, { id: 'occ-check' });
        diagnostics.push(...(await checkTypeScriptLike(filePath, compiled.content, 'typescript')));
    }
    catch (error) {
        diagnostics.push(diagnosticFromError(error, 'vue'));
    }
    return diagnostics;
}
function isMissingExecutable(error) {
    return error instanceof Error && 'code' in error && error.code === 'ENOENT';
}
function pythonDiagnostic(stderr) {
    const encodingMatch = stderr.match(/OCC_ENCODING_ERROR:([\s\S]*)/);
    if (encodingMatch) {
        const detail = encodingMatch[1].trim();
        return {
            message: `Unsupported source encoding: ${detail || 'file is not valid UTF-8 and declares no encoding cookie'}`,
            code: 'unsupported-encoding',
            source: 'python',
            severity: 'error',
        };
    }
    const syntaxMatch = stderr.match(/OCC_SYNTAX_ERROR:(\d*):(\d*):([\s\S]*)/);
    if (syntaxMatch) {
        const line = syntaxMatch[1] ? Number(syntaxMatch[1]) : undefined;
        const column = syntaxMatch[2] ? Number(syntaxMatch[2]) : undefined;
        return {
            message: syntaxMatch[3].trim() || 'Python syntax error',
            line,
            column,
            source: 'python',
            severity: 'error',
        };
    }
    // Fallback for unexpected failure modes: anchor the line to a SyntaxError frame
    // so the harness's own `File "<string>", line N` frame is never reported.
    const lineMatch = stderr.match(/SyntaxError[\s\S]*?\bline (\d+)/) ?? stderr.match(/^\s*File .*, line (\d+)/m);
    const message = stderr
        .split(/\r?\n/)
        .map(line => line.trim())
        .filter(Boolean)
        .at(-1);
    return {
        message: message ?? (stderr.trim() || 'Python syntax error'),
        line: lineMatch ? Number(lineMatch[1]) : undefined,
        source: 'python',
        severity: 'error',
    };
}
async function checkPython(filePath) {
    // Read via tokenize.open so the PEP 263 encoding cookie and a leading BOM are
    // honored, then parse the decoded text. Encoding and syntax failures are
    // reported through distinct markers so a non-UTF-8 file is not mistaken for a
    // syntax error with a fabricated line number.
    const script = [
        'import ast, sys, tokenize',
        'path = sys.argv[1]',
        'try:',
        '    with tokenize.open(path) as handle:',
        '        source = handle.read()',
        'except (SyntaxError, ValueError, LookupError, UnicodeDecodeError) as exc:',
        "    sys.stderr.write('OCC_ENCODING_ERROR:' + str(exc))",
        '    sys.exit(1)',
        'try:',
        '    ast.parse(source, filename=path)',
        'except SyntaxError as exc:',
        "    sys.stderr.write('OCC_SYNTAX_ERROR:%s:%s:%s' % (exc.lineno or '', exc.offset or '', exc.msg or ''))",
        '    sys.exit(1)',
    ].join('\n');
    try {
        await execFileAsync('python3', ['-c', script, filePath], { timeout: 5_000, maxBuffer: 1_000_000 });
        return [];
    }
    catch (error) {
        if (isMissingExecutable(error))
            return 'unsupported';
        const execError = error;
        const stderr = typeof execError.stderr === 'string' ? execError.stderr : String(execError.stderr ?? '');
        return [pythonDiagnostic(stderr)];
    }
}
async function checkTreeSitter(grammar, content) {
    const tree = await parseTreeSitterContent(grammar, content);
    if (!tree)
        return 'unsupported';
    // Free the WASM tree handle once the error nodes are copied into plain diagnostics.
    try {
        return collectTreeSitterErrorNodes(tree.rootNode).map(error => ({
            message: error.type === 'ERROR' ? 'Syntax error' : `Missing syntax node: ${error.text || error.type}`,
            line: error.line,
            column: error.column,
            source: 'tree-sitter',
            severity: 'error',
        }));
    }
    finally {
        tree.delete();
    }
}
export async function checkCodeSyntax(options) {
    const resolved = resolveWithinRepo(options.repoRoot, options.file);
    if (typeof resolved === 'string') {
        return {
            status: 'error',
            supported: false,
            valid: false,
            diagnostics: [{ message: resolved, severity: 'error', source: 'occ' }],
            message: resolved,
        };
    }
    if (!existsSync(resolved.path)) {
        const message = `File not found: ${resolved.relativePath}`;
        return {
            status: 'error',
            supported: false,
            valid: false,
            path: resolved.path,
            relativePath: resolved.relativePath,
            diagnostics: [{ message, severity: 'error', source: 'occ' }],
            message,
        };
    }
    const language = languageForExtension(resolved.path);
    const extension = path.extname(resolved.path).toLowerCase();
    const spec = getLanguageSpec(resolved.path);
    if (extension === '.py') {
        let diagnostics = await checkPython(resolved.path);
        if (diagnostics === 'unsupported' && spec?.treeSitter) {
            // python3 isn't on PATH (e.g. Windows) — fall back to the vendored tree-sitter grammar.
            const content = stripBom(await readFile(resolved.path, 'utf8'));
            diagnostics = await checkTreeSitter(spec.treeSitter.grammar, content);
        }
        if (diagnostics === 'unsupported') {
            return {
                status: 'unsupported',
                supported: false,
                path: resolved.path,
                relativePath: resolved.relativePath,
                language,
                diagnostics: [],
            };
        }
        return {
            status: 'checked',
            supported: true,
            valid: diagnostics.length === 0,
            path: resolved.path,
            relativePath: resolved.relativePath,
            language,
            diagnostics,
        };
    }
    if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue'].includes(extension) && !spec?.treeSitter) {
        return {
            status: 'unsupported',
            supported: false,
            path: resolved.path,
            relativePath: resolved.relativePath,
            language,
            diagnostics: [],
        };
    }
    const content = stripBom(await readFile(resolved.path, 'utf8'));
    const diagnostics = extension === '.vue'
        ? await checkVue(resolved.path, content)
        : ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(extension)
            ? await checkTypeScriptLike(resolved.path, content, language)
            : spec?.treeSitter
                ? await checkTreeSitter(spec.treeSitter.grammar, content)
                : 'unsupported';
    if (diagnostics === 'unsupported') {
        return {
            status: 'unsupported',
            supported: false,
            path: resolved.path,
            relativePath: resolved.relativePath,
            language,
            diagnostics: [],
        };
    }
    return {
        status: 'checked',
        supported: true,
        valid: diagnostics.length === 0,
        path: resolved.path,
        relativePath: resolved.relativePath,
        language,
        diagnostics,
    };
}
//# sourceMappingURL=check.js.map