/**
 * Differential PR Mode
 *
 * Enhanced diff mode for incremental scanning with:
 * - Git-based change detection (git diff --name-only)
 * - Integration with scanner cache
 * - Support for PR base SHA comparison
 * - Incremental certification support
 *
 * @module action/diff-mode
 */
import { exec } from "child_process";
import { promisify } from "util";
import { join, extname } from "path";
import { stat, readFile } from "fs/promises";
import { createHash } from "crypto";
import { logger } from "../logger.js";
const execAsync = promisify(exec);
/**
 * File extensions to include in scans
 */
const SCANNABLE_EXTENSIONS = new Set([
    ".ts",
    ".tsx",
    ".js",
    ".jsx",
    ".mjs",
    ".cjs",
    ".json",
    ".yaml",
    ".yml",
    ".sql",
    ".py",
    ".rb",
    ".go",
    ".java",
    ".kt",
    ".swift",
    ".rs",
    ".php",
    ".cs",
    ".vue",
    ".svelte",
    ".tf", // Terraform
    ".dockerfile", // Docker
]);
/**
 * Paths to exclude from scanning
 */
const EXCLUDED_PATHS = [
    "node_modules/",
    "dist/",
    "build/",
    "coverage/",
    ".git/",
    ".vaspera/",
    ".next/",
    ".turbo/",
    "__pycache__/",
    "vendor/",
    "venv/",
    ".venv/",
];
/**
 * Get changed files using git diff
 *
 * Uses `git diff --name-only $BASE_SHA...HEAD` for accurate change detection.
 *
 * @param projectPath - Path to git repository
 * @param baseSha - Base commit SHA (e.g., PR base branch)
 * @param headSha - Head commit SHA (default: HEAD)
 */
export async function getGitDiff(projectPath, baseSha, headSha = "HEAD") {
    const result = {
        changedFiles: [],
        baseSha: baseSha || "",
        headSha,
        success: false,
        additions: 0,
        deletions: 0,
    };
    try {
        // Get base SHA if not provided (use default branch)
        if (!baseSha) {
            try {
                const { stdout: defaultBranch } = await execAsync('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed "s@^refs/remotes/origin/@@" || echo "main"', { cwd: projectPath, timeout: 10000 });
                baseSha = `origin/${defaultBranch.trim()}`;
                result.baseSha = baseSha;
            }
            catch {
                baseSha = "HEAD^";
                result.baseSha = baseSha;
            }
        }
        // Get list of changed files
        const { stdout: diffOutput } = await execAsync(`git diff --name-only ${baseSha}...${headSha} 2>/dev/null || git diff --name-only ${baseSha} ${headSha}`, { cwd: projectPath, timeout: 30000 });
        result.changedFiles = diffOutput
            .trim()
            .split("\n")
            .filter((f) => f.length > 0);
        // Get stats (additions/deletions)
        try {
            const { stdout: statsOutput } = await execAsync(`git diff --shortstat ${baseSha}...${headSha} 2>/dev/null || git diff --shortstat ${baseSha} ${headSha}`, { cwd: projectPath, timeout: 10000 });
            const addMatch = statsOutput.match(/(\d+) insertions?/);
            const delMatch = statsOutput.match(/(\d+) deletions?/);
            result.additions = addMatch ? parseInt(addMatch[1], 10) : 0;
            result.deletions = delMatch ? parseInt(delMatch[1], 10) : 0;
        }
        catch {
            // Stats are optional
        }
        // Get actual head SHA
        try {
            const { stdout: actualHead } = await execAsync(`git rev-parse ${headSha}`, { cwd: projectPath, timeout: 5000 });
            result.headSha = actualHead.trim();
        }
        catch {
            // Keep original headSha
        }
        result.success = true;
        logger.info("diff_mode.git_diff", {
            baseSha: result.baseSha,
            headSha: result.headSha,
            changedFiles: result.changedFiles.length,
            additions: result.additions,
            deletions: result.deletions,
        });
        return result;
    }
    catch (error) {
        result.error = String(error);
        logger.warn("diff_mode.git_diff_failed", { error: result.error });
        return result;
    }
}
/**
 * Get file info with content hash
 */
export async function getFileInfo(projectPath, filePath) {
    const fullPath = join(projectPath, filePath);
    try {
        const [fileStats, content] = await Promise.all([
            stat(fullPath),
            readFile(fullPath),
        ]);
        return {
            path: filePath,
            hash: createHash("sha256").update(content).digest("hex"),
            size: fileStats.size,
            mtime: fileStats.mtime,
        };
    }
    catch {
        return null;
    }
}
/**
 * Get file info for multiple files in parallel
 */
export async function getFilesInfo(projectPath, filePaths) {
    const results = await Promise.all(filePaths.map((path) => getFileInfo(projectPath, path)));
    return results.filter((f) => f !== null);
}
/**
 * Check if a file should be scanned
 */
export function isScannableFile(filename) {
    // Check excluded paths
    for (const excluded of EXCLUDED_PATHS) {
        if (filename.includes(excluded)) {
            return false;
        }
    }
    // Check extension
    const ext = extname(filename).toLowerCase();
    if (!SCANNABLE_EXTENSIONS.has(ext)) {
        // Special cases
        if (filename.endsWith("Dockerfile"))
            return true;
        if (filename === "docker-compose.yml" || filename === "docker-compose.yaml")
            return true;
        return false;
    }
    // Exclude minified/bundled files
    if (filename.endsWith(".min.js") || filename.endsWith(".min.css"))
        return false;
    if (filename.endsWith(".bundle.js"))
        return false;
    return true;
}
/**
 * Filter changed files to only scannable ones
 */
export function filterScannableFiles(files) {
    return files.filter((file) => {
        // Exclude deleted files
        if (file.status === "removed")
            return false;
        return isScannableFile(file.filename);
    });
}
/**
 * Get the list of file paths to scan
 */
export function getFilesToScan(changedFiles) {
    return filterScannableFiles(changedFiles).map((f) => f.filename);
}
/**
 * Configure incremental scanning
 *
 * Determines which files need scanning based on:
 * 1. Git diff against base SHA
 * 2. File content hashes (for cache validation)
 * 3. Security-critical file detection
 */
export async function configureIncrementalScan(config) {
    const { projectPath, baseSha, headSha, forceInclude, exclude, alwaysScanSecurityFiles } = config;
    // Get git diff
    const diff = await getGitDiff(projectPath, baseSha, headSha);
    // Filter to scannable files
    const scannableChanged = diff.changedFiles.filter(isScannableFile);
    // Apply exclusions
    let filesToProcess = scannableChanged;
    if (exclude && exclude.length > 0) {
        const excludePatterns = exclude.map((p) => new RegExp(p));
        filesToProcess = filesToProcess.filter((f) => !excludePatterns.some((pattern) => pattern.test(f)));
    }
    // Add force-included files
    if (forceInclude && forceInclude.length > 0) {
        const additional = forceInclude.filter((f) => !filesToProcess.includes(f) && isScannableFile(f));
        filesToProcess = [...filesToProcess, ...additional];
    }
    // Add security-critical files if requested
    if (alwaysScanSecurityFiles) {
        const securityFiles = await findSecurityCriticalFiles(projectPath);
        const additional = securityFiles.filter((f) => !filesToProcess.includes(f));
        filesToProcess = [...filesToProcess, ...additional];
    }
    // Get file info (with hashes)
    const fileInfos = await getFilesInfo(projectPath, filesToProcess);
    // Calculate removed files
    const removedFiles = diff.changedFiles.filter((f) => {
        // Try to stat the file - if it fails, it's removed
        return !fileInfos.some((fi) => fi.path === f);
    });
    // Estimate savings
    const totalProjectFiles = await estimateTotalFiles(projectPath);
    const estimatedSavingsPercent = totalProjectFiles > 0
        ? Math.round(((totalProjectFiles - fileInfos.length) / totalProjectFiles) * 100)
        : 0;
    const result = {
        filesToScan: fileInfos,
        unchangedFiles: [], // Will be populated by cache lookup
        removedFiles,
        diff,
        config,
        isIncremental: diff.success && fileInfos.length < totalProjectFiles,
        estimatedSavingsPercent,
    };
    logger.info("diff_mode.incremental_config", {
        filesToScan: fileInfos.length,
        removedFiles: removedFiles.length,
        isIncremental: result.isIncremental,
        estimatedSavingsPercent,
    });
    return result;
}
/**
 * Find security-critical files that should always be scanned
 */
async function findSecurityCriticalFiles(projectPath) {
    const patterns = [
        "**/auth/**/*.{ts,tsx,js,jsx}",
        "**/security/**/*.{ts,tsx,js,jsx}",
        "**/middleware/**/*.{ts,tsx,js,jsx}",
        "**/api/**/*.{ts,tsx,js,jsx}",
        "**/*auth*.{ts,tsx,js,jsx}",
        "**/*login*.{ts,tsx,js,jsx}",
        "**/*session*.{ts,tsx,js,jsx}",
        "**/*token*.{ts,tsx,js,jsx}",
        "**/*password*.{ts,tsx,js,jsx}",
        ".env.example",
        "package.json",
        "package-lock.json",
        "yarn.lock",
        "pnpm-lock.yaml",
    ];
    const files = [];
    try {
        // Use git ls-files to get tracked files matching patterns
        for (const pattern of patterns) {
            try {
                const { stdout } = await execAsync(`git ls-files "${pattern}" 2>/dev/null || true`, { cwd: projectPath, timeout: 5000 });
                const matches = stdout.trim().split("\n").filter((f) => f.length > 0);
                files.push(...matches);
            }
            catch {
                // Ignore pattern matching errors
            }
        }
    }
    catch {
        // Git ls-files failed, return empty
    }
    // Deduplicate
    return [...new Set(files)];
}
/**
 * Estimate total number of scannable files in project
 */
async function estimateTotalFiles(projectPath) {
    try {
        const { stdout } = await execAsync('git ls-files | grep -E "\\.(ts|tsx|js|jsx|py|rb|go|java|rs|sql|json|yaml|yml)$" | wc -l', { cwd: projectPath, timeout: 10000 });
        return parseInt(stdout.trim(), 10) || 0;
    }
    catch {
        return 0;
    }
}
// ============================================================================
// GitHub API Integration (preserved from original)
// ============================================================================
/**
 * Get changed files from GitHub API
 *
 * @param octokit - GitHub API client
 * @param context - GitHub context
 * @returns Array of changed files
 */
export async function getChangedFiles(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
octokit, context) {
    const changedFiles = [];
    if (context.pullRequest) {
        // PR event - get files from PR
        let page = 1;
        const perPage = 100;
        while (true) {
            const response = await octokit.rest.pulls.listFiles({
                owner: context.owner,
                repo: context.repo,
                pull_number: context.pullRequest.number,
                per_page: perPage,
                page,
            });
            for (const file of response.data) {
                changedFiles.push({
                    filename: file.filename,
                    status: file.status,
                    additions: file.additions,
                    deletions: file.deletions,
                    changes: file.changes,
                    previousFilename: file.previous_filename,
                });
            }
            if (response.data.length < perPage)
                break;
            page++;
        }
    }
    else {
        // Push event - compare with previous commit
        const base = `${context.sha}^`;
        const head = context.sha;
        try {
            const response = await octokit.rest.repos.compareCommits({
                owner: context.owner,
                repo: context.repo,
                base,
                head,
                per_page: 100,
                page: 1,
            });
            if (response.data.files) {
                for (const file of response.data.files) {
                    changedFiles.push({
                        filename: file.filename,
                        status: file.status,
                        additions: file.additions,
                        deletions: file.deletions,
                        changes: file.changes,
                        previousFilename: file.previous_filename,
                    });
                }
            }
        }
        catch {
            // If compare fails (e.g., initial commit), return empty array
            // which will trigger full scan
            return [];
        }
    }
    return changedFiles;
}
/**
 * Check if any security-relevant files changed
 */
export function hasSecurityRelevantChanges(changedFiles) {
    const securityPatterns = [
        /package\.json$/,
        /package-lock\.json$/,
        /yarn\.lock$/,
        /pnpm-lock\.yaml$/,
        /\.env/,
        /auth/i,
        /security/i,
        /middleware/i,
        /api\//i,
        /routes?\//i,
        /session/i,
        /token/i,
        /password/i,
        /crypt/i,
        /jwt/i,
        /oauth/i,
        /supabase/i,
        /prisma/i,
        /schema\.prisma$/,
    ];
    return changedFiles.some((file) => securityPatterns.some((pattern) => pattern.test(file.filename)));
}
/**
 * Get scan scope recommendation based on changes
 */
export function getScanScopeRecommendation(changedFiles) {
    // If no changes detected, suggest full scan
    if (changedFiles.length === 0) {
        return "full";
    }
    // If many files changed (>50), suggest full scan
    if (changedFiles.length > 50) {
        return "full";
    }
    // If security-relevant changes, suggest security-focused scan
    if (hasSecurityRelevantChanges(changedFiles)) {
        return "security-only";
    }
    // Otherwise, incremental scan
    return "incremental";
}
/**
 * Generate summary of changed files
 */
export function generateChangeSummary(changedFiles) {
    const byStatus = new Map();
    const byExtension = new Map();
    let totalAdditions = 0;
    let totalDeletions = 0;
    for (const file of changedFiles) {
        byStatus.set(file.status, (byStatus.get(file.status) || 0) + 1);
        const ext = extname(file.filename) || "(no extension)";
        byExtension.set(ext, (byExtension.get(ext) || 0) + 1);
        totalAdditions += file.additions;
        totalDeletions += file.deletions;
    }
    const lines = [
        `**Changed Files**: ${changedFiles.length}`,
        `**Lines Changed**: +${totalAdditions} / -${totalDeletions}`,
        "",
        "| Status | Count |",
        "|--------|-------|",
    ];
    for (const [status, count] of byStatus) {
        const emoji = status === "added" ? "🆕" : status === "removed" ? "🗑️" : "📝";
        lines.push(`| ${emoji} ${status} | ${count} |`);
    }
    lines.push("", "| Extension | Count |", "|-----------|-------|");
    // Sort by count descending
    const sortedExtensions = [...byExtension.entries()].sort((a, b) => b[1] - a[1]);
    for (const [ext, count] of sortedExtensions.slice(0, 10)) {
        lines.push(`| ${ext} | ${count} |`);
    }
    // Add scope recommendation
    const scope = getScanScopeRecommendation(changedFiles);
    lines.push("", `**Recommended Scope**: ${scope}`);
    if (hasSecurityRelevantChanges(changedFiles)) {
        lines.push("", "⚠️ **Security-relevant files detected** - recommend full security scan");
    }
    return lines.join("\n");
}
/**
 * Group files by directory for parallel processing
 */
export function groupFilesByDirectory(files) {
    const groups = new Map();
    for (const file of files) {
        const dir = file.path.split("/").slice(0, -1).join("/") || ".";
        const existing = groups.get(dir) || [];
        existing.push(file);
        groups.set(dir, existing);
    }
    return groups;
}
/**
 * Prioritize files for scanning (security-critical first)
 */
export function prioritizeFiles(files) {
    const securityPatterns = [
        /auth/i,
        /security/i,
        /middleware/i,
        /api\//i,
        /routes?\//i,
        /session/i,
        /token/i,
        /password/i,
        /\.env/,
        /package\.json$/,
    ];
    return [...files].sort((a, b) => {
        const aIsSecurity = securityPatterns.some((p) => p.test(a.path));
        const bIsSecurity = securityPatterns.some((p) => p.test(b.path));
        if (aIsSecurity && !bIsSecurity)
            return -1;
        if (!aIsSecurity && bIsSecurity)
            return 1;
        return 0;
    });
}
//# sourceMappingURL=diff-mode.js.map