const fs = require("fs");
const http = require("http");
const https = require("https");
const crypto = require("crypto");

// ── LOCAL DATABASE CLIENT (BGP, RPKI, Threat Intel) ──────────────
const localDb = require('./local-db-client');
console.log('[PeerCortex] Local DB client initialized');


// Load .env file
require('./src/backend/config');

const BGPROUTES_API_KEY = process.env.BGPROUTES_API_KEY || "";
const BGPROUTES_API_URL = process.env.BGPROUTES_API_URL || "https://api.bgproutes.io/v1";

// bio-rd gRPC client (optional — graceful fallback if unavailable)
let risClient = null;
try {
  const { createRisClient } = require('./bio-rd-client');
  const BIO_RD_HOST = process.env.BIO_RD_HOST || 'localhost';
  const BIO_RD_PORT = parseInt(process.env.BIO_RD_PORT || '4321');
  risClient = createRisClient(BIO_RD_HOST, BIO_RD_PORT);
  console.log(`[bio-rd] RIS client configured → ${BIO_RD_HOST}:${BIO_RD_PORT}`);
} catch (e) {
  console.log('[bio-rd] RIS client not available (bio-rd-client.js missing or gRPC not installed)');
}

const {
  PEERINGDB_API_KEY,
  PEERINGDB_API_URL,
  getPdbLocal,
  queryPeeringDBLocal,
  pdbSemaphore,
  fetchPeeringDB,
  fetchPeeringDBWithRetry,
} = require("./server/services/peeringdb");

// ── SMTP / Email ──────────────────────────────────────────────
const { sendFeedbackMail } = require('./src/backend/services/smtp');
// ── SMTP / Email ──────────────────────────────────────────────

const { loadVisitors, trackVisitor } = require("./server/visitors");

// ═══════════════════════════════════════════════════════════════
// PEERCORTEX v0.6.1 — New Features
// ═══════════════════════════════════════════════════════════════

// ── BGP Community Database ─────────────────────────────────────
// Migrated to src/features/bgp-communities/bgp-community-db.ts

// ── Hijack Monitoring ──────────────────────────────────────────
const {
  DATA_DIR,
  loadHijackSubs,
  loadHijackAlerts,
  loadWebhookSubs,
  saveWebhookSubs,
  saveHijackAlerts,
  saveHijackSubs,
} = require("./server/hijack-monitoring/store");
const { webhookSignature, deliverWebhook, notifyWebhooks } = require("./server/hijack-monitoring/webhooks");
const { classifyHijackSeverity, checkHijacksForAsn, runHijackCheck } = require("./server/hijack-monitoring/monitor");

const {
  aspaAdoptionDailyHistory,
  recordAspaAdoptionSnapshot,
  getAdoptionTrend,
  buildAspaAdoptionDashboard,
} = require("./server/aspa-adoption/tracker");

const { fetchRirDelegation, fetchIpv6AdoptionStats } = require("./server/ipv6-adoption");

const { UA } = require("./server/data/constants");
const { CITY_COORDS } = require("./server/data/city-coords");

// ============================================================
const {
  scoreRingSvg,
  pdfBaseCSS,
  buildPdfHtml,
  buildComparisonPdfHtml,
  formatLabel,
  escHtml,
} = require("./server/pdf-export/templates");
const {
  getPuppeteerBrowser,
  pdfCacheGet,
  pdfCacheSet,
  renderHtmlToPdf,
} = require("./server/pdf-export/renderer");

const {
  responseCache,
  cacheGet,
  cacheSet,
  RESULT_CACHE_TTL,
  CACHE_TTL_LOOKUP,
  CACHE_TTL_LOOKUP_DEGRADED,
  CACHE_TTL_DEFAULT,
  rdapCacheGet,
  rdapCacheSet,
  whoisCacheGet,
  whoisCacheSet,
  WHOIS_NOT_FOUND_CACHE_TTL,
  quickIxCacheGet,
  quickIxCacheSet,
  bgproutesResultCache,
  aspaResultCache,
  validateResultCache,
  resultCacheGet,
  resultCacheSet,
} = require("./server/caches/response-caches");
// bgproutesVpCache/bgproutesVpCacheTs/BGPROUTES_VP_TTL removed 2026-07-16:
// confirmed dead code, never read anywhere in the file.

const { ensureManrsCache, checkManrsMembership } = require("./server/services/manrs");

// ============================================================
// subCableCache/globalFacCache removed 2026-07-16: confirmed dead code,
// never read anywhere in the file.

const { roaStore } = require("./server/caches/roa-store");

const {
  rpkiAspaMap,
  getRpkiAspaLastFetch,
  fetchRpkiAspaFeed,
  ensureAspaCache,
  lookupAspaFromRpki,
  validateRPKIWithCache,
  fetchRipeRpkiValidator,
  crossCheckRpki,
} = require("./server/services/rpki");

const { pdbSourceCache } = require("./server/caches/pdb-source-cache");
const {
  ripeStatCache,
  fetchRipeStatCached,
  fetchRipeStatCachedFromApi,
  fetchRipeStatCachedWithRetry,
  saveRipeStatCacheToDisk,
  loadRipeStatCacheFromDisk,
  Semaphore,
} = require("./server/services/ripe-stat");


const {
  fetchJSON,
  fetchJSONWithRetry,
  fetchHTML,
  postJSON,
  fetchBgproutesVisibility,
} = require("./server/services/http-helpers");
// checkRateLimit/rateLimitMap removed 2026-07-16: dead code, never called by
// the request handler (confirmed via the server.js structural exploration).

const { resolveASNames } = require("./server/resolve-as-names");



const {
  hasAsSet,
  hopCheck,
  collapsePrepends,
  verifyUpstream,
  verifyDownstream,
  detectValleys,
  buildAspaStore,
  calculateAspaReadinessScore,
} = require("./server/aspa-verification/engine");


const { fetchBgpHeNet } = require("./server/bgp-he-net");

const { fetchTopology } = require("./server/topology");

const { computeResilienceScore } = require("./server/resilience-score");
const { computeRouteLeakDetection } = require("./server/route-leak");
const { fetchWhois } = require("./server/whois");

// ============================================================
// Internal API Server Proxy
// ============================================================
// Several routes were extracted into src/api/ + src/features/*/routes.ts
// (Fastify, dist/api/index.js, run as a separate PM2 process on
// API_SERVER_PORT) but were never re-wired here after that migration —
// they just silently 404'd. This forwards those specific paths through
// rather than reimplementing them inline a second time.
const API_SERVER_PORT = parseInt(process.env.API_SERVER_PORT || "3102", 10);

function proxyToApiServer(req, res, targetUrl) {
  const proxyReq = http.request(
    {
      hostname: "127.0.0.1",
      port: API_SERVER_PORT,
      path: targetUrl,
      method: req.method,
      headers: Object.assign({}, req.headers, { host: "127.0.0.1:" + API_SERVER_PORT }),
    },
    (proxyRes) => {
      res.writeHead(proxyRes.statusCode, proxyRes.headers);
      proxyRes.pipe(res);
    }
  );
  proxyReq.on("error", (err) => {
    console.error("[API Proxy] Error forwarding " + targetUrl + ":", err.message);
    if (!res.headersSent) {
      res.writeHead(502, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "API server unavailable", detail: err.message }));
    }
  });
  req.pipe(proxyReq);
}

// ============================================================
// HTTP Server
// ============================================================

const staticRoutes = require("./server/routes/static");
const searchRoutes = require("./server/routes/search");
const feedbackRoutes = require("./server/routes/feedback");
const liaRoutes = require("./server/routes/lia");
const healthRoute = require("./server/routes/health");
const aspaVerifyRoute = require("./server/routes/aspa-verify");
const aspaLegacyRoute = require("./server/routes/aspa-legacy");
const bgpRoute = require("./server/routes/bgp");
const validateRoute = require("./server/routes/validate");
const lookupRoute = require("./server/routes/lookup");
const relationshipsRoute = require("./server/routes/relationships");
const compareRoute = require("./server/routes/compare");
const quickIxRoute = require("./server/routes/quick-ix");
const peersFindRoute = require("./server/routes/peers-find");
const prefixDetailRoute = require("./server/routes/prefix-detail");
const ixDetailRoute = require("./server/routes/ix-detail");
const topologyRoute = require("./server/routes/topology");
const whoisRoute = require("./server/routes/whois");
const enrichRoute = require("./server/routes/enrich");

const server = http.createServer(async (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");

  if (req.method === "OPTIONS") {
    res.writeHead(204);
    return res.end();
  }

  let url, reqPath;
  try {
    url = new URL(req.url, "http://localhost");
    reqPath = url.pathname;
  } catch (_urlErr) {
    res.writeHead(400);
    return res.end('Bad Request');
  }

  // Serve static files — host-based routing
  const host = (req.headers.host || '').split(':')[0];

  if (reqPath === "/" || reqPath === "/index.html") {
    return staticRoutes.handleRoot(req, res, host, reqPath);
  }

  // Direct access to editorial version
  if (reqPath === "/index-editorial.html") {
    return staticRoutes.handleIndexEditorial(req, res);
  }

  // Name Search (RIPE Stat + PeeringDB combined)
  if (reqPath === "/api/search") {
    return searchRoutes.handleSearch(req, res);
  }

  // GET /api/visitors — unique visitor count
  if (reqPath === "/api/visitors" && req.method === "GET") {
    return searchRoutes.handleVisitors(req, res);
  }

  // OPTIONS preflight (CORS)
  if (reqPath === "/api/feedback" && req.method === "OPTIONS") {
    return feedbackRoutes.handleOptions(req, res);
  }

  // POST /api/feedback — submit feedback entry
  if (reqPath === "/api/feedback" && req.method === "POST") {
    return feedbackRoutes.handlePost(req, res);
  }

  // GET /api/feedback?token=... — admin: fetch all entries as JSON
  if (reqPath === "/api/feedback" && req.method === "GET") {
    return feedbackRoutes.handleGet(req, res, url);
  }

  // Serve favicon
  if (reqPath === "/favicon.ico") {
    return staticRoutes.handleFavicon(req, res);
  }

  // Lia's Atlas Paradise - Easter egg page
  if (reqPath === "/lia" || reqPath === "/lia/") {
    return staticRoutes.handleLia(req, res);
  }

  // Lia's Atlas Paradise: Atlas probe coverage endpoint
  if (reqPath === "/api/atlas/coverage") {
    return liaRoutes.handleAtlasCoverage(req, res);
  }

  // Lia's Paradise: File parsing endpoint (for binary uploads)
  if (reqPath === "/api/lia/parse-file" && req.method === "POST") {
    return liaRoutes.handleParseFile(req, res);
  }

  // Lia's Paradise: Combined PeeringDB + Atlas coverage data
  if (reqPath === "/api/lia/coverage") {
    return liaRoutes.handleLiaCoverage(req, res);
  }

  res.setHeader("Content-Type", "application/json");

  // Health endpoint — extended with cache status, ASPA metrics, and local DB stats
  if (reqPath === "/api/health") {
    return healthRoute.handleHealth(req, res);
  }


  // ============================================================
  // ASPA Deep Verification endpoint: /api/aspa/verify?asn=X
  if (reqPath === "/api/aspa/verify") {
    return aspaVerifyRoute.handleAspaVerify(req, res, url);
  }

  // ASPA Check endpoint: /api/aspa?asn=X (existing, kept for compat)
  if (reqPath === "/api/aspa") {
    return aspaLegacyRoute.handleAspaLegacy(req, res, url);
  }

  // BGP endpoint (LOCAL DB): /api/bgp?asn=X (or prefix=X)
  if (reqPath === "/api/bgp") {
    return bgpRoute.handleBgp(req, res, url);
  }



  // Unified Validation endpoint: /api/validate?asn=X
  if (reqPath === "/api/validate") {
    return validateRoute.handleValidate(req, res, url);
  }

  // Main lookup endpoint: /api/lookup?asn=X
  if (reqPath === "/api/lookup") {
    return lookupRoute.handleLookup(req, res, url);
  }

  if (reqPath === "/api/relationships") {
    return relationshipsRoute.handleRelationships(req, res, url);
  }

  // Compare endpoint: /api/compare?asn1=X&asn2=Y
  if (reqPath === "/api/compare") {
    return compareRoute.handleCompare(req, res, url);
  }

  // Quick-IX endpoint: /api/quick-ix?asn=X
  if (reqPath === "/api/quick-ix") {
    return quickIxRoute.handleQuickIx(req, res, url);
  }

  // Peer Matching endpoint: /api/peers/find?ix=NAME&policy=open&min_speed=10000
  if (reqPath === "/api/peers/find") {
    return peersFindRoute.handlePeersFind(req, res, url);
  }

  // Prefix Detail endpoint: /api/prefix/detail?prefix=X
  if (reqPath === "/api/prefix/detail") {
    return prefixDetailRoute.handlePrefixDetail(req, res, url);
  }

  // IX Detail endpoint: /api/ix/detail?ix_id=X
  if (reqPath === "/api/ix/detail") {
    return ixDetailRoute.handleIxDetail(req, res, url);
  }

  // Feature 25: Topology endpoint
  if (reqPath === "/api/topology") {
    return topologyRoute.handleTopology(req, res, url);
  }

  // Feature 27: WHOIS endpoint
  if (reqPath === "/api/whois") {
    return whoisRoute.handleWhoisRoute(req, res, url);
  }

  // Feature 28b: Company enrichment via Wikipedia + website meta scraping
  if (reqPath === "/api/enrich") {
    return enrichRoute.handleEnrich(req, res, url);
  }


  // Feature 28: Submarine Cable overlay (TeleGeography proxy)
  // ── Submarine Cables map data ─────────────────────────────────
  if (reqPath === '/api/submarine-cables') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── Global datacenter/IXP map (PeeringDB proxy) ───────────────
  if (reqPath === '/api/global-infra') {
    return proxyToApiServer(req, res, req.url);
  }


  // ── Changelog page ─────────────────────────────────────────
  if (reqPath === '/changelog') {
    try {
      const md = fs.readFileSync('/opt/peercortex-app/CHANGELOG.md', 'utf8');
      const lines = md.split('\n');
      let html = '';
      for (const line of lines) {
        if (line.startsWith('## ')) {
          html += `<h2 style="font-family:var(--serif);font-size:1.4rem;font-weight:800;margin:2rem 0 .5rem;border-top:2px solid var(--text);padding-top:1rem">${line.slice(3)}</h2>`;
        } else if (line.startsWith('### ')) {
          html += `<h3 style="font-family:var(--body);font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin:1rem 0 .4rem">${line.slice(4)}</h3>`;
        } else if (line.startsWith('- **')) {
          const m = line.replace(/^- \*\*(.+?)\*\*(.*)$/, '<strong>$1</strong>$2');
          html += `<p style="font-family:var(--body);font-size:.85rem;margin:.2rem 0;padding-left:1rem;border-left:2px solid var(--border)">· ${m}</p>`;
        } else if (line.startsWith('- ')) {
          html += `<p style="font-family:var(--body);font-size:.82rem;margin:.15rem 0;color:var(--muted);padding-left:1rem">· ${line.slice(2)}</p>`;
        } else if (line.startsWith('# ')) {
          html += `<h1 style="font-family:var(--serif);font-size:2rem;font-weight:900;margin-bottom:.25rem">${line.slice(2)}</h1>`;
        }
      }
      const page = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>PeerCortex Changelog</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root{--bg:#F5F2EC;--text:#1C1917;--muted:#57534E;--border:#C9C3B6;--serif:'Playfair Display',Georgia,serif;--body:'Source Serif 4',Georgia,serif;--mono:'IBM Plex Mono',monospace;--purple:#B83A1B}
body{font-family:var(--body);background:var(--bg);color:var(--text);max-width:760px;margin:0 auto;padding:2rem}
a{color:var(--purple);text-decoration:none}
.back{font-family:var(--mono);font-size:.72rem;color:var(--muted);margin-bottom:2rem;display:block}
body.dark{--bg:#0f0f0f;--text:#e8e4dc;--muted:#a09890;--border:#333}
</style></head><body>
<a href="/" class="back">← peercortex.org</a>
${html}
<p style="margin-top:3rem;font-family:var(--mono);font-size:.6rem;color:var(--muted)">PeerCortex · v0.5.0 · Open Source · MIT</p>
</body></html>`;
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.setHeader('Cache-Control', 'no-store');
      res.writeHead(200);
      return res.end(page);
    } catch(e) {
      res.writeHead(500); return res.end('Changelog not available');
    }
  }

  // ── BGP Community Decoder ────────────────────────────────────
  if (reqPath === '/api/communities') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── IRR Audit ─────────────────────────────────────────────────
  if (reqPath === '/api/irr-audit') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── AS-SET Expander ───────────────────────────────────────────
  if (reqPath === '/api/asset-expand') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── Routing History (prefix table via RIPE Stat routing-history) ──
  if (reqPath === '/api/rpki-history') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── AS-PATH Visualizer (RIPE Stat looking-glass) ────────────────
  if (reqPath === '/api/aspath') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── Looking Glass (RIPE Stat) ─────────────────────────────────
  if (reqPath === '/api/looking-glass') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── IXP Peering Matrix ────────────────────────────────────────
  if (reqPath === '/api/ix-matrix') {
    return proxyToApiServer(req, res, req.url);
  }


  // ── CORS preflight for all /api/webhooks + /api/hijacks ──────
  if ((reqPath.startsWith('/api/webhooks') || reqPath.startsWith('/api/hijacks') || reqPath === '/api/hijack-subscribe') && req.method === 'OPTIONS') {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
    res.writeHead(204); return res.end();
  }

  // ── Webhook: Register ─────────────────────────────────────────
  // POST /api/webhooks?asn=X  body: { endpoint_url, timeout_ms?, max_retries? }
  if (reqPath === '/api/webhooks' && req.method === 'POST') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    const params = new URL(req.url, 'http://localhost').searchParams;
    const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
    if (!asn) { res.writeHead(400); return res.end(JSON.stringify({error:'asn query param required'})); }
    let body = '';
    req.on('data', c => body += c);
    req.on('end', () => {
      try {
        const { endpoint_url, timeout_ms, max_retries } = JSON.parse(body || '{}');
        if (!endpoint_url || !endpoint_url.startsWith('http')) {
          res.writeHead(400); return res.end(JSON.stringify({error:'endpoint_url required (http/https)'}));
        }
        const subs = loadWebhookSubs();
        const exists = subs.find(s => s.asn === asn && s.endpoint_url === endpoint_url);
        if (exists) { res.writeHead(200); return res.end(JSON.stringify({id: exists.id, already_exists: true, secret_key: exists.secret})); }
        const webhookToken = crypto.randomBytes(32).toString('hex');
        const entry = {
          id: crypto.randomBytes(8).toString('hex'),
          asn, endpoint_url,
          secret: webhookToken,
          timeout_ms: timeout_ms || 10000,
          max_retries: max_retries || 5,
          active: true,
          created_at: new Date().toISOString(),
          last_triggered_at: null,
          failure_count: 0
        };
        subs.push(entry);
        saveWebhookSubs(subs);
        // Also ensure this ASN is in hijack monitoring
        const hijackSubs = loadHijackSubs();
        if (!hijackSubs.find(s => s.asn === asn)) {
          checkHijacksForAsn(asn).then(prefixes => {
            // null (lookup failed) becomes [] here so this record's shape stays
            // consistent; runHijackCheck retries the real baseline next cycle.
            hijackSubs.push({ asn, prefixes: prefixes || [], subscribed: new Date().toISOString() });
            saveHijackSubs(hijackSubs);
          });
        }
        res.writeHead(201);
        res.end(JSON.stringify({ id: entry.id, asn, endpoint_url, secret_key: secret, created_at: entry.created_at, note: 'Store secret_key safely — it signs all webhook payloads' }));
      } catch(e) { res.writeHead(400); res.end(JSON.stringify({error: e.message})); }
    });
    return;
  }

  // ── Webhook: List ─────────────────────────────────────────────
  // GET /api/webhooks?asn=X
  if (reqPath === '/api/webhooks' && req.method === 'GET') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    const params = new URL(req.url, 'http://localhost').searchParams;
    const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
    const subs = loadWebhookSubs();
    const result = (asn ? subs.filter(s => s.asn === asn) : subs)
      .map(s => ({ id: s.id, asn: s.asn, endpoint_url: s.endpoint_url, active: s.active, failure_count: s.failure_count, last_triggered_at: s.last_triggered_at, created_at: s.created_at }));
    res.writeHead(200);
    return res.end(JSON.stringify({ webhooks: result, total: result.length }));
  }

  // ── Webhook: Delete ───────────────────────────────────────────
  // DELETE /api/webhooks/:id
  if (reqPath.startsWith('/api/webhooks/') && req.method === 'DELETE' && !reqPath.includes('/test')) {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    const id = reqPath.split('/')[3];
    const subs = loadWebhookSubs();
    const idx = subs.findIndex(s => s.id === id);
    if (idx === -1) { res.writeHead(404); return res.end(JSON.stringify({error:'webhook not found'})); }
    subs.splice(idx, 1);
    saveWebhookSubs(subs);
    res.writeHead(200);
    return res.end(JSON.stringify({ deleted: true, id }));
  }

  // ── Webhook: Test ─────────────────────────────────────────────
  // POST /api/webhooks/:id/test
  if (reqPath.match(/^\/api\/webhooks\/[^/]+\/test$/) && req.method === 'POST') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    const id = reqPath.split('/')[3];
    const subs = loadWebhookSubs();
    const sub = subs.find(s => s.id === id);
    if (!sub) { res.writeHead(404); return res.end(JSON.stringify({error:'webhook not found'})); }
    const testPayload = { event: 'test', asn: sub.asn, message: 'PeerCortex webhook test — if you receive this, delivery works!', timestamp: new Date().toISOString() };
    const start = Date.now();
    deliverWebhook(sub, testPayload, 1).then(result => {
      res.writeHead(result.ok ? 200 : 502);
      res.end(JSON.stringify({ ok: result.ok, response_time_ms: Date.now() - start, status: result.status, error: result.error || null }));
    });
    return;
  }

  // ── Hijack Events: List ───────────────────────────────────────
  // GET /api/hijacks?asn=X&limit=50&resolved=false
  if (reqPath === '/api/hijacks' && req.method === 'GET') {
    const params = new URL(req.url, 'http://localhost').searchParams;
    const asn = params.get('asn') ? String(params.get('asn')).replace(/[^0-9]/g,'') : '';
    const limit = Math.min(parseInt(params.get('limit') || '50', 10), 200);
    const resolvedFilter = params.get('resolved');
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Cache-Control', 'no-store');
    let events = loadHijackAlerts();
    if (asn) events = events.filter(a => String(a.asn) === asn);
    if (resolvedFilter === 'false') events = events.filter(a => !a.resolved);
    if (resolvedFilter === 'true')  events = events.filter(a => a.resolved);
    const total = events.length;
    events = events.slice(-limit).reverse();
    res.writeHead(200);
    return res.end(JSON.stringify({ total, events }));
  }

  // ── Hijack Events: Resolve ────────────────────────────────────
  // POST /api/hijacks/:id/resolve  body: { resolution_notes? }
  if (reqPath.match(/^\/api\/hijacks\/[^/]+\/resolve$/) && req.method === 'POST') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    const id = reqPath.split('/')[3];
    let body = '';
    req.on('data', c => body += c);
    req.on('end', () => {
      const alerts = loadHijackAlerts();
      const alert = alerts.find(a => a.id === id);
      if (!alert) { res.writeHead(404); return res.end(JSON.stringify({error:'event not found'})); }
      alert.resolved = true;
      alert.resolved_at = new Date().toISOString();
      try { const { resolution_notes } = JSON.parse(body || '{}'); if (resolution_notes) alert.resolution_notes = resolution_notes; } catch(_){}
      saveHijackAlerts(alerts);
      res.writeHead(200);
      res.end(JSON.stringify({ resolved: true, id, resolved_at: alert.resolved_at }));
    });
    return;
  }

  // ── Hijack Subscribe (legacy, kept for backwards compatibility) ─
  if (reqPath === '/api/hijack-subscribe' && req.method === 'POST') {
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    let body = '';
    req.on('data', c => body += c);
    req.on('end', async () => {
      try {
        const { asn } = JSON.parse(body || '{}');
        const asnNum = String(asn || '').replace(/[^0-9]/g,'');
        if (!asnNum) { res.writeHead(400); return res.end(JSON.stringify({error:'asn required'})); }
        const subs = loadHijackSubs();
        const exists = subs.find(s => s.asn === asnNum);
        if (!exists) {
          const prefixes = await checkHijacksForAsn(asnNum);
          // prefixes may be null if the lookup failed just now -- store an empty
          // array so this record shape stays consistent; runHijackCheck's own
          // "!sub.prefixes.length" check will retry establishing the real
          // baseline on its next cycle rather than treating this as permanent.
          subs.push({ asn: asnNum, prefixes: prefixes || [], subscribed: new Date().toISOString() });
          saveHijackSubs(subs);
        }
        const sub = subs.find(s => s.asn === asnNum) || { prefixes: [] };
        res.writeHead(200);
        res.end(JSON.stringify({ ok: true, asn: asnNum, monitoring: true, prefix_count: (sub.prefixes || []).length }));
      } catch(e) { res.writeHead(500); res.end(JSON.stringify({error:e.message})); }
    });
    return;
  }

  // ── Hijack Alerts (legacy endpoint, kept for backwards compatibility) ─
  if (reqPath.startsWith('/api/hijack-alerts')) {
    const params = new URL(req.url, 'http://localhost').searchParams;
    const asn = (params.get('asn') || '').replace(/[^0-9]/g,'');
    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Cache-Control', 'no-store');
    const allAlerts = loadHijackAlerts();
    const alerts = asn ? allAlerts.filter(a => String(a.asn) === asn) : allAlerts;
    const subs = loadHijackSubs();
    const sub = subs.find(s => s.asn === asn);
    res.writeHead(200);
    return res.end(JSON.stringify({ asn, alerts: alerts.slice(-50), monitoring: !!sub, prefix_count: sub ? sub.prefixes.length : 0 }));
  }


  // ── Hijack Alerts (legacy read) ───────────────────────────────
  // src/routes/hijack-alerts.ts registers /api/webhooks + /api/hijacks under
  // the same paths this file already serves above (file-backed, live).
  // Deliberately NOT proxied here — that would shadow working data with an
  // empty Postgres-backed implementation. Not a gap, just two feature
  // branches that ended up naming their routes the same thing.

  // ── Changelog JSON API ────────────────────────────────────────
  if (reqPath === '/changelog-data') {
    return proxyToApiServer(req, res, req.url);
  }

  // ── bio-rd RIB routes ─────────────────────────────────────────
  if (reqPath.startsWith('/api/rib/')) {
    return proxyToApiServer(req, res, req.url);
  }

  // ── Prefix Changes ──────────────────────────────────────────────
  if (reqPath === '/api/prefix-changes') {
    return proxyToApiServer(req, res, req.url);
  }

  // ============================================================
  // FEATURE 2: PDF Export
  // GET  /api/export/pdf?asn=X&format=report|executive|technical
  // GET  /api/export/pdf/compare?asn1=X&asn2=Y
  // ============================================================

  // Helper: fetch JSON from internal endpoint
  async function fetchInternal(path) {
    return new Promise((resolve, reject) => {
      const opts = { hostname: '127.0.0.1', port: PORT, path, method: 'GET' };
      const req2 = http.request(opts, res2 => {
        let body = '';
        res2.on('data', c => body += c);
        res2.on('end', () => {
          try { resolve(JSON.parse(body)); }
          catch(e) { reject(new Error('JSON parse failed: ' + body.slice(0, 200))); }
        });
      });
      req2.on('error', reject);
      req2.setTimeout(30000, () => { req2.destroy(); reject(new Error('Internal request timeout')); });
      req2.end();
    });
  }

  if (reqPath === '/api/export/pdf' && req.method === 'GET') {
    const rawAsn = (url.searchParams.get('asn') || '').replace(/[^0-9]/g, '');
    const format  = ['executive', 'technical', 'report'].includes(url.searchParams.get('format'))
      ? url.searchParams.get('format') : 'report';
    if (!rawAsn) {
      res.writeHead(400, {'Content-Type':'application/json'});
      return res.end(JSON.stringify({ error: 'Missing asn parameter' }));
    }
    const cacheKey = `pdf:${rawAsn}:${format}`;
    const cached   = pdfCacheGet(cacheKey);
    if (cached) {
      res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length });
      return res.end(cached);
    }
    try {
      const [validateData, aspaData] = await Promise.all([
        fetchInternal(`/api/validate?asn=${rawAsn}`),
        fetchInternal(`/api/aspa?asn=${rawAsn}`).catch(() => null),
      ]);
      if (validateData.error) {
        res.writeHead(400, {'Content-Type':'application/json'});
        return res.end(JSON.stringify({ error: validateData.error }));
      }
      const html = buildPdfHtml(validateData, aspaData, format);
      const pdfBuf = await renderHtmlToPdf(html);
      pdfCacheSet(cacheKey, pdfBuf);
      res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${rawAsn}-${format}.pdf"`, 'Content-Length': pdfBuf.length });
      return res.end(pdfBuf);
    } catch (err) {
      console.error('[PDF] Error:', err.message);
      res.writeHead(503, {'Content-Type':'application/json'});
      return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message }));
    }
  }

  if (reqPath === '/api/export/pdf/compare' && req.method === 'GET') {
    const asn1 = (url.searchParams.get('asn1') || '').replace(/[^0-9]/g, '');
    const asn2 = (url.searchParams.get('asn2') || '').replace(/[^0-9]/g, '');
    if (!asn1 || !asn2) {
      res.writeHead(400, {'Content-Type':'application/json'});
      return res.end(JSON.stringify({ error: 'Missing asn1 or asn2 parameter' }));
    }
    if (asn1 === asn2) {
      res.writeHead(400, {'Content-Type':'application/json'});
      return res.end(JSON.stringify({ error: 'asn1 and asn2 must be different' }));
    }
    const cacheKey = `pdf:compare:${Math.min(+asn1,+asn2)}:${Math.max(+asn1,+asn2)}`;
    const cached   = pdfCacheGet(cacheKey);
    if (cached) {
      res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'X-Cache': 'HIT', 'Content-Length': cached.length });
      return res.end(cached);
    }
    try {
      const [v1, a1, l1, v2, a2, l2] = await Promise.all([
        fetchInternal(`/api/validate?asn=${asn1}`),
        fetchInternal(`/api/aspa?asn=${asn1}`).catch(() => null),
        fetchInternal(`/api/lookup?asn=${asn1}`).catch(() => null),
        fetchInternal(`/api/validate?asn=${asn2}`),
        fetchInternal(`/api/aspa?asn=${asn2}`).catch(() => null),
        fetchInternal(`/api/lookup?asn=${asn2}`).catch(() => null),
      ]);
      if (v1.error || v2.error) {
        res.writeHead(400, {'Content-Type':'application/json'});
        return res.end(JSON.stringify({ error: v1.error || v2.error }));
      }
      const html = buildComparisonPdfHtml(v1, a1, l1, v2, a2, l2);
      const pdfBuf = await renderHtmlToPdf(html);
      pdfCacheSet(cacheKey, pdfBuf);
      res.writeHead(200, { ...{'Content-Type':'application/json'}, 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="peercortex-AS${asn1}-vs-AS${asn2}.pdf"`, 'Content-Length': pdfBuf.length });
      return res.end(pdfBuf);
    } catch (err) {
      console.error('[PDF] Error:', err.message);
      res.writeHead(503, {'Content-Type':'application/json'});
      return res.end(JSON.stringify({ error: 'PDF generation failed', message: err.message }));
    }
  }

  // CORS preflight for PDF export (already handled globally above, belt-and-suspenders)
  if (reqPath.startsWith('/api/export/') && req.method === 'OPTIONS') {
    res.writeHead(204);
    return res.end();
  }

  // ============================================================
  // FEATURE 3: ASPA Adoption Tracker — API + Dashboard
  // GET  /aspa-adoption                               → dashboard HTML
  // GET  /api/aspa-adoption-stats?period=7d|30d|1y   → JSON trend
  // GET  /api/aspa-adoption-stats/export?format=csv|json&period=30d
  // ============================================================

  if (reqPath === '/aspa-adoption') {
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    res.writeHead(200);
    return res.end(buildAspaAdoptionDashboard());
  }

  if (reqPath === '/api/aspa-adoption-stats' && req.method === 'GET') {
    const period   = ['7d','30d','90d','1y'].includes(url.searchParams.get('period'))
      ? url.searchParams.get('period') : '30d';
    const trend    = getAdoptionTrend(period);
    const latest   = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 1];
    const prev     = aspaAdoptionDailyHistory[aspaAdoptionDailyHistory.length - 2];

    // Linear regression forecast
    function forecast(data, daysAhead) {
      if (data.length < 2) return null;
      const n = data.length;
      const xs = data.map((_, i) => i);
      const ys = data.map(d => d.coverage_pct_atlas);
      const xMean = xs.reduce((a,b)=>a+b,0)/n;
      const yMean = ys.reduce((a,b)=>a+b,0)/n;
      const num = xs.reduce((s,x,i)=>s+(x-xMean)*(ys[i]-yMean),0);
      const den = xs.reduce((s,x)=>s+(x-xMean)**2,0);
      if (den===0) return yMean;
      const slope = num/den;
      return Math.max(0, Math.min(100, Math.round((yMean + slope*(n-1+daysAhead))*100)/100));
    }

    const trend30 = getAdoptionTrend('30d');
    const result = {
      period,
      current: {
        date: latest?.date,
        aspa_objects: latest?.aspa_objects ?? rpkiAspaMap.size,
        roa_count: latest?.roa_count,
        atlas_asns_total: latest?.atlas_asns_total ?? 0,
        atlas_asns_with_aspa: latest?.atlas_asns_with_aspa ?? 0,
        coverage_pct_atlas: latest?.coverage_pct_atlas ?? 0,
        delta_from_previous: prev ? (latest?.aspa_objects ?? 0) - prev.aspa_objects : 0,
      },
      trend: trend.map(s => ({
        date: s.date,
        aspa_objects: s.aspa_objects,
        coverage_pct_atlas: s.coverage_pct_atlas,
        atlas_asns_total: s.atlas_asns_total,
      })),
      forecast: {
        predicted_90d: forecast(trend30, 90),
        confidence: trend30.length >= 10 ? 0.75 : 0.5,
        method: 'linear_regression',
        based_on_samples: trend30.length,
      },
      meta: {
        total_snapshots: aspaAdoptionDailyHistory.length,
        last_updated: latest?.date ?? null,
        denominator: 'atlas_visible_asns',
        source: 'rpki_cloudflare_feed',
      }
    };
    res.setHeader('Content-Type', 'application/json');
    res.writeHead(200);
    return res.end(JSON.stringify(result, null, 2));
  }

  if (reqPath === '/api/aspa-adoption-stats/export' && req.method === 'GET') {
    const fmt    = url.searchParams.get('format') === 'csv' ? 'csv' : 'json';
    const period = ['7d','30d','90d','1y'].includes(url.searchParams.get('period'))
      ? url.searchParams.get('period') : '30d';
    const data   = getAdoptionTrend(period);

    if (fmt === 'csv') {
      const header = 'date,aspa_objects,roa_count,atlas_asns_total,atlas_asns_with_aspa,coverage_pct_atlas,aspa_delta\n';
      const rows   = data.map(s =>
        `${s.date},${s.aspa_objects},${s.roa_count},${s.atlas_asns_total},${s.atlas_asns_with_aspa},${s.coverage_pct_atlas},${s.aspa_delta??0}`
      ).join('\n');
      res.setHeader('Content-Type', 'text/csv');
      res.setHeader('Content-Disposition', `attachment; filename="peercortex-aspa-adoption-${period}.csv"`);
      res.writeHead(200);
      return res.end(header + rows);
    }

    res.setHeader('Content-Type', 'application/json');
    res.setHeader('Content-Disposition', `attachment; filename="peercortex-aspa-adoption-${period}.json"`);
    res.writeHead(200);
    return res.end(JSON.stringify({ period, count: data.length, data }, null, 2));
  }

  // GET /api/ipv6-adoption-stats?refresh=1
  if (reqPath === '/api/ipv6-adoption-stats' && req.method === 'GET') {
    const force = url.searchParams.get('refresh') === '1';
    try {
      const data = await fetchIpv6AdoptionStats(force);
      res.setHeader('Content-Type', 'application/json');
      res.writeHead(200);
      return res.end(JSON.stringify(data, null, 2));
    } catch (err) {
      res.writeHead(503);
      return res.end(JSON.stringify({ error: 'IPv6 stats unavailable', message: err.message }));
    }
  }

  // 404
  res.writeHead(404);
  res.end(
    JSON.stringify({
      error: "Not found. Endpoints: /api/health, /api/validate?asn=X, /api/lookup?asn=X, /api/aspa?asn=X, /api/aspa/verify?asn=X, /api/bgproutes?asn=X, /api/compare?asn1=X&asn2=Y, /api/peers/find?ix=NAME, /api/prefix/detail?prefix=X, /api/ix/detail?ix_id=X, /api/export/pdf?asn=X&format=report|executive|technical, /api/export/pdf/compare?asn1=X&asn2=Y, /api/aspa-adoption-stats?period=30d, /api/ipv6-adoption-stats",
    })
  );
});


const { atlasState, fetchAllAtlasProbes } = require("./server/atlas-probes");

const { pdbOrgState, fetchPdbOrgCountries } = require("./server/pdb-org-countries");

const PORT = process.env.PORT || 3101;

// ============================================================
// Startup Sequence — load disk caches first, then fetch fresh data
// ============================================================

// Phase 0: Load disk caches for fast restart (instant)
roaStore.loadFromDisk("/opt/peercortex-app/.roa-cache.json");
pdbSourceCache.loadFromDisk("/opt/peercortex-app/.pdb-source-cache.json");
loadRipeStatCacheFromDisk("/opt/peercortex-app/.ripe-stat-cache.json");

// Phase 1: Fetch fresh RPKI feed (ASPA + ROA) + Atlas probes + PDB org countries + MANRS participants
ensureManrsCache(); // fire-and-forget, 24h cache
Promise.all([fetchRpkiAspaFeed(), fetchAllAtlasProbes(), fetchPdbOrgCountries()]).then(() => {
  // Re-record ASPA adoption snapshot now that Atlas data is fully loaded
  recordAspaAdoptionSnapshot(rpkiAspaMap.size, roaStore.count, rpkiAspaMap);

  server.listen(PORT, "0.0.0.0", () => {
    console.log("PeerCortex v0.6.5 running on http://0.0.0.0:" + PORT);
    console.log("bgproutes.io API key: " + (BGPROUTES_API_KEY ? "configured" : "NOT configured"));
    console.log("PeeringDB API key: " + (PEERINGDB_API_KEY ? "configured" : "NOT configured"));
    console.log("RPKI ASPA objects: " + rpkiAspaMap.size);
    console.log("ROA store: " + roaStore.count + " entries (" + (roaStore.ready ? "ready" : "loading...") + ")");
    console.log("PDB source cache: net=" + pdbSourceCache.net.size + " ix=" + pdbSourceCache.netixlan.size + " fac=" + pdbSourceCache.netfac.size);
    console.log("RIPE Stat cache: " + ripeStatCache.size + " entries");
  });
});

// ============================================================
// bio-rd RIB WebSocket — live route streaming on /ws/rib
// ============================================================
let WebSocketServer = null;
try { WebSocketServer = require('ws').Server; } catch(_e) {}

if (WebSocketServer) {
  const ribWss = new WebSocketServer({ server, path: '/ws/rib' });
  ribWss.on('connection', function(ws) {
    let cancelStream = null;

    ws.on('message', function(raw) {
      try {
        const msg = JSON.parse(raw);
        if (msg.type === 'rib-subscribe') {
          if (cancelStream) { cancelStream(); cancelStream = null; }
          if (!risClient) {
            ws.send(JSON.stringify({ type: 'error', error: 'bio-rd RIS not configured' }));
            return;
          }
          const router = msg.router || 'default';
          cancelStream = risClient.observeRib(
            router,
            'default',
            function(update) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(update)); },
            function(err) { if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: err.message })); }
          );
        }
      } catch(e) {
        if (ws.readyState === ws.OPEN) ws.send(JSON.stringify({ type: 'error', error: e.message }));
      }
    });

    ws.on('close', function() {
      if (cancelStream) { cancelStream(); cancelStream = null; }
    });
  });
  console.log('[bio-rd] RIB WebSocket server listening on /ws/rib');
} else {
  console.log('[bio-rd] WebSocket server skipped (ws package not installed)');
}

// ============================================================
// Refresh timers — jittered to avoid thundering herd
// ============================================================

// RPKI feed (ASPA + ROA): every 4h ± 5min jitter
setInterval(() => {
  fetchRpkiAspaFeed();
}, 4 * 60 * 60 * 1000 + Math.floor(Math.random() * 10 * 60 * 1000) - 5 * 60 * 1000);

// Atlas probe cache: every 12h ± 10min jitter
setInterval(function() {
  fetchAllAtlasProbes();
}, 12 * 60 * 60 * 1000 + Math.floor(Math.random() * 20 * 60 * 1000) - 10 * 60 * 1000);

// Disk cache persistence: every 30 minutes
setInterval(function() {
  pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json");
  saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json");
}, 30 * 60 * 1000);

// Save caches on graceful shutdown
process.on("SIGTERM", function() {
  console.log("[SHUTDOWN] Saving caches to disk...");
  pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json");
  saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json");
  roaStore.saveToDisk("/opt/peercortex-app/.roa-cache.json");
  process.exit(0);
});
process.on("SIGINT", function() {
  console.log("[SHUTDOWN] Saving caches to disk...");
  pdbSourceCache.saveToDisk("/opt/peercortex-app/.pdb-source-cache.json");
  saveRipeStatCacheToDisk("/opt/peercortex-app/.ripe-stat-cache.json");
  roaStore.saveToDisk("/opt/peercortex-app/.roa-cache.json");
  process.exit(0);
});
