/**
 * Scan workspace mock/sample data directories for fiction, schema drift, and leaks.
 */

const fs = require('fs');
const path = require('path');
const {
    validateSampleSchema,
    hashFileContent,
    findDuplicateContentGroups
} = require('./lib/mock-data-schema-validator');
const { PAGE_SAMPLE_SPECS } = require('./lib/page-sample-specs');
const { validateRoadmapFiles } = require('./lib/roadmap-json-specs');
const { checkSampleConsistency } = require('./lib/sample-consistency-checker');
const { scanCredentialPatterns } = require('./lib/credential-pattern-scanner');
const { scanProductionLeaks } = require('./rules/production-leak');
const { checkJestBaseline } = require('./rules/jest-baseline');
const { loadSamplebeaconConfig, resolveScanPaths, isRuleEnabled, getRuleOptions } = require('./config');
const { resolvePlatformRoot } = require('./project-detect');
const { countRepositoryInventory } = require('./lib/repository-inventory');

const EXT_CATEGORIES = {
    '.json': 'JSON Files',
    '.csv': 'CSV Files',
    '.xml': 'XML Files',
    '.sql': 'Database Files',
    '.db': 'Database Files',
    '.sqlite': 'Database Files',
    '.yaml': 'Config Files',
    '.yml': 'Config Files',
    '.txt': 'Text Files',
    '.md': 'Documentation Files'
};

const INFORMATIONAL_ISSUE_TYPES = new Set([
    'Legacy Fiction Roadmap',
    'Oversized Roadmap File'
]);

function normalizePathKey(filePath) {
    return path.resolve(filePath).replace(/\\/g, '/').toLowerCase();
}

function dedupeScannedFiles(files) {
    const seen = new Set();
    const unique = [];
    for (const file of files) {
        const key = normalizePathKey(file.path);
        if (seen.has(key)) continue;
        seen.add(key);
        unique.push(file);
    }
    return unique;
}

function displayRelativePath(baseDir, filePath) {
    return path.relative(baseDir, filePath).replace(/\\/g, '/');
}

function resolveEffectiveScanPaths(scanRoot, platformRoot, config, extraPaths = []) {
    const scanKey = normalizePathKey(scanRoot);
    const platformKey = normalizePathKey(platformRoot);

    if (scanKey === platformKey) {
        return resolveScanPaths(platformRoot, config, extraPaths);
    }
    if (scanKey.startsWith(`${platformKey}/`)) {
        return [scanRoot];
    }
    if (platformKey.startsWith(`${scanKey}/`)) {
        return resolveScanPaths(platformRoot, config, extraPaths);
    }
    return resolveScanPaths(platformRoot, config, [scanRoot, ...(extraPaths || [])]);
}

function computeFilesAnalyzed(mockCount, credentialScan, productionLeakScan) {
    return Math.max(
        mockCount,
        credentialScan?.scanned || 0,
        productionLeakScan?.scanned || 0
    );
}

async function walkFiles(dir, results = [], depth = 0) {
    if (depth > 6) return results;
    let entries;
    try {
        entries = await fs.promises.readdir(dir, { withFileTypes: true });
    } catch {
        return results;
    }

    for (const entry of entries) {
        const fullPath = path.join(dir, entry.name);
        if (entry.isDirectory()) {
            if (['node_modules', '.git', 'uploads', 'coverage'].includes(entry.name)) continue;
            await walkFiles(fullPath, results, depth + 1);
            continue;
        }
        if (!entry.isFile()) continue;
        try {
            const stat = await fs.promises.stat(fullPath);
            results.push({
                path: fullPath,
                name: entry.name,
                ext: path.extname(entry.name).toLowerCase(),
                size: stat.size
            });
        } catch {
            /* skip unreadable files */
        }
    }
    return results;
}

function formatBytes(bytes) {
    if (!bytes || bytes <= 0) return '0B';
    const units = ['B', 'KB', 'MB', 'GB'];
    let size = bytes;
    let unit = 0;
    while (size >= 1024 && unit < units.length - 1) {
        size /= 1024;
        unit += 1;
    }
    return `${size.toFixed(unit === 0 ? 0 : 1)}${units[unit]}`;
}

async function readJsonFile(filePath) {
    try {
        const raw = await fs.promises.readFile(filePath, 'utf8');
        if (!raw.trim()) {
            return { valid: false, issue: 'empty file', raw };
        }
        const payload = JSON.parse(raw);
        return { valid: true, payload, raw };
    } catch (error) {
        return { valid: false, issue: error.message, raw: null };
    }
}

function categoryForExt(ext) {
    return EXT_CATEGORIES[ext] || 'Other Files';
}

function isBlockingIssue(issue) {
    return !INFORMATIONAL_ISSUE_TYPES.has(issue.type);
}

function groupIssues(issues) {
    const grouped = new Map();

    for (const issue of issues) {
        const key = issue.id
            ? `${issue.severity}|${issue.type}|${issue.id}`
            : `${issue.severity}|${issue.type}|${issue.description}`;
        const existing = grouped.get(key);
        if (existing) {
            existing.count += 1;
            for (const fileName of issue.affectedFiles || []) {
                if (!existing.affectedFiles.includes(fileName)) {
                    existing.affectedFiles.push(fileName);
                }
            }
            for (const filePath of issue.filePaths || issue.metadata?.duplicatePaths || []) {
                if (!existing.filePaths.includes(filePath)) {
                    existing.filePaths.push(filePath);
                }
            }
        } else {
            grouped.set(key, {
                severity: issue.severity,
                type: issue.type,
                count: 1,
                description: issue.description,
                recommendedAction: issue.recommendedAction,
                affectedFiles: [...(issue.affectedFiles || [])],
                filePaths: [
                    ...(issue.filePaths || issue.metadata?.duplicatePaths || (issue.filePath ? [issue.filePath] : []))
                ]
            });
        }
    }

    return [...grouped.values()].map((item) => ({
        ...item,
        affectedFiles: item.affectedFiles.slice(0, 8)
    }));
}

function countBySeverity(issues) {
    const counts = { high: 0, medium: 0, low: 0 };
    for (const issue of issues) {
        if (counts[issue.severity] != null) {
            counts[issue.severity] += issue.count || 1;
        }
    }
    return counts;
}

async function scanMockDataDirectories(baseDir, extraPaths = [], options = {}) {
    const scanRoot = path.resolve(baseDir);
    const { platformRoot } = resolvePlatformRoot(scanRoot);
    const root = platformRoot;
    const config = options.config || loadSamplebeaconConfig(root, options.configPath);
    if (options.withJest && config.rules?.['jest-baseline']) {
        config.rules['jest-baseline'] = { ...config.rules['jest-baseline'], enabled: true, runTests: true };
    }
    const scanPaths = resolveEffectiveScanPaths(scanRoot, root, config, extraPaths);
    const schemaEnabled = isRuleEnabled(config, 'json-schema');
    const inventoryPromise = countRepositoryInventory(scanRoot, {
        profile: options.inventoryProfile || 'explorer'
    });

    const files = [];
    for (const scanPath of scanPaths) {
        if (fs.existsSync(scanPath)) {
            await walkFiles(scanPath, files);
        }
    }
    const uniqueFiles = dedupeScannedFiles(files);

    const categories = new Map();
    const issues = [];
    const hashEntries = [];
    let invalidJson = 0;
    let emptyFiles = 0;
    let schemaChecked = 0;
    let schemaPassed = 0;
    let pageSampleSchemaChecked = 0;
    let pageSampleSchemaPassed = 0;

    for (const file of uniqueFiles) {
        const category = categoryForExt(file.ext);
        const bucket = categories.get(category) || {
            category,
            fileCount: 0,
            totalSize: 0,
            issues: 0,
            files: []
        };
        bucket.fileCount += 1;
        bucket.totalSize += file.size;
        bucket.files.push(file.name);

        if (file.ext === '.json') {
            const parsed = await readJsonFile(file.path);
            if (!parsed.valid) {
                bucket.issues += 1;
                invalidJson += 1;
                if (parsed.issue === 'empty file') emptyFiles += 1;
                issues.push({
                    id: `invalid-json-${file.name}`,
                    severity: parsed.issue === 'empty file' ? 'low' : 'high',
                    type: parsed.issue === 'empty file' ? 'Empty File' : 'Invalid JSON',
                    filePath: file.path,
                    count: 1,
                    description: `${file.name}: ${parsed.issue}`,
                    recommendedAction: parsed.issue === 'empty file'
                        ? 'Remove or populate empty mock files'
                        : 'Fix JSON syntax errors in mock data',
                    affectedFiles: [file.name]
                });
            } else {
                hashEntries.push({
                    name: file.name,
                    path: file.path,
                    contentHash: hashFileContent(parsed.raw)
                });

                if (schemaEnabled && file.name.endsWith('-sample.json') && PAGE_SAMPLE_SPECS[file.name]) {
                    schemaChecked += 1;
                    pageSampleSchemaChecked += 1;
                    const schema = validateSampleSchema(file.name, parsed.payload);
                    if (schema.valid) {
                        schemaPassed += 1;
                        pageSampleSchemaPassed += 1;
                    } else {
                        bucket.issues += 1;
                        issues.push({
                            id: `schema-${file.name}`,
                            severity: 'high',
                            type: 'Schema Violation',
                            filePath: file.path,
                            count: schema.violations.length,
                            description: `${file.name}: ${schema.violations.map((v) => v.message).join('; ')}`,
                            recommendedAction: 'Update mock data to conform to dashboard page schema requirements',
                            affectedFiles: [file.name],
                            metadata: {
                                missingFields: schema.missingFields,
                                specFile: file.name,
                                violations: schema.violations
                            }
                        });
                    }
                }
            }
        }

        categories.set(category, bucket);
    }

    const duplicateGroups = findDuplicateContentGroups(hashEntries);
    for (const group of duplicateGroups) {
        const relativePaths = group.map((entry) => displayRelativePath(root, entry.path));
        issues.push({
            id: `duplicate-${group[0].contentHash.slice(0, 8)}`,
            severity: 'low',
            type: 'Duplicate Data',
            filePath: group[0].path,
            filePaths: group.map((entry) => entry.path),
            count: group.length,
            description: `${group.length} files share identical JSON content`,
            recommendedAction: 'Remove duplicate entries to optimize data size',
            affectedFiles: relativePaths,
            metadata: {
                duplicatePaths: group.map((entry) => entry.path),
                relativePaths,
                contentHash: group[0].contentHash
            }
        });
    }

    let roadmapValidation = { checked: 0, passed: 0, issues: [] };
    if (isRuleEnabled(config, 'roadmap')) {
        roadmapValidation = await validateRoadmapFiles(root, { baseline: config.baseline });
        schemaChecked += roadmapValidation.checked;
        schemaPassed += roadmapValidation.passed;
        issues.push(...roadmapValidation.issues);
    }

    let consistency = { checked: 0, passed: 0, score: null, issues: [] };
    if (isRuleEnabled(config, 'sample-consistency')) {
        consistency = await checkSampleConsistency(root, {
            sampleDir: config.sampleDir,
            baseline: config.baseline,
            anchorSamples: config.consistencyAnchorSamples
        });
        issues.push(...consistency.issues);
    }

    let credentialScan = { scanned: 0, findings: 0, issues: [] };
    if (isRuleEnabled(config, 'credentials')) {
        const credOpts = getRuleOptions(config, 'credentials');
        credentialScan = await scanCredentialPatterns(uniqueFiles, {
            scanProduction: credOpts.scanProduction !== false,
            baseDir: root,
            productionPaths: credOpts.productionPaths || config.productionPaths,
            ignoreGlobs: config.ignore
        });
        issues.push(...credentialScan.issues);
    }

    let productionLeakScan = { scanned: 0, findings: 0, issues: [] };
    if (isRuleEnabled(config, 'production-leak')) {
        const leakOpts = getRuleOptions(config, 'production-leak');
        productionLeakScan = await scanProductionLeaks(root, {
            productionPaths: leakOpts.productionPaths || config.productionPaths,
            ignoreGlobs: leakOpts.ignoreGlobs || config.ignore,
            allowlistFiles: leakOpts.allowlistFiles || [],
            severity: leakOpts.severity || 'high'
        });
        issues.push(...productionLeakScan.issues);
    }

    let jestBaseline = { checked: false, passed: true, issues: [], summary: null };
    if (isRuleEnabled(config, 'jest-baseline')) {
        const jestOpts = getRuleOptions(config, 'jest-baseline');
        jestBaseline = await checkJestBaseline(root, {
            baseline: config.baseline,
            runTests: jestOpts.runTests === true,
            testCommand: jestOpts.testCommand,
            timeoutMs: jestOpts.timeoutMs
        });
        issues.push(...jestBaseline.issues);
    }

    const totalSize = uniqueFiles.reduce((sum, file) => sum + file.size, 0);
    const issueCount = issues
        .filter(isBlockingIssue)
        .reduce((sum, issue) => sum + (issue.count || 1), 0);
    const qualityScore = uniqueFiles.length
        ? Math.max(55, Math.min(99, Math.round(100 - (issues.length / Math.max(uniqueFiles.length, 1)) * 25)))
        : 0;
    const schemaCompliance = schemaChecked
        ? Math.round((schemaPassed / schemaChecked) * 100)
        : null;

    const mockDataCategories = [...categories.values()].map((cat) => ({
        category: cat.category,
        fileCount: cat.fileCount,
        totalSize: formatBytes(cat.totalSize),
        qualityScore: Math.max(60, Math.min(99, Math.round(100 - (cat.issues / Math.max(cat.fileCount, 1)) * 40))),
        issues: cat.issues,
        confidence: null,
        description: `${cat.category} discovered during filesystem scan`
    }));

    const rawIssues = issues;
    const severityCounts = countBySeverity(rawIssues);
    const repositoryInventory = await inventoryPromise;

    return {
        type: 'samplebeacon-report',
        generatedAt: new Date().toISOString(),
        generatedBy: 'Samplebeacon',
        projectRoot: scanRoot,
        platformRoot: platformRoot !== scanRoot ? platformRoot : undefined,
        configPath: config.configPath,
        scanPaths,
        repositoryInventory,
        mockSampleFiles: uniqueFiles.length,
        totalFiles: uniqueFiles.length,
        filesAnalyzed: computeFilesAnalyzed(uniqueFiles.length, credentialScan, productionLeakScan),
        totalSizeBytes: totalSize,
        totalSizeLabel: formatBytes(totalSize),
        issueCount,
        invalidJson,
        emptyFiles,
        qualityScore,
        schemaCompliance,
        schemaChecked,
        schemaPassed,
        pageSampleSchemaChecked,
        pageSampleSchemaPassed,
        duplicateGroups: duplicateGroups.length,
        roadmapSchemaChecked: roadmapValidation.checked,
        roadmapSchemaPassed: roadmapValidation.passed,
        consistencyChecked: consistency.checked,
        consistencyPassed: consistency.passed,
        consistencyScore: consistency.score,
        credentialScanned: credentialScan.scanned,
        credentialFindings: credentialScan.findings,
        productionLeakScanned: productionLeakScan.scanned,
        productionLeakFindings: productionLeakScan.findings,
        jestBaselineChecked: jestBaseline.checked,
        jestBaselinePassed: jestBaseline.passed,
        jestSummary: jestBaseline.summary || null,
        severityCounts,
        mockDataCategories,
        detectedIssues: groupIssues(issues).slice(0, 12),
        rawIssues,
        sampleFiles: uniqueFiles.map((f) => f.name)
    };
}

async function runScan(baseDir, options = {}) {
    const scan = await scanMockDataDirectories(baseDir, options.extraPaths || [], options);
    return scan;
}

module.exports = {
    runScan,
    scanMockDataDirectories,
    formatBytes,
    categoryForExt,
    validateSampleSchema,
    groupIssues,
    isBlockingIssue,
    countBySeverity,
    resolveEffectiveScanPaths,
    computeFilesAnalyzed
};
