/**
 * Auto-Fix Capabilities for Common Patterns
 *
 * Provides automatic fix suggestions and applications for common
 * code quality issues detected during certification.
 *
 * @module certification/autofix
 */
import { readFile, writeFile } from "fs/promises";
import { join } from "path";
import { logger } from "../logger.js";
/**
 * Built-in fix patterns for common issues
 */
export const FIX_PATTERNS = [
    // Performance: async forEach antipattern
    {
        patternId: "perf-async-foreach",
        name: "Fix async forEach",
        description: "Convert array.forEach(async ...) to Promise.all(array.map(async ...))",
        matchPattern: /(\w+)\.forEach\(\s*async\s*\(([^)]*)\)\s*=>\s*\{/g,
        replacement: "await Promise.all($1.map(async ($2) => {",
        safeToAutoApply: false,
        risk: "medium",
    },
    // TypeScript: unsafe type assertion
    {
        patternId: "type-unsafe-assertion",
        name: "Fix unsafe type assertion",
        description: "Replace 'as unknown as T' with proper type guard or optional chaining",
        matchPattern: /as\s+unknown\s+as\s+(\w+)/g,
        replacement: "?? (undefined as $1 | undefined)",
        safeToAutoApply: false,
        risk: "high",
    },
    // Security: hardcoded secrets pattern
    {
        patternId: "sec-hardcoded-secret",
        name: "Extract hardcoded secret",
        description: "Replace hardcoded secret with environment variable reference",
        matchPattern: /(password|secret|apiKey|api_key|token)\s*[:=]\s*["']([^"']+)["']/gi,
        replacement: "$1: process.env.$1_VALUE",
        safeToAutoApply: false,
        risk: "high",
    },
    // Reliability: missing error handling
    {
        patternId: "rel-missing-try-catch",
        name: "Add try-catch wrapper",
        description: "Wrap async function body in try-catch block",
        matchPattern: /async\s+function\s+(\w+)\s*\([^)]*\)\s*\{([^}]+)\}/g,
        replacement: (match, name, body) => `async function ${name}() {\n  try {\n    ${body.trim()}\n  } catch (error) {\n    console.error('Error in ${name}:', error);\n    throw error;\n  }\n}`,
        safeToAutoApply: false,
        risk: "medium",
    },
    // Quality: console.log statements
    {
        patternId: "qual-console-log",
        name: "Remove console.log",
        description: "Remove or replace console.log with proper logging",
        matchPattern: /console\.log\([^)]*\);?\n?/g,
        replacement: "",
        safeToAutoApply: true,
        risk: "low",
    },
    // Performance: unnecessary await in return
    {
        patternId: "perf-unnecessary-await",
        name: "Remove unnecessary await",
        description: "Remove await on direct return statements",
        matchPattern: /return\s+await\s+/g,
        replacement: "return ",
        safeToAutoApply: true,
        risk: "low",
    },
    // Security: IDOR - missing authorization check
    {
        patternId: "sec-idor-missing-auth",
        name: "Add authorization check",
        description: "Add user ownership verification before resource access",
        matchPattern: /const\s+(\w+)\s*=\s*await\s+(db|prisma|supabase)\.(\w+)\.find\w*\(\s*\{\s*where:\s*\{\s*id:\s*(\w+)/g,
        replacement: `const $1 = await $2.$3.findFirst({
  where: {
    id: $4,
    userId: session.user.id, // SECURITY: Added ownership check`,
        safeToAutoApply: false,
        risk: "high",
    },
    // Security: Missing rate limiting on API route
    {
        patternId: "sec-missing-rate-limit",
        name: "Add rate limiting",
        description: "Add rate limiting middleware to API endpoint",
        matchPattern: /export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/g,
        replacement: `// SECURITY: Add rate limiting - npm install @upstash/ratelimit @upstash/redis
// import { Ratelimit } from "@upstash/ratelimit";
// const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, "10 s") });
export $1function $2(`,
        safeToAutoApply: false,
        risk: "medium",
    },
    // Security: Missing CSRF protection
    {
        patternId: "sec-missing-csrf",
        name: "Add CSRF token validation",
        description: "Add CSRF token check for state-changing operations",
        matchPattern: /export\s+async\s+function\s+(POST|PUT|DELETE|PATCH)\s*\(\s*request:\s*Request/g,
        replacement: `export async function $1(request: Request) {
  // SECURITY: Validate CSRF token
  const csrfToken = request.headers.get("x-csrf-token");
  if (!csrfToken || !validateCsrfToken(csrfToken)) {
    return new Response("Invalid CSRF token", { status: 403 });
  }

  // Original handler
  return (async function $1_handler(request: Request`,
        safeToAutoApply: false,
        risk: "high",
    },
    // Security: Insecure cookie settings
    {
        patternId: "sec-insecure-cookie",
        name: "Fix insecure cookie",
        description: "Add secure, httpOnly, and sameSite attributes to cookies",
        matchPattern: /\.set\s*\(\s*["']([^"']+)["']\s*,\s*([^,)]+)\s*\)/g,
        replacement: `.set("$1", $2, { httpOnly: true, secure: true, sameSite: "strict" })`,
        safeToAutoApply: false,
        risk: "medium",
    },
    // Security: SQL injection - string concatenation
    {
        patternId: "sec-sql-injection",
        name: "Fix SQL injection",
        description: "Replace string concatenation with parameterized query",
        matchPattern: /`SELECT\s+\*\s+FROM\s+(\w+)\s+WHERE\s+(\w+)\s*=\s*'\$\{(\w+)\}'`/g,
        replacement: "{ text: 'SELECT * FROM $1 WHERE $2 = $1', values: [$3] }",
        safeToAutoApply: false,
        risk: "high",
    },
    // Security: XSS - innerHTML usage
    {
        patternId: "sec-xss-innerhtml",
        name: "Fix innerHTML XSS",
        description: "Replace innerHTML with textContent or sanitized HTML",
        matchPattern: /\.innerHTML\s*=\s*(\w+)/g,
        replacement: ".textContent = $1 // SECURITY: Changed from innerHTML to prevent XSS",
        safeToAutoApply: false,
        risk: "high",
    },
    // TypeScript: explicit any
    {
        patternId: "type-explicit-any",
        name: "Replace explicit any",
        description: "Replace 'any' with 'unknown' for safer typing",
        matchPattern: /:\s*any\b/g,
        replacement: ": unknown",
        safeToAutoApply: false,
        risk: "medium",
    },
    // Reliability: unhandled promise rejection
    {
        patternId: "rel-unhandled-promise",
        name: "Add promise error handling",
        description: "Add .catch() to unhandled promise",
        matchPattern: /(\w+)\.(then)\s*\(\s*([^)]+)\s*\)\s*(?!\.catch)/g,
        replacement: "$1.then($3).catch((err) => console.error('Unhandled promise rejection:', err))",
        safeToAutoApply: false,
        risk: "medium",
    },
    // Security: Missing input validation
    {
        patternId: "sec-missing-validation",
        name: "Add input validation",
        description: "Add Zod schema validation for request body",
        matchPattern: /const\s+(\w+)\s*=\s*await\s+request\.json\(\)/g,
        replacement: `// SECURITY: Add input validation
// const schema = z.object({ /* define your schema */ });
const rawBody = await request.json();
// const $1 = schema.parse(rawBody);
const $1 = rawBody // TODO: Add validation`,
        safeToAutoApply: false,
        risk: "high",
    },
];
/**
 * Find the fix pattern for a given finding
 */
export function findFixPattern(finding) {
    // First try exact ID match
    const idPattern = FIX_PATTERNS.find((p) => finding.id.includes(p.patternId) || finding.category.includes(p.patternId));
    if (idPattern)
        return idPattern;
    // Try category-based matching
    const categoryPatterns = {
        "async-foreach": ["perf-async-foreach"],
        "type-assertion": ["type-unsafe-assertion"],
        "hardcoded": ["sec-hardcoded-secret"],
        "console": ["qual-console-log"],
    };
    const category = finding.category.toLowerCase();
    for (const [keyword, patternIds] of Object.entries(categoryPatterns)) {
        if (category.includes(keyword)) {
            const pattern = FIX_PATTERNS.find((p) => patternIds.includes(p.patternId));
            if (pattern)
                return pattern;
        }
    }
    return undefined;
}
/**
 * Generate a preview of the fix without applying it
 */
export async function previewFix(projectPath, finding) {
    const pattern = findFixPattern(finding);
    if (!pattern) {
        return {
            findingId: finding.id,
            canAutoFix: false,
            risk: "high",
            requiresReview: true,
        };
    }
    if (!finding.file) {
        return {
            findingId: finding.id,
            canAutoFix: false,
            pattern: pattern.name,
            fixDescription: pattern.description,
            risk: pattern.risk,
            requiresReview: true,
        };
    }
    try {
        const filePath = join(projectPath, finding.file);
        const content = await readFile(filePath, "utf-8");
        const lines = content.split("\n");
        // Find the matching line
        const lineIndex = (finding.line || 1) - 1;
        const contextStart = Math.max(0, lineIndex - 2);
        const contextEnd = Math.min(lines.length, lineIndex + 3);
        const contextLines = lines.slice(contextStart, contextEnd);
        const contextBefore = contextLines.join("\n");
        // Apply the pattern to the context
        const replacement = typeof pattern.replacement === "function"
            ? contextBefore.replace(pattern.matchPattern, pattern.replacement)
            : contextBefore.replace(pattern.matchPattern, pattern.replacement);
        if (replacement === contextBefore) {
            return {
                findingId: finding.id,
                canAutoFix: false,
                pattern: pattern.name,
                fixDescription: "Pattern not found in specified location",
                risk: pattern.risk,
                requiresReview: true,
            };
        }
        return {
            findingId: finding.id,
            canAutoFix: true,
            pattern: pattern.name,
            fixDescription: pattern.description,
            preview: {
                before: contextBefore,
                after: replacement,
                file: finding.file,
                lineStart: contextStart + 1,
                lineEnd: contextEnd,
            },
            risk: pattern.risk,
            requiresReview: !pattern.safeToAutoApply,
        };
    }
    catch (error) {
        logger.warn("autofix.preview_failed", {
            findingId: finding.id,
            error: String(error),
        });
        return {
            findingId: finding.id,
            canAutoFix: false,
            pattern: pattern.name,
            fixDescription: `Error reading file: ${error}`,
            risk: "high",
            requiresReview: true,
        };
    }
}
/**
 * Apply a fix to a file
 */
export async function applyFix(projectPath, finding, dryRun = false) {
    const pattern = findFixPattern(finding);
    if (!pattern) {
        return {
            findingId: finding.id,
            file: finding.file || "unknown",
            applied: false,
            error: "No auto-fix pattern available for this finding",
        };
    }
    if (!finding.file) {
        return {
            findingId: finding.id,
            file: "unknown",
            applied: false,
            error: "Finding does not specify a file location",
        };
    }
    try {
        const filePath = join(projectPath, finding.file);
        const content = await readFile(filePath, "utf-8");
        // Apply the fix
        const replacement = typeof pattern.replacement === "function"
            ? content.replace(pattern.matchPattern, pattern.replacement)
            : content.replace(pattern.matchPattern, pattern.replacement);
        if (replacement === content) {
            return {
                findingId: finding.id,
                file: finding.file,
                applied: false,
                error: "Pattern not found in file",
            };
        }
        // Calculate diff for the changed lines
        const beforeLines = content.split("\n");
        const afterLines = replacement.split("\n");
        let diffStart = 0;
        let diffEnd = beforeLines.length;
        for (let i = 0; i < Math.min(beforeLines.length, afterLines.length); i++) {
            if (beforeLines[i] !== afterLines[i]) {
                diffStart = i;
                break;
            }
        }
        for (let i = 0; i < Math.min(beforeLines.length, afterLines.length); i++) {
            if (beforeLines[beforeLines.length - 1 - i] !== afterLines[afterLines.length - 1 - i]) {
                diffEnd = beforeLines.length - i;
                break;
            }
        }
        const diff = {
            before: beforeLines.slice(diffStart, diffEnd).join("\n"),
            after: afterLines.slice(diffStart, Math.min(diffEnd + 1, afterLines.length)).join("\n"),
            lineNumber: diffStart + 1,
        };
        if (!dryRun) {
            await writeFile(filePath, replacement, "utf-8");
            logger.info("autofix.applied", {
                findingId: finding.id,
                file: finding.file,
                pattern: pattern.name,
            });
        }
        return {
            findingId: finding.id,
            file: finding.file,
            applied: !dryRun,
            diff,
        };
    }
    catch (error) {
        logger.error("autofix.failed", {
            findingId: finding.id,
            error: String(error),
        });
        return {
            findingId: finding.id,
            file: finding.file,
            applied: false,
            error: String(error),
        };
    }
}
/**
 * Get all available fix patterns with their metadata
 */
export function listFixPatterns() {
    return FIX_PATTERNS.map((p) => ({
        id: p.patternId,
        name: p.name,
        description: p.description,
        risk: p.risk,
        safeToAutoApply: p.safeToAutoApply,
    }));
}
/**
 * Add a custom fix pattern at runtime
 */
export function registerFixPattern(pattern) {
    // Check for duplicate
    const existing = FIX_PATTERNS.findIndex((p) => p.patternId === pattern.patternId);
    if (existing >= 0) {
        FIX_PATTERNS[existing] = pattern;
    }
    else {
        FIX_PATTERNS.push(pattern);
    }
    logger.debug("autofix.pattern_registered", { patternId: pattern.patternId });
}
/**
 * Apply all safe fixes to a list of findings
 * Only applies fixes marked as safeToAutoApply: true
 */
export async function batchApplySafeFixes(projectPath, findings, options) {
    const dryRun = options?.dryRun ?? false;
    const includeUnsafe = options?.includeUnsafe ?? false;
    const safePatterns = FIX_PATTERNS.filter((p) => p.safeToAutoApply || includeUnsafe);
    const safePatternIds = new Set(safePatterns.map((p) => p.patternId));
    const results = [];
    const byPattern = {};
    const byFile = {};
    let applied = 0;
    let skipped = 0;
    let failed = 0;
    let safeFixesAvailable = 0;
    for (const finding of findings) {
        const pattern = findFixPattern(finding);
        if (!pattern) {
            skipped++;
            continue;
        }
        // Check if pattern is safe (or includeUnsafe is true)
        const patternIsSafe = safePatternIds.has(pattern.patternId);
        if (!patternIsSafe && !includeUnsafe) {
            skipped++;
            continue;
        }
        safeFixesAvailable++;
        const result = await applyFix(projectPath, finding, dryRun);
        results.push(result);
        if (result.applied || (dryRun && result.diff)) {
            applied++;
            byPattern[pattern.patternId] = (byPattern[pattern.patternId] || 0) + 1;
            if (finding.file) {
                byFile[finding.file] = (byFile[finding.file] || 0) + 1;
            }
        }
        else if (result.error) {
            failed++;
        }
        else {
            skipped++;
        }
    }
    logger.info("autofix.batch_complete", {
        totalFindings: findings.length,
        safeFixesAvailable,
        applied,
        skipped,
        failed,
        dryRun,
    });
    return {
        totalFindings: findings.length,
        safeFixesAvailable,
        applied,
        skipped,
        failed,
        results,
        summary: {
            byPattern,
            byFile,
        },
    };
}
/**
 * Get safe fix patterns only
 */
export function getSafeFixPatterns() {
    return FIX_PATTERNS.filter((p) => p.safeToAutoApply).map((p) => ({
        id: p.patternId,
        name: p.name,
        description: p.description,
    }));
}
//# sourceMappingURL=autofix.js.map