'use strict';

/**
 * Fabius Threat Intelligence Pipeline
 * 
 * Polls configured sources, classifies threats via AI,
 * deduplicates, and feeds the registry.
 */

const https = require('https');
const http = require('http');
const { URL } = require('url');
const { parseString } = require('xml2js').parseString || { parseString: null };

// Simple XML parser fallback if xml2js not available
function simpleXmlParse(xml) {
  const items = [];
  const itemRegex = /<item>([\s\S]*?)<\/item>/gi;
  let match;
  while ((match = itemRegex.exec(xml)) !== null) {
    const itemXml = match[1];
    const getTag = (tag) => {
      const r = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
      const m = itemXml.match(r);
      return m ? m[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').trim() : '';
    };
    items.push({
      title: getTag('title'),
      description: getTag('description'),
      link: getTag('link'),
      pubDate: getTag('pubDate'),
    });
  }
  return items;
}

// Atom feed parser
function parseAtomFeed(xml) {
  const items = [];
  const entryRegex = /<entry>([\s\S]*?)<\/entry>/gi;
  let match;
  while ((match = entryRegex.exec(xml)) !== null) {
    const entryXml = match[1];
    const getTag = (tag) => {
      const r = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
      const m = entryXml.match(r);
      return m ? m[1].replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1').trim() : '';
    };
    const linkMatch = entryXml.match(/<link[^>]+href=["']([^"']+)["']/);
    items.push({
      title: getTag('title'),
      description: getTag('summary') || getTag('content'),
      link: linkMatch ? linkMatch[1] : '',
      pubDate: getTag('updated') || getTag('published'),
    });
  }
  return items;
}

// ─── Default Sources ──────────────────────────────────────────────────────

const DEFAULT_SOURCES = [
  {
    id: 'github-advisory',
    name: 'GitHub Advisory Database',
    type: 'api',
    url: 'https://api.github.com/advisories?type=malware&ecosystem=npm&per_page=20',
    trusted: true,
    pollInterval: 1800,
    ecosystemFilter: ['npm'],
  },
  {
    id: 'snyk-npm',
    name: 'Snyk npm Vulnerabilities',
    type: 'rss',
    url: 'https://security.snyk.io/vuln/npm.rss',
    trusted: true,
    pollInterval: 1800,
    ecosystemFilter: ['npm'],
  },
  {
    id: 'nodejs-security',
    name: 'Node.js Security Releases',
    type: 'rss',
    url: 'https://nodejs.org/en/feed/vulnerability.xml',
    trusted: true,
    pollInterval: 3600,
    ecosystemFilter: ['npm'],
  },
  {
    id: 'bleepingcomputer',
    name: 'BleepingComputer',
    type: 'rss',
    url: 'https://www.bleepingcomputer.com/feed/',
    trusted: false,
    pollInterval: 7200,
    ecosystemFilter: [],
  },
  {
    id: 'hackernews-security',
    name: 'The Hacker News',
    type: 'rss',
    url: 'https://feeds.feedburner.com/TheHackersNews',
    trusted: false,
    pollInterval: 7200,
    ecosystemFilter: [],
  },
];

class IntelPipeline {
  /**
   * @param {object} options
   * @param {import('../registry/db')} options.db - FabiusDB instance
   * @param {import('../ai/provider')} options.ai - AIProvider instance
   * @param {import('../config/config')} options.config - Config instance
   * @param {function} options.onThreatDetected - Callback when new threat is found
   */
  constructor(options) {
    this.db = options.db;
    this.ai = options.ai;
    this.config = options.config;
    this.onThreatDetected = options.onThreatDetected || (() => {});
    this.pollTimers = new Map();
    this.threatCounter = 0;
  }

  /**
   * Initialize default sources if this is the first run
   */
  initDefaultSources() {
    const existing = this.db.listSources();
    if (existing.length === 0) {
      for (const source of DEFAULT_SOURCES) {
        this.db.addSource(source);
      }
      console.log(`[Fabius Intel] Initialized ${DEFAULT_SOURCES.length} default sources`);
    }
  }

  /**
   * Start polling all enabled sources
   */
  start() {
    const sources = this.db.listSources(true);
    for (const source of sources) {
      this._startPolling(source);
    }
    console.log(`[Fabius Intel] Started polling ${sources.length} sources`);
  }

  /**
   * Stop all polling
   */
  stop() {
    for (const [id, timer] of this.pollTimers) {
      clearInterval(timer);
    }
    this.pollTimers.clear();
    console.log('[Fabius Intel] Stopped polling');
  }

  /**
   * Poll a single source immediately
   * @param {string} sourceId
   */
  async pollSource(sourceId) {
    const source = this.db.listSources().find(s => s.id === sourceId);
    if (!source) throw new Error(`Source not found: ${sourceId}`);
    return this._fetchAndProcess(source);
  }

  /**
   * Poll all enabled sources immediately
   */
  async pollAll() {
    const sources = this.db.listSources(true);
    const results = [];
    for (const source of sources) {
      try {
        const newThreats = await this._fetchAndProcess(source);
        results.push({ source: source.id, threats: newThreats });
      } catch (err) {
        results.push({ source: source.id, error: err.message });
      }
    }
    return results;
  }

  // ─── Internal ─────────────────────────────────────────────────────────

  _startPolling(source) {
    // Poll immediately on start
    this._fetchAndProcess(source).catch(err => {
      console.warn(`[Fabius Intel] Initial poll failed for ${source.name}: ${err.message}`);
    });

    // Set up interval
    const interval = (source.poll_interval || 3600) * 1000;
    const timer = setInterval(() => {
      this._fetchAndProcess(source).catch(err => {
        console.warn(`[Fabius Intel] Poll failed for ${source.name}: ${err.message}`);
      });
    }, interval);

    this.pollTimers.set(source.id, timer);
  }

  async _fetchAndProcess(source) {
    const newThreats = [];

    try {
      let advisories;
      if (source.source_type === 'rss') {
        advisories = await this._fetchRSS(source.url);
      } else if (source.source_type === 'api') {
        advisories = await this._fetchAPI(source.url, source.api_key_ref);
      } else {
        return newThreats;
      }

      this.db.updateSourcePoll(source.id, null);

      for (const advisory of advisories) {
        // Filter for supply chain relevance
        const text = `${advisory.title} ${advisory.description}`.toLowerCase();
        const supplyChainKeywords = [
          'supply chain', 'malware', 'malicious package', 'backdoor', 'compromised',
          'npm', 'typosquatting', 'dependency confusion', 'trojan', 'rat ',
          'postinstall', 'credential', 'token theft', 'exfiltration',
        ];

        const isRelevant = supplyChainKeywords.some(kw => text.includes(kw));
        if (!isRelevant) continue;

        // Deduplicate by content hash
        const contentHash = this._hash(`${advisory.title}|${advisory.link}`);
        const potentialId = `FABIUS-${contentHash}`;
        if (this.db.threatExists(potentialId)) continue;

        // Classify with AI
        try {
          const classification = await this.ai.classifyThreat(
            `Title: ${advisory.title}\nSource: ${source.name}\nURL: ${advisory.link}\nDate: ${advisory.pubDate}\n\nDescription:\n${advisory.description}`
          );

          if (classification.relevant === false) continue;

          // Generate unique ID
          this.threatCounter++;
          const year = new Date().getFullYear();
          const id = `FABIUS-${year}-${String(this.threatCounter).padStart(4, '0')}`;

          const threat = {
            id,
            name: classification.name || advisory.title,
            description: classification.description || advisory.description,
            severity: classification.severity || 'medium',
            status: source.trusted ? 'verified' : 'detected',
            cve: classification.cve || null,
            ecosystems: classification.ecosystems || ['npm'],
            attackVector: classification.attackVector || 'unknown',
            sourceName: source.name,
            sourceUrl: advisory.link,
            sourceTrusted: source.trusted,
            rawAdvisory: advisory.description,
            discoveredAt: advisory.pubDate || new Date().toISOString(),
            affectedPackages: classification.affectedPackages || [],
          };

          this.db.addThreat(threat);
          newThreats.push(threat);

          // Notify callback
          this.onThreatDetected(threat);

        } catch (err) {
          console.warn(`[Fabius Intel] AI classification failed: ${err.message}`);
        }
      }
    } catch (err) {
      this.db.updateSourcePoll(source.id, err.message);
      throw err;
    }

    return newThreats;
  }

  async _fetchRSS(url) {
    const xml = await this._httpGet(url);
    // Try RSS first, then Atom
    let items = simpleXmlParse(xml);
    if (items.length === 0) {
      items = parseAtomFeed(xml);
    }
    return items.map(item => ({
      title: item.title,
      description: item.description,
      link: item.link,
      pubDate: item.pubDate,
    }));
  }

  async _fetchAPI(url, apiKeyRef) {
    const headers = {};
    if (apiKeyRef) {
      // API key could come from env or config
      const key = process.env[apiKeyRef] || apiKeyRef;
      headers['Authorization'] = `token ${key}`;
    }
    headers['Accept'] = 'application/json';
    headers['User-Agent'] = 'Fabius/0.1';

    const data = await this._httpGet(url, headers);
    const parsed = JSON.parse(data);

    // Handle GitHub Advisory API format
    if (Array.isArray(parsed)) {
      return parsed.map(adv => ({
        title: adv.summary || adv.title || 'Untitled',
        description: adv.description || adv.summary || '',
        link: adv.html_url || adv.url || '',
        pubDate: adv.published_at || adv.updated_at || '',
      }));
    }

    return [];
  }

  _httpGet(url, headers = {}) {
    return new Promise((resolve, reject) => {
      const parsedUrl = new URL(url);
      const client = parsedUrl.protocol === 'https:' ? https : http;

      const req = client.get(url, {
        headers: {
          'User-Agent': 'Fabius/0.1 (Supply Chain Threat Intelligence)',
          ...headers,
        },
        timeout: 15000,
      }, (res) => {
        // Follow redirects
        if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
          return this._httpGet(res.headers.location, headers).then(resolve, reject);
        }

        if (res.statusCode >= 400) {
          reject(new Error(`HTTP ${res.statusCode} from ${url}`));
          return;
        }

        let data = '';
        res.on('data', chunk => data += chunk);
        res.on('end', () => resolve(data));
      });

      req.on('timeout', () => {
        req.destroy();
        reject(new Error(`Timeout fetching ${url}`));
      });

      req.on('error', reject);
    });
  }

  _hash(str) {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash;
    }
    return Math.abs(hash).toString(36).slice(0, 8);
  }
}

IntelPipeline.DEFAULT_SOURCES = DEFAULT_SOURCES;

module.exports = IntelPipeline;
