// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * NERF OSINT Pipeline — comprehensive open-source intelligence.
 *
 * Integrates Bellingcat toolkit methodology with programmatic tool wrappers:
 *
 * Domain intelligence:
 *   - DNS (A/AAAA/MX/NS/TXT/CNAME/SOA via dig or node:dns)
 *   - WHOIS (registration, registrar, dates, name servers)
 *   - Certificate Transparency (crt.sh subdomain discovery)
 *   - Wayback Machine (archive snapshots)
 *   - Web technology fingerprinting (headers + HTML analysis)
 *   - URL reputation (urlscan.io when API key available)
 *
 * IP intelligence:
 *   - Reverse DNS, classification (private/public/multicast/loopback)
 *   - IP geolocation (ip-api.com — free, no key)
 *   - Abuse contact lookup (via WHOIS)
 *
 * People/username:
 *   - Sherlock (username → 400+ social platforms)
 *   - Holehe (email → account existence on 120+ services)
 *
 * Document metadata:
 *   - EXIF extraction (exiftool)
 *   - PDF metadata (pdfinfo)
 *
 * Archive/history:
 *   - Wayback Machine CDX API (snapshot history)
 *   - archive.today availability check
 *
 * All external tool calls degrade gracefully when tools aren't installed.
 * All network calls use configurable timeouts. No API keys required for
 * core functionality — optional keys unlock deeper queries.
 *
 * @module pipeline/osint
 */

import { execFileSync, execSync } from 'node:child_process';
import dns from 'node:dns';
import { promises as dnsPromises } from 'node:dns';
import net from 'node:net';
import { resolve, dirname } from 'node:path';
import { existsSync } from 'node:fs';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const HTTP_TIMEOUT = 15000;

/**
 * Find the Python venv path for OSINT tools (sherlock, holehe).
 * Walks up from cli/lib/pipeline/ to find .venv/bin/.
 * @returns {string|null}
 */
function findVenvBin() {
  let dir = resolve(__dirname, '..', '..', '..');
  for (let i = 0; i < 5; i++) {
    const venvBin = resolve(dir, '.venv', 'bin');
    if (existsSync(venvBin)) return venvBin;
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

// ---------------------------------------------------------------------------
// IP classification helper
// ---------------------------------------------------------------------------

/**
 * Check if an IP address is private (RFC 1918 / RFC 4193).
 * @param {string} ip
 * @returns {boolean}
 */
function isPrivateIP(ip) {
  const version = net.isIP(ip);
  if (version === 0) return false;
  if (version === 4) {
    const parts = ip.split('.').map(Number);
    if (parts[0] === 10) return true;
    if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
    if (parts[0] === 192 && parts[1] === 168) return true;
    if (parts[0] === 127) return true;
    return false;
  }
  const lower = ip.toLowerCase();
  if (lower === '::1') return true;
  const parts = lower.split(':');
  if (!parts[0]) return false;
  const val = parseInt(parts[0], 16);
  if (isNaN(val)) return false;
  if (val >= 0xfc00 && val <= 0xfdff) return true;
  if (val >= 0xfe80 && val <= 0xfebf) return true;
  return false;
}

function _isMulticast(ip, version) {
  if (version === 4) {
    const first = parseInt(ip.split('.')[0], 10);
    return first >= 224 && first <= 239;
  }
  return ip.toLowerCase().startsWith('ff');
}

// ---------------------------------------------------------------------------
// Data class
// ---------------------------------------------------------------------------

class OSINTResult {
  constructor(opts) {
    this.source = opts.source;
    this.query = opts.query;
    this.data = opts.data ?? {};
    this.confidence = opts.confidence ?? 'medium';
    this.timestamp = opts.timestamp ?? new Date().toISOString();
    this.collectionMethod = opts.collectionMethod ?? 'passive';
  }

  get success() { return !this.data.error; }
  get error() { return this.data.error ?? null; }

  toDict() {
    return {
      source: this.source,
      query: this.query,
      data: this.data,
      confidence: this.confidence,
      timestamp: this.timestamp,
      collection_method: this.collectionMethod,
    };
  }
}

// ---------------------------------------------------------------------------
// HTTP helper (uses global fetch, available in Node 18+)
// ---------------------------------------------------------------------------

async function fetchJSON(url, opts = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), opts.timeout || HTTP_TIMEOUT);
  try {
    const resp = await fetch(url, {
      signal: controller.signal,
      headers: opts.headers || {},
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    return await resp.json();
  } finally {
    clearTimeout(timer);
  }
}

async function fetchText(url, opts = {}) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), opts.timeout || HTTP_TIMEOUT);
  try {
    const resp = await fetch(url, {
      signal: controller.signal,
      headers: opts.headers || { 'User-Agent': 'NERF-OSINT/2.0' },
    });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
    return await resp.text();
  } finally {
    clearTimeout(timer);
  }
}

// ---------------------------------------------------------------------------
// Domain Intelligence
// ---------------------------------------------------------------------------

class DomainIntelligence {
  /**
   * DNS record lookup via dig with node:dns fallback.
   */
  static async dnsLookup(domain) {
    const data = { domain, records: {} };
    const rtypes = ['A', 'AAAA', 'MX', 'NS', 'TXT', 'CNAME', 'SOA'];
    let hasDig = true;

    for (const rtype of rtypes) {
      if (hasDig) {
        try {
          const out = execFileSync('dig', ['+short', domain, rtype], {
            encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'],
          });
          const records = out.split('\n').map(r => r.trim()).filter(Boolean);
          if (records.length > 0) data.records[rtype] = records;
        } catch (err) {
          if (err.code === 'ENOENT') { hasDig = false; } else { continue; }
        }
      }
      if (!hasDig && rtype === 'A') {
        // Use native dns.promises instead of spawning a subprocess with
        // interpolated user input — prevents CWE-94 code injection.
        try {
          const ips = await dnsPromises.resolve4(domain);
          if (ips.length > 0) data.records.A = ips;
        } catch { /* skip — domain may not have A records */ }
        break;
      }
    }
    return new OSINTResult({ source: 'dns', query: domain, data, confidence: 'high' });
  }

  /**
   * WHOIS registration lookup.
   */
  static whoisLookup(domain) {
    try {
      const out = execFileSync('whois', [domain], {
        encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
      });
      const data = { domain, raw_length: out.length };
      const patterns = {
        registrar: /Registrar:\s*(.+)/i,
        creation_date: /Creat(?:ion|ed) Date:\s*(.+)/i,
        expiration_date: /(?:Expir(?:ation|y) Date|Registry Expiry Date):\s*(.+)/i,
        name_servers: /Name Server:\s*(.+)/ig,
        status: /Status:\s*(.+)/ig,
        registrant_org: /Registrant Organi[sz]ation:\s*(.+)/i,
        registrant_country: /Registrant Country:\s*(.+)/i,
      };
      for (const [key, pattern] of Object.entries(patterns)) {
        if (pattern.global) {
          const matches = [...out.matchAll(pattern)].map(m => m[1]);
          if (matches.length > 0) data[key] = matches.length > 1 ? matches : matches[0];
        } else {
          const m = pattern.exec(out);
          if (m) data[key] = m[1];
        }
      }
      return new OSINTResult({ source: 'whois', query: domain, data, confidence: 'high' });
    } catch (err) {
      return new OSINTResult({
        source: 'whois', query: domain,
        data: { error: err.code === 'ENOENT' ? 'whois binary not found' : err.message },
        confidence: 'low',
      });
    }
  }

  /**
   * Certificate Transparency search via crt.sh for subdomain discovery.
   */
  static async certTransparency(domain) {
    try {
      const data = await fetchJSON(`https://crt.sh/?q=%25.${encodeURIComponent(domain)}&output=json`, { timeout: 20000 });
      const subdomains = [...new Set(
        (data || [])
          .flatMap(entry => (entry.name_value || '').split('\n'))
          .map(name => name.trim().toLowerCase().replace(/^\*\./, ''))
          .filter(name => name.endsWith(domain) && name !== domain)
      )].sort();
      return new OSINTResult({
        source: 'cert_transparency', query: domain,
        data: { domain, subdomains, count: subdomains.length },
        confidence: 'high',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'cert_transparency', query: domain,
        data: { error: `crt.sh query failed: ${err.message}` },
        confidence: 'low',
      });
    }
  }

  /**
   * Wayback Machine snapshot history via CDX API.
   */
  static async waybackHistory(domain) {
    try {
      const url = `https://web.archive.org/cdx/search/cdx?url=${encodeURIComponent(domain)}&output=json&limit=20&fl=timestamp,original,statuscode,mimetype`;
      const data = await fetchJSON(url, { timeout: 20000 });
      if (!data || data.length < 2) {
        return new OSINTResult({
          source: 'wayback_machine', query: domain,
          data: { domain, snapshots: [], count: 0, first_seen: null, last_seen: null },
          confidence: 'medium',
        });
      }
      // First row is headers, rest are data
      const snapshots = data.slice(1).map(row => ({
        timestamp: row[0],
        url: row[1],
        status: row[2],
        mime: row[3],
        archive_url: `https://web.archive.org/web/${row[0]}/${row[1]}`,
      }));
      return new OSINTResult({
        source: 'wayback_machine', query: domain,
        data: {
          domain,
          snapshots,
          count: snapshots.length,
          first_seen: snapshots[0]?.timestamp || null,
          last_seen: snapshots[snapshots.length - 1]?.timestamp || null,
        },
        confidence: 'high',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'wayback_machine', query: domain,
        data: { error: `Wayback Machine query failed: ${err.message}` },
        confidence: 'low',
      });
    }
  }

  /**
   * Web technology fingerprinting via HTTP headers and HTML meta analysis.
   */
  static async webTechFingerprint(domain) {
    try {
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
      const resp = await fetch(`https://${domain}`, {
        signal: controller.signal,
        redirect: 'follow',
        headers: { 'User-Agent': 'NERF-OSINT/2.0' },
      });
      clearTimeout(timer);

      const headers = Object.fromEntries(resp.headers.entries());
      const html = await resp.text();
      const techs = [];

      // Server header
      if (headers.server) techs.push({ name: headers.server, category: 'server', source: 'header' });
      // X-Powered-By
      if (headers['x-powered-by']) techs.push({ name: headers['x-powered-by'], category: 'framework', source: 'header' });
      // CDN detection
      if (headers['cf-ray']) techs.push({ name: 'Cloudflare', category: 'cdn', source: 'header' });
      if (headers['x-amz-cf-id'] || headers['x-cache']) techs.push({ name: 'AWS CloudFront', category: 'cdn', source: 'header' });
      if (headers['x-vercel-id']) techs.push({ name: 'Vercel', category: 'hosting', source: 'header' });
      if (headers['x-netlify-request-id']) techs.push({ name: 'Netlify', category: 'hosting', source: 'header' });
      // Security headers
      const securityHeaders = {};
      for (const h of ['strict-transport-security', 'content-security-policy', 'x-frame-options',
                         'x-content-type-options', 'x-xss-protection', 'permissions-policy',
                         'referrer-policy', 'cross-origin-opener-policy']) {
        if (headers[h]) securityHeaders[h] = headers[h];
      }
      // HTML-based detection (limited, fast)
      const htmlLower = html.slice(0, 50000).toLowerCase();
      if (htmlLower.includes('wp-content') || htmlLower.includes('wordpress')) techs.push({ name: 'WordPress', category: 'cms', source: 'html' });
      if (htmlLower.includes('drupal')) techs.push({ name: 'Drupal', category: 'cms', source: 'html' });
      if (htmlLower.includes('joomla')) techs.push({ name: 'Joomla', category: 'cms', source: 'html' });
      if (htmlLower.includes('shopify')) techs.push({ name: 'Shopify', category: 'ecommerce', source: 'html' });
      if (htmlLower.includes('squarespace')) techs.push({ name: 'Squarespace', category: 'cms', source: 'html' });
      if (htmlLower.includes('wix.com')) techs.push({ name: 'Wix', category: 'cms', source: 'html' });
      if (htmlLower.includes('next/') || htmlLower.includes('__next')) techs.push({ name: 'Next.js', category: 'framework', source: 'html' });
      if (htmlLower.includes('react')) techs.push({ name: 'React', category: 'framework', source: 'html' });
      if (htmlLower.includes('vue')) techs.push({ name: 'Vue.js', category: 'framework', source: 'html' });
      if (htmlLower.includes('angular')) techs.push({ name: 'Angular', category: 'framework', source: 'html' });
      if (htmlLower.includes('bootstrap')) techs.push({ name: 'Bootstrap', category: 'css', source: 'html' });
      if (htmlLower.includes('tailwind')) techs.push({ name: 'Tailwind CSS', category: 'css', source: 'html' });
      if (htmlLower.includes('jquery')) techs.push({ name: 'jQuery', category: 'library', source: 'html' });
      // Meta generator
      const genMatch = html.match(/<meta[^>]+name=["']generator["'][^>]+content=["']([^"']+)["']/i);
      if (genMatch) techs.push({ name: genMatch[1], category: 'generator', source: 'meta' });

      return new OSINTResult({
        source: 'web_tech', query: domain,
        data: { domain, technologies: techs, security_headers: securityHeaders, status: resp.status, final_url: resp.url },
        confidence: 'medium',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'web_tech', query: domain,
        data: { error: `Web tech scan failed: ${err.message}` },
        confidence: 'low',
      });
    }
  }
}

// ---------------------------------------------------------------------------
// IP Intelligence
// ---------------------------------------------------------------------------

class IPIntelligence {
  static async reverseDns(ip) {
    // Validate IP before lookup — prevents CWE-94 code injection that was
    // previously possible by interpolating ip into a Node -e subprocess.
    if (net.isIP(ip) === 0) {
      return new OSINTResult({ source: 'reverse_dns', query: ip, data: { ip, hostname: null }, confidence: 'low' });
    }
    try {
      const hostnames = await dnsPromises.reverse(ip);
      const hostname = hostnames?.[0] ?? null;
      return new OSINTResult({
        source: 'reverse_dns', query: ip,
        data: { ip, hostname }, confidence: hostname ? 'high' : 'low',
      });
    } catch {
      return new OSINTResult({ source: 'reverse_dns', query: ip, data: { ip, hostname: null }, confidence: 'low' });
    }
  }

  static async ipInfo(ip) {
    const data = { ip };
    const version = net.isIP(ip);
    if (version === 0) {
      return new OSINTResult({ source: 'ip_info', query: ip, data: { error: 'Invalid IP address' }, confidence: 'low' });
    }
    data.version = version;
    data.is_private = isPrivateIP(ip);
    data.is_loopback = (version === 4 && ip.startsWith('127.')) || ip === '::1';
    data.is_multicast = _isMulticast(ip, version);

    try {
      const hostnames = await dnsPromises.reverse(ip);
      data.hostname = hostnames?.[0] ?? null;
    } catch { data.hostname = null; }

    return new OSINTResult({ source: 'ip_info', query: ip, data, confidence: 'high' });
  }

  /**
   * IP geolocation via ip-api.com (free, no key, 45 req/min).
   */
  static async geolocate(ip) {
    if (isPrivateIP(ip)) {
      return new OSINTResult({ source: 'ip_geo', query: ip, data: { ip, note: 'Private IP — no geolocation' }, confidence: 'low' });
    }
    try {
      const data = await fetchJSON(`http://ip-api.com/json/${encodeURIComponent(ip)}?fields=status,message,country,countryCode,region,regionName,city,zip,lat,lon,timezone,isp,org,as,asname,query`);
      if (data.status === 'fail') {
        return new OSINTResult({ source: 'ip_geo', query: ip, data: { error: data.message }, confidence: 'low' });
      }
      return new OSINTResult({ source: 'ip_geo', query: ip, data, confidence: 'high' });
    } catch (err) {
      return new OSINTResult({ source: 'ip_geo', query: ip, data: { error: `Geolocation failed: ${err.message}` }, confidence: 'low' });
    }
  }
}

// ---------------------------------------------------------------------------
// Username/People Intelligence
// ---------------------------------------------------------------------------

class PeopleIntelligence {
  /**
   * Username search across 400+ platforms via Sherlock.
   * Returns found accounts with URLs.
   */
  static usernameSearch(username) {
    const venvBin = findVenvBin();
    const sherlock = venvBin ? resolve(venvBin, 'sherlock') : 'sherlock';

    try {
      const out = execFileSync(sherlock, [username, '--print-found', '--timeout', '10', '--output', '/dev/null'], {
        encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'],
      });
      const accounts = [];
      for (const line of out.split('\n')) {
        // Sherlock output: [+] SiteName: URL
        const match = line.match(/^\[\+\]\s+(.+?):\s+(https?:\/\/.+)/);
        if (match) {
          accounts.push({ platform: match[1].trim(), url: match[2].trim() });
        }
      }
      return new OSINTResult({
        source: 'sherlock', query: username,
        data: { username, accounts, count: accounts.length },
        confidence: 'high', collectionMethod: 'active',
      });
    } catch (err) {
      const msg = err.code === 'ENOENT' ? 'sherlock not installed (pip install sherlock-project)' : err.message;
      return new OSINTResult({
        source: 'sherlock', query: username,
        data: { error: msg }, confidence: 'low',
      });
    }
  }

  /**
   * Email account existence check across 120+ services via Holehe.
   */
  static emailOsint(email) {
    const venvBin = findVenvBin();
    const holehe = venvBin ? resolve(venvBin, 'holehe') : 'holehe';

    try {
      const out = execFileSync(holehe, [email, '--no-color', '--only-used', '-NP'], {
        encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'],
      });
      const services = [];
      for (const line of out.split('\n')) {
        // Holehe output: [+] service.com (skip legend line)
        const match = line.match(/^\[\+\]\s+(.+)/);
        if (match && !match[1].includes('Email used') && !match[1].includes('Email not used')) {
          services.push(match[1].trim());
        }
      }
      return new OSINTResult({
        source: 'holehe', query: email,
        data: { email, services, count: services.length },
        confidence: 'medium', collectionMethod: 'active',
      });
    } catch (err) {
      const msg = err.code === 'ENOENT' ? 'holehe not installed (pip install holehe)' : err.message;
      return new OSINTResult({
        source: 'holehe', query: email,
        data: { error: msg }, confidence: 'low',
      });
    }
  }
}

// ---------------------------------------------------------------------------
// Archive Intelligence
// ---------------------------------------------------------------------------

class ArchiveIntelligence {
  /**
   * Check if a URL has been archived on archive.today.
   */
  static async archiveTodayCheck(url) {
    try {
      const resp = await fetchText(`https://archive.ph/newest/${url}`, { timeout: 15000 });
      const hasArchive = resp.includes('id="CONTENT"') || resp.includes('archived');
      return new OSINTResult({
        source: 'archive_today', query: url,
        data: { url, archived: hasArchive, check_url: `https://archive.ph/newest/${url}` },
        confidence: 'medium',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'archive_today', query: url,
        data: { error: `archive.today check failed: ${err.message}` },
        confidence: 'low',
      });
    }
  }

  /**
   * Wayback Machine availability check for a specific URL.
   */
  static async waybackCheck(url) {
    try {
      const data = await fetchJSON(`https://archive.org/wayback/available?url=${encodeURIComponent(url)}`);
      const snapshot = data?.archived_snapshots?.closest;
      return new OSINTResult({
        source: 'wayback_check', query: url,
        data: {
          url,
          available: !!snapshot,
          closest_snapshot: snapshot ? { url: snapshot.url, timestamp: snapshot.timestamp, status: snapshot.status } : null,
        },
        confidence: snapshot ? 'high' : 'medium',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'wayback_check', query: url,
        data: { error: `Wayback check failed: ${err.message}` },
        confidence: 'low',
      });
    }
  }
}

// ---------------------------------------------------------------------------
// Document Metadata
// ---------------------------------------------------------------------------

class DocumentMetadata {
  static extractExif(filepath) {
    try {
      const out = execFileSync('exiftool', ['-json', filepath], {
        encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
      });
      const metadata = JSON.parse(out);
      return new OSINTResult({
        source: 'exif', query: filepath,
        data: { metadata: Array.isArray(metadata) ? metadata[0] : metadata },
        confidence: 'high',
      });
    } catch (err) {
      return new OSINTResult({
        source: 'exif', query: filepath,
        data: { error: err.code === 'ENOENT' ? 'exiftool not found' : 'Failed to extract EXIF' },
        confidence: 'low',
      });
    }
  }

  static extractPdfMetadata(filepath) {
    try {
      const out = execFileSync('pdfinfo', [filepath], {
        encoding: 'utf-8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
      });
      const data = {};
      for (const line of out.split('\n')) {
        const idx = line.indexOf(':');
        if (idx >= 0) data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
      }
      return new OSINTResult({ source: 'pdf_metadata', query: filepath, data, confidence: 'high' });
    } catch (err) {
      return new OSINTResult({
        source: 'pdf_metadata', query: filepath,
        data: { error: err.code === 'ENOENT' ? 'pdfinfo not found' : 'Failed to extract PDF metadata' },
        confidence: 'low',
      });
    }
  }
}

// ---------------------------------------------------------------------------
// OSINT Pipeline — comprehensive orchestrator
// ---------------------------------------------------------------------------

class OSINTPipeline {
  constructor() {
    this._results = [];
  }

  /**
   * Full domain investigation — DNS, WHOIS, cert transparency,
   * Wayback Machine, web tech, and IP pivot with geolocation.
   * @param {string} domain
   * @returns {Promise<OSINTResult[]>}
   */
  async investigateDomain(domain) {
    const results = [];

    // Synchronous lookups
    results.push(await DomainIntelligence.dnsLookup(domain));
    results.push(DomainIntelligence.whoisLookup(domain));

    // Async lookups — run in parallel
    const asyncResults = await Promise.allSettled([
      DomainIntelligence.certTransparency(domain),
      DomainIntelligence.waybackHistory(domain),
      DomainIntelligence.webTechFingerprint(domain),
    ]);

    for (const r of asyncResults) {
      if (r.status === 'fulfilled') results.push(r.value);
    }

    // IP pivot — resolve IPs and geolocate them
    const dnsResult = results[0];
    const aRecords = dnsResult.data?.records?.A ?? [];
    for (const ip of aRecords.slice(0, 3)) {
      results.push(await IPIntelligence.ipInfo(ip));
      const geo = await IPIntelligence.geolocate(ip);
      results.push(geo);
    }

    this._results.push(...results);
    return results;
  }

  /**
   * Full IP investigation — info, reverse DNS, geolocation.
   * @param {string} ip
   * @returns {Promise<OSINTResult[]>}
   */
  async investigateIp(ip) {
    const results = await Promise.all([IPIntelligence.ipInfo(ip), IPIntelligence.reverseDns(ip)]);
    const geo = await IPIntelligence.geolocate(ip);
    results.push(geo);
    this._results.push(...results);
    return results;
  }

  /**
   * Username OSINT — search across 400+ platforms via Sherlock.
   * @param {string} username
   * @returns {OSINTResult[]}
   */
  investigateUsername(username) {
    const results = [PeopleIntelligence.usernameSearch(username)];
    this._results.push(...results);
    return results;
  }

  /**
   * Email OSINT — check account existence across 120+ services.
   * @param {string} email
   * @returns {OSINTResult[]}
   */
  investigateEmail(email) {
    const results = [PeopleIntelligence.emailOsint(email)];
    this._results.push(...results);
    return results;
  }

  /**
   * URL archive investigation — Wayback Machine + archive.today.
   * @param {string} url
   * @returns {Promise<OSINTResult[]>}
   */
  async investigateUrl(url) {
    const results = await Promise.allSettled([
      ArchiveIntelligence.waybackCheck(url),
      ArchiveIntelligence.archiveTodayCheck(url),
    ]);
    const settled = results.filter(r => r.status === 'fulfilled').map(r => r.value);
    this._results.push(...settled);
    return settled;
  }

  /**
   * Extract metadata from a file (EXIF for images, pdfinfo for PDFs).
   */
  extractMetadata(filepath) {
    const result = filepath.toLowerCase().endsWith('.pdf')
      ? DocumentMetadata.extractPdfMetadata(filepath)
      : DocumentMetadata.extractExif(filepath);
    this._results.push(result);
    return result;
  }

  getAllResults() { return this._results.map(r => r.toDict()); }

  summary() {
    const bySource = {};
    const byConfidence = {};
    for (const r of this._results) {
      bySource[r.source] = (bySource[r.source] ?? 0) + 1;
      byConfidence[r.confidence] = (byConfidence[r.confidence] ?? 0) + 1;
    }
    return { total_results: this._results.length, by_source: bySource, by_confidence: byConfidence };
  }
}

// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------

export {
  isPrivateIP,
  OSINTResult,
  DomainIntelligence,
  IPIntelligence,
  PeopleIntelligence,
  ArchiveIntelligence,
  DocumentMetadata,
  OSINTPipeline,
};
