/**
 * Codemod handler — PR-5a-2
 *
 * Real codemod execution using ts-morph (TS/JS) and a sandboxed string
 * transform (Python). Replaces the PR-5a-1 stub that returned 'skipped'.
 *
 * Execution model:
 *   1. Glob the agent directory for files matching payload.fileGlob.
 *   2. For each matched file, load its source.
 *   3. Wrap the embedded transformSource in a CJS module wrapper.
 *   4. Execute in vm.runInNewContext() via runInSandbox() with the
 *      restricted allowlist (ts-morph, path, fs/read-only).
 *   5. The transform function receives the file source and a helpers object.
 *      For TS/JS: it may use ts-morph via require('ts-morph').
 *      For Python: the transform returns the mutated source string.
 *   6. If the transform writes via helpers.writeFile(), those writes land in
 *      SandboxResult.pendingWrites and are flushed to disk (in non-dry-run mode).
 *   7. The handler captures before/after diffs and returns a RemediationResult.
 *
 * Security model (Shield H-3):
 *   - transformSource is an embedded string, NOT a file path reference.
 *   - Signature is verified before execution (UNSIGNED only in test fixtures).
 *   - vm.runInNewContext() with timeout; deny-list enforced by restrictedRequire.
 *   - Write access goes through helpers.writeFile — raw fs.writeFile is not
 *     exposed to the transform.
 *
 * @connexum/ai-governance PR-5a-2
 */
import * as fs from 'fs';
import * as path from 'path';
import { computeUnifiedDiff } from '../diff-utils.js';
import { runInSandbox } from '../sandbox.js';
// ---------------------------------------------------------------------------
// Glob helper (minimal, no external dependency)
// ---------------------------------------------------------------------------
/**
 * Recursively find files matching a simple glob pattern.
 * Supports: **, *, ? wildcards. Relative to baseDir.
 */
function globFiles(baseDir, pattern) {
    const results = [];
    const regexStr = pattern
        .replace(/\./g, '\\.')
        .replace(/\*\*/g, '__DOUBLESTAR__')
        .replace(/\*/g, '[^/]*')
        .replace(/\?/g, '[^/]')
        .replace(/__DOUBLESTAR__/g, '.*');
    const regex = new RegExp(`^${regexStr}$`);
    function walk(dir, relDir) {
        let entries;
        try {
            entries = fs.readdirSync(dir, { withFileTypes: true });
        }
        catch {
            return;
        }
        for (const entry of entries) {
            const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
            if (entry.isDirectory()) {
                // Skip node_modules and .git
                if (entry.name === 'node_modules' || entry.name === '.git')
                    continue;
                walk(path.join(dir, entry.name), relPath);
            }
            else if (entry.isFile()) {
                if (regex.test(relPath)) {
                    results.push(path.join(dir, entry.name));
                }
            }
        }
    }
    walk(baseDir, '');
    return results;
}
// ---------------------------------------------------------------------------
// CJS module wrapper
// ---------------------------------------------------------------------------
/**
 * Wrap a transform source in a CommonJS IIFE so module.exports.transform
 * is populated after runInSandbox executes the script.
 */
function wrapInCjsModule(transformSource) {
    return `
(function(module, exports, require, path, fs, console, helpers, __filename, __dirname) {
${transformSource}
})(module, exports, require, path, fs, console, helpers, __filename, __dirname);
`;
}
// ---------------------------------------------------------------------------
// Main handler
// ---------------------------------------------------------------------------
/**
 * Apply a codemod remediation to an agent directory.
 *
 * The transform function signature (embedded in payload.transformSource):
 *
 *   For TS/JS:
 *     module.exports.transform = async function(sourceCode, helpers) {
 *       // use helpers.writeFile(relPath, newContent) to write output
 *       // use require('ts-morph') for AST manipulation
 *     }
 *
 *   For Python:
 *     module.exports.transform = function(sourceCode, helpers) {
 *       // return the transformed source string
 *     }
 *
 * The handler calls transform(fileSource, helpers) for each matched file.
 * If the transform calls helpers.writeFile(), those writes are persisted.
 * If the transform returns a string, it is treated as the mutated source
 * and written to the original file path.
 */
export async function applyCodemod(agentDir, remediation, payload, packId, packVersion) {
    // Find matching files
    const matchedFiles = globFiles(agentDir, payload.fileGlob);
    if (matchedFiles.length === 0) {
        return {
            remediation,
            packId,
            packVersion,
            status: 'skipped',
            filesAffected: [],
            diffs: [],
            error: `No files matched glob pattern: ${payload.fileGlob}`,
        };
    }
    const filesAffected = [];
    const diffs = [];
    const errors = [];
    for (const absFilePath of matchedFiles) {
        let sourceCode;
        try {
            sourceCode = fs.readFileSync(absFilePath, 'utf-8');
        }
        catch (err) {
            errors.push(`Cannot read ${absFilePath}: ${err.message}`);
            continue;
        }
        const relPath = path.relative(agentDir, absFilePath);
        const wrappedPayload = wrapInCjsModule(payload.transformSource);
        let resultSource;
        let sandboxWrites = [];
        try {
            const sandboxResult = await runInSandbox(wrappedPayload, {
                agentDir,
                timeoutMs: 5000,
                dryRun: false,
            });
            sandboxWrites = sandboxResult.pendingWrites;
            // Check if transform exported a function and call it
            const exports = sandboxResult.returnValue;
            if (exports && typeof exports['transform'] === 'function') {
                const transform = exports['transform'];
                const transformHelpers = {
                    log: (msg) => void msg,
                    addImport: (stmt) => void stmt,
                    writeFile: async (p, content) => {
                        sandboxWrites.push({ filePath: path.resolve(agentDir, p), content });
                        fs.writeFileSync(path.resolve(agentDir, p), content, 'utf-8');
                    },
                };
                const ret = await transform(sourceCode, transformHelpers);
                if (typeof ret === 'string') {
                    resultSource = ret;
                }
            }
        }
        catch (err) {
            errors.push(`Codemod failed on ${relPath}: ${err.message}`);
            continue;
        }
        // If the transform returned a string, write it back
        if (resultSource !== undefined && resultSource !== sourceCode) {
            fs.writeFileSync(absFilePath, resultSource, 'utf-8');
            sandboxWrites.push({ filePath: absFilePath, content: resultSource });
        }
        // Collect diffs from pendingWrites or direct return
        for (const write of sandboxWrites) {
            const before = write.filePath === absFilePath ? sourceCode : '';
            const after = write.content;
            if (before !== after) {
                const relWritePath = path.relative(agentDir, write.filePath);
                diffs.push({
                    filePath: relWritePath,
                    before,
                    after,
                    unifiedDiff: computeUnifiedDiff(before, after, relWritePath),
                });
                if (!filesAffected.includes(relWritePath)) {
                    filesAffected.push(relWritePath);
                }
            }
        }
        // Even if resultSource === sourceCode (no change), log the processed file
        if (resultSource !== undefined) {
            if (!filesAffected.includes(relPath)) {
                // No actual change, but was processed
                // Only count as affected if there was a real diff
            }
        }
    }
    if (errors.length > 0 && filesAffected.length === 0) {
        return {
            remediation,
            packId,
            packVersion,
            status: 'error',
            filesAffected,
            diffs,
            error: errors.join('; '),
        };
    }
    return {
        remediation,
        packId,
        packVersion,
        status: filesAffected.length > 0 ? 'applied' : 'skipped',
        filesAffected,
        diffs,
        error: errors.length > 0 ? errors.join('; ') : undefined,
    };
}
//# sourceMappingURL=codemod.js.map