/**
 * Lightweight secret/credential pattern scan for mock-data and production paths.
 */

const fs = require('fs');
const path = require('path');
const { walkProductionFiles, globMatch } = require('../rules/production-leak');

const CREDENTIAL_PATTERNS = [
    { id: 'aws-access-key', regex: /\bAKIA[0-9A-Z]{16}\b/g, severity: 'high' },
    { id: 'github-pat', regex: /\bghp_[A-Za-z0-9]{20,}\b/g, severity: 'high' },
    { id: 'github-oauth', regex: /\bgho_[A-Za-z0-9]{20,}\b/g, severity: 'high' },
    { id: 'openai-key', regex: /\bsk-[A-Za-z0-9]{20,}\b/g, severity: 'high' },
    { id: 'jwt-token', regex: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, severity: 'high' },
    { id: 'slack-token', regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, severity: 'high' },
    { id: 'stripe-key', regex: /\b(sk|pk)_(test|live)_[A-Za-z0-9]{16,}\b/g, severity: 'high' },
    { id: 'generic-api-key', regex: /\b(api[_-]?key|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"][^'"\s]{12,}['"]/gi, severity: 'medium' },
    { id: 'bearer-token', regex: /Bearer\s+[A-Za-z0-9._-]{20,}/g, severity: 'medium' },
    { id: 'private-key-block', regex: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g, severity: 'high' }
];

const ALLOWLIST_SNIPPETS = [
    'demo123',
    'dev@simplebeacon.ai',
    'your-api-key-here',
    'your-secret-key',
    'your_secret_key',
    'your_secret',
    '<your-',
    'placeholder',
    'example.com',
    'xxxxxxxx',
    'replace_me',
    'changeme',
    'dummy',
    'test-only',
    'not-a-real',
    'hardcoded-secret-for-unit-test',
    'secret-key-for-unit-test',
    'sk_test_your',
    'pk_test_1234567890abcdef',
    'pk_test_51234567890abcdef',
    '51234567890abcdef',
    '1234567890abcdef',
    'kh9nv',
    'AKIAIOSFODNN7EXAMPLE',
    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0'
];

const SCANNABLE_EXTENSIONS = new Set(['.json', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.env', '.yaml', '.yml', '.txt', '.md']);
const MAX_SCAN_BYTES = 256000;

function isAllowlisted(match, content) {
    const snippet = content.slice(Math.max(0, match.index - 24), match.index + match[0].length + 24);
    const lower = snippet.toLowerCase();
    return ALLOWLIST_SNIPPETS.some((allowed) => lower.includes(allowed.toLowerCase()));
}

function scanTextContent(fileName, content, filePath = fileName) {
    const findings = [];

    for (const pattern of CREDENTIAL_PATTERNS) {
        pattern.regex.lastIndex = 0;
        let match;
        while ((match = pattern.regex.exec(content)) !== null) {
            if (isAllowlisted(match, content)) continue;
            findings.push({
                id: `${pattern.id}-${fileName}-${match.index}`,
                severity: pattern.severity,
                type: 'Credential Pattern',
                filePath,
                count: 1,
                description: `${fileName}: possible ${pattern.id.replace(/-/g, ' ')} at offset ${match.index}`,
                recommendedAction: 'Remove secret from repo; rotate credential if ever committed',
                affectedFiles: [fileName],
                metadata: { patternId: pattern.id, offset: match.index }
            });
        }
    }

    return findings;
}

async function scanCredentialPatterns(files, options = {}) {
    const issues = [];
    let scanned = 0;

    for (const file of files) {
        if (!SCANNABLE_EXTENSIONS.has(file.ext)) continue;
        if (file.size > MAX_SCAN_BYTES) continue;

        let content;
        try {
            content = await fs.promises.readFile(file.path, 'utf8');
        } catch {
            continue;
        }

        scanned += 1;
        issues.push(...scanTextContent(file.name, content, file.path));
    }

    if (options.scanProduction && options.baseDir && options.productionPaths?.length) {
        const prodFiles = [];
        for (const rel of options.productionPaths) {
            const abs = path.isAbsolute(rel)
                ? rel
                : path.join(options.baseDir, ...rel.split('/'));
            if (fs.existsSync(abs)) {
                await walkProductionFiles(abs, prodFiles);
            }
        }

        const ignoreGlobs = options.ignoreGlobs || [];
        for (const file of prodFiles) {
            const relativePath = path.relative(options.baseDir, file.path).split(path.sep).join('/');
            if (ignoreGlobs.some((pattern) => globMatch(relativePath, pattern))) continue;
            if (!SCANNABLE_EXTENSIONS.has(file.ext)) continue;
            if (file.size > MAX_SCAN_BYTES) continue;

            let content;
            try {
                content = await fs.promises.readFile(file.path, 'utf8');
            } catch {
                continue;
            }

            scanned += 1;
            const hits = scanTextContent(file.name, content, relativePath);
            issues.push(...hits.map((hit) => ({
                ...hit,
                description: `${relativePath}: possible ${hit.metadata.patternId.replace(/-/g, ' ')} in production path`
            })));
        }
    }

    return {
        scanned,
        findings: issues.length,
        issues
    };
}

module.exports = {
    CREDENTIAL_PATTERNS,
    scanCredentialPatterns,
    scanTextContent
};
