// engine/config-health/python-checker.ts — SPEC-042
// Python project configuration health checks.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import healthSchemas from '../../config/config-health-schemas.json' with { type: 'json' };
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
async function readFileSafe(filePath) {
    try {
        return await readFile(filePath, 'utf-8');
    }
    catch {
        return '';
    }
}
// ---------------------------------------------------------------------------
// TOML parser (minimal — no external deps)
// ---------------------------------------------------------------------------
/**
 * Extract a TOML section as a list of key names under a given section header.
 * Example: given "[tool.ruff.lint]" section, returns the keys underneath.
 */
function extractTomlSection(content, sectionPath) {
    const lines = content.split('\n');
    const headerPattern = new RegExp(`^\\[${sectionPath.replace('.', '\\.')}\\]`);
    const keys = [];
    let inSection = false;
    for (const line of lines) {
        const trimmed = line.trim();
        if (headerPattern.test(trimmed)) {
            inSection = true;
            continue;
        }
        if (inSection) {
            if (trimmed.startsWith('[')) {
                break;
            }
            const keyMatch = /^([\w-]+)\s*=/.exec(trimmed);
            if (keyMatch?.[1]) {
                keys.push(keyMatch[1]);
            }
        }
    }
    return keys;
}
/**
 * Extract top-level section names from a TOML file.
 * Returns sections like ["tool.ruff", "tool.mypy", "project"]
 */
function extractTomlSections(content) {
    const lines = content.split('\n');
    const sections = [];
    for (const line of lines) {
        const trimmed = line.trim();
        const sectionMatch = /^\[([^\]]+)\]/.exec(trimmed);
        if (sectionMatch?.[1]) {
            sections.push(sectionMatch[1].trim());
        }
    }
    return sections;
}
// ---------------------------------------------------------------------------
// Check: pyproject.toml schema validation
// ---------------------------------------------------------------------------
/**
 * Detect pyproject.toml with unknown keys in [tool.ruff] or [tool.mypy] sections.
 */
export async function checkPyprojectSchema(projectPath) {
    const findings = [];
    const content = await readFileSafe(join(projectPath, 'pyproject.toml'));
    if (!content) {
        return findings;
    }
    // Check [tool.ruff] section for unknown sub-sections
    const sections = extractTomlSections(content);
    const ruffSections = sections.filter((s) => s.startsWith('tool.ruff'));
    for (const section of ruffSections) {
        const subSection = section.replace('tool.ruff', '').replace(/^\./, '');
        if (!subSection) {
            continue;
        } // [tool.ruff] itself is valid
        const validSections = healthSchemas.ruff.validSections;
        if (!validSections.includes(subSection)) {
            findings.push({
                severity: 'warning',
                checker: 'pyproject-schema',
                file: 'pyproject.toml',
                message: `[tool.ruff.${subSection}] is not a known ruff section`,
                fix: `Valid ruff sections: ${validSections.join(', ')}. Check the ruff documentation for the correct section name`,
            });
        }
    }
    // Check [tool.mypy] for unknown keys
    const mypyKeys = extractTomlSection(content, 'tool.mypy');
    const validMypyOptions = healthSchemas.mypy.validOptions;
    for (const key of mypyKeys) {
        if (!validMypyOptions.includes(key)) {
            findings.push({
                severity: 'warning',
                checker: 'pyproject-schema',
                file: 'pyproject.toml',
                message: `'${key}' is not a recognized mypy option in [tool.mypy]`,
                fix: `Check the mypy documentation for valid options. Common valid options: ${validMypyOptions.slice(0, 5).join(', ')}`,
            });
        }
    }
    return findings;
}
// ---------------------------------------------------------------------------
// Check: mypy.ini / pyrightconfig.json invalid options
// ---------------------------------------------------------------------------
/**
 * Detect invalid options in mypy.ini or pyrightconfig.json.
 */
export async function checkMypyConfig(projectPath) {
    const findings = [];
    // Check mypy.ini
    const mypyIniContent = await readFileSafe(join(projectPath, 'mypy.ini'));
    if (mypyIniContent) {
        const validOptions = healthSchemas.mypy.validOptions;
        const lines = mypyIniContent.split('\n');
        let inMypySection = false;
        for (let i = 0; i < lines.length; i++) {
            const line = lines[i]?.trim() ?? '';
            if (line === '[mypy]') {
                inMypySection = true;
                continue;
            }
            if (line.startsWith('[') && line !== '[mypy]') {
                inMypySection = false;
            }
            if (!inMypySection || !line || line.startsWith('#') || line.startsWith(';')) {
                continue;
            }
            const keyMatch = /^([\w-]+)\s*=/.exec(line);
            if (keyMatch?.[1] && !validOptions.includes(keyMatch[1])) {
                findings.push({
                    severity: 'warning',
                    checker: 'mypy-config',
                    file: 'mypy.ini',
                    line: i + 1,
                    message: `'${keyMatch[1]}' is not a recognized mypy option`,
                    fix: `Remove or fix '${keyMatch[1]}' in mypy.ini — see mypy documentation for valid options`,
                });
            }
        }
    }
    // Check pyrightconfig.json
    const pyrightContent = await readFileSafe(join(projectPath, 'pyrightconfig.json'));
    if (pyrightContent) {
        let pyrightConfig;
        try {
            pyrightConfig = JSON.parse(pyrightContent);
        }
        catch {
            findings.push({
                severity: 'error',
                checker: 'mypy-config',
                file: 'pyrightconfig.json',
                message: 'pyrightconfig.json is not valid JSON',
                fix: 'Fix the JSON syntax in pyrightconfig.json',
            });
            return findings;
        }
        const knownPyrightKeys = [
            'include',
            'exclude',
            'ignore',
            'strict',
            'pythonVersion',
            'pythonPlatform',
            'typeshedPath',
            'stubPath',
            'venvPath',
            'venv',
            'reportMissingImports',
            'reportMissingTypeStubs',
            'useLibraryCodeForTypes',
            'typeCheckingMode',
        ];
        for (const key of Object.keys(pyrightConfig)) {
            if (!knownPyrightKeys.includes(key)) {
                findings.push({
                    severity: 'warning',
                    checker: 'mypy-config',
                    file: 'pyrightconfig.json',
                    message: `'${key}' is not a recognized pyrightconfig.json option`,
                    fix: `Check the Pyright documentation for valid options. Remove unknown key '${key}' to avoid silent misconfiguration`,
                });
            }
        }
    }
    return findings;
}
// ---------------------------------------------------------------------------
// Check: pyproject.toml [project.scripts] referencing non-existent modules
// ---------------------------------------------------------------------------
/**
 * Detect [project.scripts] entries that reference Python modules/functions
 * that don't appear to exist as .py files in the project.
 */
export async function checkPythonScripts(projectPath) {
    const findings = [];
    const content = await readFileSafe(join(projectPath, 'pyproject.toml'));
    if (!content) {
        return findings;
    }
    const scriptKeys = extractTomlSection(content, 'project.scripts');
    if (scriptKeys.length === 0) {
        return findings;
    }
    // Extract script values (module:function format)
    const lines = content.split('\n');
    const scriptSection = {};
    let inScriptsSection = false;
    for (const line of lines) {
        const trimmed = line.trim();
        if (trimmed === '[project.scripts]') {
            inScriptsSection = true;
            continue;
        }
        if (inScriptsSection && trimmed.startsWith('[')) {
            break;
        }
        if (inScriptsSection) {
            const match = /^([\w-]+)\s*=\s*"([^"]+)"/.exec(trimmed);
            if (match?.[1] && match[2]) {
                scriptSection[match[1]] = match[2];
            }
        }
    }
    for (const [scriptName, moduleRef] of Object.entries(scriptSection)) {
        // module:function format, e.g. "mypackage.cli:main"
        const modulePart = moduleRef.split(':')[0] ?? moduleRef;
        // Convert module path to file path (mypackage.cli -> mypackage/cli.py)
        const filePath = modulePart.replace(/\./g, '/') + '.py';
        const fullPath = join(projectPath, filePath);
        const exists = await readFileSafe(fullPath);
        if (!exists) {
            findings.push({
                severity: 'warning',
                checker: 'pyproject-schema',
                file: 'pyproject.toml',
                message: `Script '${scriptName}' references module '${modulePart}' but '${filePath}' does not exist`,
                fix: `Create '${filePath}' or fix the module reference for script '${scriptName}'`,
            });
        }
    }
    return findings;
}
// ---------------------------------------------------------------------------
// Check: requirements.txt version conflicts
// ---------------------------------------------------------------------------
/**
 * Detect obviously incompatible version constraints in requirements.txt.
 * Uses basic heuristics (same package pinned to different versions).
 */
export async function checkRequirementsConflicts(projectPath) {
    const findings = [];
    const reqFiles = ['requirements.txt', 'requirements-dev.txt', 'requirements-test.txt'];
    const packageVersions = new Map();
    for (const reqFile of reqFiles) {
        const content = await readFileSafe(join(projectPath, reqFile));
        if (!content) {
            continue;
        }
        const lines = content.split('\n');
        for (const line of lines) {
            const trimmed = line.trim();
            if (!trimmed || trimmed.startsWith('#')) {
                continue;
            }
            // Parse "package==1.0.0" or "package>=1.0.0"
            const match = /^([\w-]+)\s*(==|>=|<=|~=|!=)\s*([\d.]+)/.exec(trimmed);
            if (match?.[1] && match[2] && match[3]) {
                const pkgName = match[1].toLowerCase();
                const constraint = `${match[2]}${match[3]}`;
                const existing = packageVersions.get(pkgName) ?? [];
                packageVersions.set(pkgName, [...existing, `${reqFile}: ${constraint}`]);
            }
        }
    }
    // Detect packages pinned to exact different versions across files
    for (const [pkgName, constraints] of packageVersions) {
        if (constraints.length < 2) {
            continue;
        }
        const exactVersions = constraints
            .filter((c) => c.includes('=='))
            .map((c) => c.split('==')[1]?.trim() ?? '');
        const uniqueVersions = [...new Set(exactVersions)];
        if (uniqueVersions.length > 1) {
            findings.push({
                severity: 'error',
                checker: 'pyproject-schema',
                file: 'requirements.txt',
                message: `Package '${pkgName}' is pinned to different exact versions: ${constraints.join(', ')}`,
                fix: `Consolidate '${pkgName}' to a single version constraint across all requirements files`,
            });
        }
    }
    return findings;
}
//# sourceMappingURL=python-checker.js.map