'use strict';

/**
 * Fabius Scanner Engine v2
 *
 * Evolved from the original Glassworm Shield scanner.
 * Supports dynamically loaded detection modules from the threat registry.
 */

const fs = require('fs');
const path = require('path');

// ─── Built-in Threat Detectors (imported from existing Glassworm Shield) ─────

const BUILT_IN_DETECTORS = {
  glassworm: {
    name: 'Glassworm',
    severity: 'critical',
    patterns: {
      decoders: [/0xFE00[^)]*0xE0100/, /0xE0100[^)]*0xFE00/],
      evalBuffer: /eval\s*\(\s*Buffer\s*\.\s*from/,
      codePoint: /codePointAt\s*\([^)]*\)[^;]*0xFE00/,
    },
    packages: ['@aifabrix/miso-client', '@iflow-mcp/watercrawl-watercrawl-mcp', 'os-info-checker-es6'],
    extensions: ['quartz.quartz-markdown-editor'],
    repos: [
      'pedronauck/reworm', 'pedronauck/spacefold', 'anomalyco/opencode-bench',
      'doczjs/docz-plugin-css', 'uknfire/theGreatFilter', 'sillyva/rpg-schedule',
      'wasmer-examples/hono-wasmer-starter',
    ],
  },
  canisterworm: {
    name: 'CanisterWorm',
    severity: 'critical',
    patterns: {
      icpC2: /ic0\.app|icp0\.io|raw\.ic0\.app|@dfinity\/agent/,
      npmToken: /\.npmrc|NPM_TOKEN|npm_token/,
      credTheft: /\.kube\/config|\.docker\/config\.json|\.ssh\/(id_rsa|id_ed25519)|solana.*keypair/,
      suspExec: /child_process.*exec.*https?:|exec.*curl|exec.*wget|npm\s+token\s+list/,
    },
  },
  axiosAttack: {
    name: 'Axios Supply Chain Attack',
    severity: 'critical',
    compromisedVersions: ['1.14.1', '0.30.4'],
    maliciousDep: 'plain-crypto-js',
    packages: ['plain-crypto-js'],
  },
};

const SKIP_DIRS = new Set([
  'node_modules', '.git', '.next', 'dist', 'build', '.cache',
  'coverage', '.nyc_output', '__pycache__', '.tox', 'vendor',
]);

const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);

class ScannerEngine {
  /**
   * @param {object} options
   * @param {object} options.config - Fabius config
   * @param {object} options.db - FabiusDB instance (for loading dynamic detections)
   */
  constructor(options = {}) {
    this.config = options.config || {};
    this.db = options.db || null;
    this.maxDepth = this.config.maxDepth || 10;
    this.fileSizeLimit = this.config.fileSizeLimit || 5 * 1024 * 1024;
    this.dynamicDetectors = [];

    // Load dynamic detectors from DB
    if (this.db) {
      this._loadDynamicDetectors();
    }
  }

  _loadDynamicDetectors() {
    try {
      const detections = this.db.getLatestDetections();
      for (const det of detections) {
        try {
          // Wrap the AI-generated code in a safe function
          const fn = new Function('filePath', 'content', 'context',
            `'use strict';\n${det.code}\nreturn detect(filePath, content, context);`
          );
          this.dynamicDetectors.push({
            threatId: det.threat_id,
            threatName: det.threat_name,
            severity: det.threat_severity,
            detect: fn,
          });
        } catch (err) {
          console.warn(`[Fabius Scanner] Failed to load detection for ${det.threat_name}: ${err.message}`);
        }
      }
    } catch {
      // DB might not be available
    }
  }

  /**
   * Scan a repository or directory
   * @param {string} scanPath - Path to scan
   * @param {object} options
   * @param {string} options.threatId - Scan for specific threat only
   * @param {function} options.onProgress - Progress callback
   * @returns {object} Scan results
   */
  async scan(scanPath, options = {}) {
    const resolvedPath = path.resolve(scanPath);

    const results = {
      scanPath: resolvedPath,
      startTime: new Date().toISOString(),
      endTime: null,
      duration: 0,
      filesScanned: 0,
      alerts: 0,
      warnings: 0,
      findings: [],
    };

    const start = Date.now();

    // 1. Scan project files
    this._scanDirectory(resolvedPath, 0, results, options);

    // 2. Check compromised packages in node_modules
    this._checkCompromisedPackages(resolvedPath, results);

    // 3. Check package.json dependencies
    this._checkPackageJsonDeps(resolvedPath, results);

    // 4. Check lock files (Axios attack)
    this._checkLockFiles(resolvedPath, results);

    // 5. Run dynamic detectors
    if (!options.threatId) {
      this._runDynamicDetectors(resolvedPath, results);
    }

    results.endTime = new Date().toISOString();
    results.duration = ((Date.now() - start) / 1000).toFixed(1);

    return results;
  }

  // ─── File Scanning ──────────────────────────────────────────────────────

  _scanDirectory(dir, depth, results, options) {
    if (depth > this.maxDepth) return;

    let entries;
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }

    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);

      if (entry.isDirectory()) {
        if (SKIP_DIRS.has(entry.name)) continue;
        this._scanDirectory(fullPath, depth + 1, results, options);
      } else if (entry.isFile()) {
        const ext = path.extname(entry.name).toLowerCase();
        if (!SCAN_EXTENSIONS.has(ext)) continue;
        this._scanFile(fullPath, results);
      }
    }
  }

  _scanFile(filePath, results) {
    results.filesScanned++;

    // Skip our own files
    if (filePath.includes('fabius') && filePath.includes('scanner')) return;
    if (filePath.includes('glassworm-shield')) return;

    let content;
    try {
      const stat = fs.statSync(filePath);
      if (stat.size > this.fileSizeLimit) return;
      content = fs.readFileSync(filePath, 'utf-8');
    } catch {
      return;
    }

    // ── Glassworm checks ──
    const gw = BUILT_IN_DETECTORS.glassworm;
    for (const pattern of gw.patterns.decoders) {
      if (pattern.test(content)) {
        const hasEval = gw.patterns.evalBuffer.test(content) || /eval\s*\(/.test(content);
        this._addFinding(results, {
          severity: hasEval ? 'critical' : 'low',
          threat: 'glassworm',
          type: hasEval ? 'Glassworm decoder + eval()' : 'Unicode range reference (likely benign)',
          file: filePath,
          detail: hasEval
            ? 'File contains the full Glassworm decoder pattern with eval() — likely malicious'
            : 'File references Unicode variation selector ranges but does not use eval()',
        });
        break;
      }
    }

    if (gw.patterns.evalBuffer.test(content) && gw.patterns.codePoint.test(content)) {
      this._addFinding(results, {
        severity: 'critical',
        threat: 'glassworm',
        type: 'eval(Buffer.from) + codePointAt',
        file: filePath,
        detail: 'Glassworm payload execution signature',
      });
    }

    // Invisible Unicode
    this._checkInvisibleUnicode(content, filePath, results);

    // ── CanisterWorm checks ──
    const cw = BUILT_IN_DETECTORS.canisterworm;
    if (cw.patterns.icpC2.test(content)) {
      const hasExec = /child_process|execSync|spawnSync/.test(content);
      const hasFetch = /fetch\(|https?\.get|axios|got\(/.test(content);
      if (hasExec || hasFetch) {
        this._addFinding(results, {
          severity: 'critical',
          threat: 'canisterworm',
          type: 'CanisterWorm ICP C2 + exec/fetch',
          file: filePath,
          detail: 'ICP blockchain C2 endpoint with code execution capability',
        });
      }
    }

    // Credential harvesting cross-check
    if (cw.patterns.npmToken.test(content) && cw.patterns.credTheft.test(content)) {
      const hasExec = /child_process|execSync/.test(content);
      if (hasExec) {
        this._addFinding(results, {
          severity: 'critical',
          threat: 'canisterworm',
          type: 'CanisterWorm credential harvesting',
          file: filePath,
          detail: 'File accesses both npm tokens and system credentials with exec capability',
        });
      }
    }

    if (options?.onProgress && results.filesScanned % 1000 === 0) {
      options.onProgress(results.filesScanned);
    }
  }

  _checkInvisibleUnicode(content, filePath, results) {
    for (let i = 0; i < content.length; i++) {
      const cp = content.codePointAt(i);

      if (cp >= 0xFE00 && cp <= 0xFE0F) {
        this._addFinding(results, {
          severity: 'warning',
          threat: 'glassworm',
          type: 'Variation Selector character',
          file: filePath,
          detail: `Invisible Unicode char U+${cp.toString(16).toUpperCase()} at position ${i}`,
        });
        return;
      }

      if (cp >= 0xE0100 && cp <= 0xE01EF) {
        this._addFinding(results, {
          severity: 'critical',
          threat: 'glassworm',
          type: 'Supplementary Variation Selector (payload)',
          file: filePath,
          detail: `Invisible Unicode char U+${cp.toString(16).toUpperCase()} — likely Glassworm payload`,
        });
        return;
      }

      if (cp > 0xFFFF) i++;
    }
  }

  // ─── Package Checks ─────────────────────────────────────────────────────

  _checkCompromisedPackages(scanPath, results) {
    const allCompromised = [
      ...BUILT_IN_DETECTORS.glassworm.packages,
      ...BUILT_IN_DETECTORS.axiosAttack.packages,
    ];

    this._findNodeModules(scanPath, 0, (nmPath) => {
      for (const pkg of allCompromised) {
        const parts = pkg.split('/');
        const checkPath = parts.length === 2
          ? path.join(nmPath, parts[0], parts[1])
          : path.join(nmPath, pkg);

        if (fs.existsSync(checkPath)) {
          this._addFinding(results, {
            severity: 'critical',
            threat: 'compromised-package',
            type: 'Compromised npm package installed',
            file: checkPath,
            detail: `Known compromised package "${pkg}" found in node_modules`,
          });
        }
      }
    });
  }

  _checkPackageJsonDeps(scanPath, results) {
    this._findFiles(scanPath, 'package.json', 3, (pkgPath) => {
      if (pkgPath.includes('node_modules')) return;

      let pkg;
      try {
        pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
      } catch {
        return;
      }

      const allDeps = Object.assign({},
        pkg.dependencies, pkg.devDependencies,
        pkg.peerDependencies, pkg.optionalDependencies
      );

      const allCompromised = [
        ...BUILT_IN_DETECTORS.glassworm.packages,
        ...BUILT_IN_DETECTORS.axiosAttack.packages,
      ];

      for (const [name, version] of Object.entries(allDeps)) {
        if (allCompromised.includes(name)) {
          this._addFinding(results, {
            severity: 'critical',
            threat: 'compromised-package',
            type: 'Compromised package in dependencies',
            file: pkgPath,
            detail: `"${name}@${version}" is a known compromised package`,
          });
        }

        // Axios version check
        if (name === 'axios' && BUILT_IN_DETECTORS.axiosAttack.compromisedVersions.includes(version)) {
          this._addFinding(results, {
            severity: 'critical',
            threat: 'axios-attack',
            type: 'Compromised axios version',
            file: pkgPath,
            detail: `axios@${version} is compromised — RAT dropper via plain-crypto-js`,
          });
        }

        // Unsafe ranges for axios
        if (name === 'axios' && typeof version === 'string') {
          if (version.startsWith('^1.') || version.startsWith('~1.14') || version === '*' || version === 'latest') {
            this._addFinding(results, {
              severity: 'warning',
              threat: 'axios-attack',
              type: 'Unsafe axios version range',
              file: pkgPath,
              detail: `axios@${version} could auto-upgrade to compromised version. Pin to a safe version.`,
            });
          }
        }

        // Git deps to compromised repos
        if (typeof version === 'string' && version.includes('github')) {
          for (const repo of BUILT_IN_DETECTORS.glassworm.repos) {
            if (version.includes(repo)) {
              this._addFinding(results, {
                severity: 'high',
                threat: 'compromised-repo',
                type: 'Compromised GitHub repo in dependencies',
                file: pkgPath,
                detail: `Dependency "${name}" points to compromised repo "${repo}"`,
              });
            }
          }
        }
      }

      // Suspicious install scripts
      if (pkg.scripts) {
        const suspiciousKeywords = ['curl', 'wget', 'bash -c', 'eval', 'base64', 'ic0.app', 'icp0.io'];
        for (const scriptName of ['preinstall', 'postinstall', 'install', 'prepare']) {
          const cmd = pkg.scripts[scriptName];
          if (!cmd || cmd.includes('glassworm') || cmd.includes('fabius')) continue;
          for (const kw of suspiciousKeywords) {
            if (cmd.includes(kw)) {
              this._addFinding(results, {
                severity: 'high',
                threat: 'canisterworm',
                type: `Suspicious ${scriptName} script`,
                file: pkgPath,
                detail: `${scriptName} contains "${kw}": ${cmd.slice(0, 120)}`,
              });
              break;
            }
          }
        }
      }
    });
  }

  _checkLockFiles(scanPath, results) {
    this._findFiles(scanPath, 'package-lock.json', 3, (lockPath) => {
      let content;
      try {
        content = fs.readFileSync(lockPath, 'utf-8');
      } catch {
        return;
      }

      if (content.includes('plain-crypto-js')) {
        this._addFinding(results, {
          severity: 'critical',
          threat: 'axios-attack',
          type: 'Malicious plain-crypto-js in lock file',
          file: lockPath,
          detail: 'Lock file references the malicious plain-crypto-js package',
        });
      }

      for (const ver of BUILT_IN_DETECTORS.axiosAttack.compromisedVersions) {
        if (content.includes(`/axios/-/axios-${ver}.tgz`) || content.includes(`"version": "${ver}"`)) {
          this._addFinding(results, {
            severity: 'critical',
            threat: 'axios-attack',
            type: `Compromised axios@${ver} in lock file`,
            file: lockPath,
            detail: `Lock file pins to compromised axios version ${ver}`,
          });
        }
      }
    });
  }

  // ─── Dynamic Detectors ──────────────────────────────────────────────────

  _runDynamicDetectors(scanPath, results) {
    if (!this.dynamicDetectors.length) return;

    this._findFiles(scanPath, '*.js', this.maxDepth, (filePath) => {
      let content;
      try {
        content = fs.readFileSync(filePath, 'utf-8');
      } catch {
        return;
      }

      for (const detector of this.dynamicDetectors) {
        try {
          const result = detector.detect(filePath, content, { scanPath });
          if (result && result.found) {
            this._addFinding(results, {
              severity: result.severity || detector.severity,
              threat: detector.threatId,
              type: `[${detector.threatName}] ${result.details || 'Detection triggered'}`,
              file: filePath,
              detail: result.details || '',
            });
          }
        } catch {
          // Silent fail for dynamic detectors
        }
      }
    });
  }

  // ─── Helpers ────────────────────────────────────────────────────────────

  _addFinding(results, finding) {
    finding.timestamp = new Date().toISOString();
    results.findings.push(finding);
    if (finding.severity === 'critical' || finding.severity === 'high') {
      results.alerts++;
    } else {
      results.warnings++;
    }
  }

  _findNodeModules(dir, depth, callback) {
    if (depth > 3) return;
    let entries;
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      if (entry.name === 'node_modules') {
        callback(path.join(dir, entry.name));
      } else if (entry.name !== '.git') {
        this._findNodeModules(path.join(dir, entry.name), depth + 1, callback);
      }
    }
  }

  _findFiles(dir, filename, maxDepth, callback, depth = 0) {
    if (depth > maxDepth) return;
    let entries;
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isFile()) {
        if (filename.includes('*')) {
          const ext = filename.replace('*', '');
          if (entry.name.endsWith(ext)) callback(fullPath);
        } else if (entry.name === filename) {
          callback(fullPath);
        }
      } else if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
        this._findFiles(fullPath, filename, maxDepth, callback, depth + 1);
      }
    }
  }
}

ScannerEngine.BUILT_IN_DETECTORS = BUILT_IN_DETECTORS;

module.exports = ScannerEngine;
