const localDb = require("../../../local-db-client");
const { fetchJSON } = require("../../services/http-helpers");
const { fetchPeeringDB, fetchPeeringDBWithRetry } = require("../../services/peeringdb");
const { ensureAspaCache, validateRPKIWithCache } = require("../../services/rpki");
const { computeResilienceScore } = require("../../resilience-score");
const { computeRouteLeakDetection } = require("../../route-leak");
const { pdbSourceCache } = require("../../caches/pdb-source-cache");
const {
  cacheGet, cacheSet, CACHE_TTL_LOOKUP, CACHE_TTL_LOOKUP_DEGRADED,
  rdapCacheGet, rdapCacheSet,
} = require("../../caches/response-caches");
const { resolveFacilitiesAndIxLocations } = require("./facilities");
const { enrichNeighbours } = require("./enrich-neighbours");
const { deriveRirAndCountry } = require("./rir-country");
const { computeRoutingInfo } = require("./routing-info");
const { computeCrossChecks } = require("./cross-check");

// Main lookup endpoint: /api/lookup?asn=X
async function handleLookup(req, res, url) {
  const rawAsn = (url.searchParams.get("asn") || "").replace(/[^0-9]/g, "");
  if (!rawAsn) {
    res.writeHead(400);
    return res.end(JSON.stringify({ error: "Missing or invalid ASN parameter" }));
  }
  const asn = rawAsn;
  const cacheKey = "lookup:" + asn;
  const cached = cacheGet(cacheKey);
  if (cached) {
    res.writeHead(200, { "Content-Type": "application/json", "X-Cache": "HIT" });
    return res.end(JSON.stringify(cached));
  }
  const start = Date.now();

  try {
    // Phase 0: Get PDB net first — check L2 cache, then API with retry
    let pdbNet = pdbSourceCache.get("net", asn);
    if (!pdbNet) {
      pdbNet = await fetchPeeringDBWithRetry("/net?asn=" + asn);
      if (pdbNet) pdbSourceCache.set("net", asn, pdbNet);
    }
    const net = pdbNet?.data?.[0] || {};
    const netId = net.id;

    // Phase 1: ALL calls in parallel — RIPE Stat (cached+throttled) + PDB IX/Fac (cached) + Atlas + bgp.he.net
    const ixQuery = netId
      ? "/netixlan?net_id=" + netId + "&limit=1000"
      : "/netixlan?asn=" + asn + "&limit=1000";
    const ixCacheKey = netId ? String(netId) : "asn:" + asn;

    // Check PDB source cache for IX/Fac data
    let cachedIxlan = pdbSourceCache.get("netixlan", ixCacheKey);
    let cachedFac = netId ? pdbSourceCache.get("netfac", String(netId)) : null;

    // Per-source timing tracking — 9s hard cap per source to prevent long-tail blocking
    const sourceTiming = {};
    function timedFetch(name, promise) {
      const ts = Date.now();
      return Promise.race([
        Promise.resolve(promise),
        new Promise(function(r) { setTimeout(function() { r(null); }, 9000); }),
      ])
        .then(function(r)  { sourceTiming[name] = Date.now() - ts; return r; })
        .catch(function() { sourceTiming[name] = null; return null; });
    }

    const pocQuery = netId ? "/poc?net_id=" + netId + "&limit=25" : null;

    // RDAP: check module-level cache first, only hit RIR endpoints on cache miss
    const rdapCached = rdapCacheGet(asn);
    const rdapPromise = rdapCached !== undefined
      ? Promise.resolve(rdapCached)
      : Promise.race([
          ...["https://rdap.db.ripe.net/autnum/"+asn, "https://rdap.arin.net/registry/autnum/"+asn,
              "https://rdap.apnic.net/autnum/"+asn, "https://rdap.lacnic.net/rdap/autnum/"+asn,
              "https://rdap.afrinic.net/rdap/autnum/"+asn].map(url =>
            fetchJSON(url, { timeout: 4000 })
              .then(d => (d && !d.errorCode && d.handle) ? d : new Promise(() => {}))
              .catch(() => new Promise(() => {}))
          ),
          new Promise(resolve => setTimeout(() => resolve(null), 5000)),
        ]).then(d => { rdapCacheSet(asn, d); return d; });

    const promises = [
      timedFetch("RIPE Stat Prefixes",    localDb ? localDb.getRipeStatAnnouncedPrefixes(asn) : Promise.resolve(null)),
      timedFetch("RIPE Stat Neighbours",  localDb ? localDb.getRipeStatAsnNeighbours(asn) : Promise.resolve(null)),
      timedFetch("RIPE Stat Overview",    localDb ? localDb.getRipeStatAsOverview(asn) : Promise.resolve(null)),
      timedFetch("RIPE Stat RIR",         Promise.resolve(null)),
      timedFetch("RIPE Atlas",            Promise.resolve(null)),
      timedFetch("bgp.he.net",            Promise.resolve(null)),
      timedFetch("RIPE Stat Visibility",  localDb ? localDb.getRipeStatVisibility(asn) : Promise.resolve(null)),
      timedFetch("RIPE Stat PrefixSize",  localDb ? localDb.getRipeStatPrefixSizeDistribution(asn) : Promise.resolve(null)),
      timedFetch("PeeringDB IXLan",       cachedIxlan ? Promise.resolve(cachedIxlan) : fetchPeeringDBWithRetry(ixQuery)),
      timedFetch("PeeringDB Facilities",  cachedFac ? Promise.resolve(cachedFac) : (netId ? fetchPeeringDBWithRetry("/netfac?net_id=" + netId + "&limit=1000") : Promise.resolve(null))),
      timedFetch("PeeringDB Contacts",    pocQuery ? fetchPeeringDB(pocQuery).catch(() => null) : Promise.resolve(null)),
      timedFetch("RDAP Registration",     rdapPromise),
    ];
    const [prefixData, neighbourData, overviewData, rirData, atlasProbeData, bgpHeData, visibilityData, prefixSizeData, ixlanData, facData, pocData, rdapData] = await Promise.all(promises);

    // Store PDB results in L2 source cache for future lookups
    if (!cachedIxlan && ixlanData) pdbSourceCache.set("netixlan", ixCacheKey, ixlanData);
    if (!cachedFac && facData) pdbSourceCache.set("netfac", String(netId), facData);

    // local-db-client's getRipeStat* functions return status:"error" (not "ok") when
    // the local Postgres DB query itself failed -- track which sources actually
    // failed so we don't cache a DB hiccup's fake-empty result as if it were a
    // genuine "this ASN has 0 prefixes" answer for the full 5-minute TTL.
    const degradedSources = [
      ["RIPE Stat Prefixes", prefixData], ["RIPE Stat Neighbours", neighbourData],
      ["RIPE Stat Overview", overviewData], ["RIPE Stat Visibility", visibilityData],
      ["RIPE Stat PrefixSize", prefixSizeData],
    ].filter(([, v]) => v && v.status === "error").map(([name]) => name);

    const prefixes = prefixData?.data?.prefixes || [];
    const neighbours = neighbourData?.data?.neighbours || [];
    const overview = overviewData?.data || {};
    const rirEntries = rirData?.data?.located_resources || rirData?.data?.rir_stats || [];

    // Bug 6 fix: Atlas probe status uses status.name (object), not status_name (flat)
    const atlasProbes = atlasProbeData?.results || [];
    const atlasConnected = atlasProbes.filter(p => {
      const sName = (p.status_name || (p.status && p.status.name) || "").toLowerCase();
      return sName === "connected";
    });
    const atlasAnchors = atlasProbes.filter(p => p.is_anchor === true);

    // RPKI: validate ALL prefixes using local Cloudflare RPKI data (all 5 RIRs, instant)
    await ensureAspaCache();
    const allPrefixes = prefixes.map((p) => p.prefix);
    const rpkiAllResults = await Promise.all(allPrefixes.map((pfx) => validateRPKIWithCache(asn, pfx)));

    const ixConnections = (ixlanData?.data || [])
      .map((ix) => ({
        ix_name: ix.name || "",
        ix_id: ix.ix_id,
        speed_mbps: ix.speed || 0,
        ipv4: ix.ipaddr4 || null,
        ipv6: ix.ipaddr6 || null,
        city: ix.city || "",
        is_rs_peer: ix.is_rs_peer === true,
      }))
      .sort((a, b) => b.speed_mbps - a.speed_mbps);

    const facilitiesRaw = (facData?.data || []).map((f) => ({
      fac_id: f.fac_id,
      name: f.name || "",
      city: f.city || "",
      country: f.country || "",
    }));

    const { facilities, ixLocations } = await resolveFacilitiesAndIxLocations(facilitiesRaw, ixConnections);

    const rpkiStatuses = rpkiAllResults;
    const rpkiValid = rpkiStatuses.filter((r) => r.status === "valid").length;
    const rpkiInvalid = rpkiStatuses.filter((r) => r.status === "invalid").length;
    const rpkiUnavailable = rpkiStatuses.filter((r) => r.status === "unavailable").length;
    const rpkiNotFound = rpkiStatuses.filter((r) => r.status !== "valid" && r.status !== "invalid" && r.status !== "unavailable").length;
    // Exclude "unavailable" (DB error) from the coverage denominator -- a DB hiccup
    // must not lower a network's displayed RPKI coverage percentage.
    const rpkiTotal = rpkiStatuses.length - rpkiUnavailable;
    const rpkiCoverage = rpkiTotal > 0 ? Math.round((rpkiValid / rpkiTotal) * 100) : 0;

    let upstreams = neighbours
      .filter((n) => n.type === "left")
      .map((n) => ({ asn: n.asn, name: n.as_name || "", power: n.power || 0 }))
      .sort((a, b) => b.power - a.power);
    let downstreams = neighbours
      .filter((n) => n.type === "right")
      .map((n) => ({ asn: n.asn, name: n.as_name || "", power: n.power || 0 }))
      .sort((a, b) => b.power - a.power);
    let peers = neighbours
      .filter((n) => n.type === "uncertain" || n.type === "peer")
      .map((n) => ({ asn: n.asn, name: n.as_name || "", power: n.power || 0 }))
      .sort((a, b) => b.power - a.power);

    const enriched = await enrichNeighbours(upstreams, downstreams, peers);
    upstreams = enriched.upstreams;
    downstreams = enriched.downstreams;
    peers = enriched.peers;

    const { rir, country } = deriveRirAndCountry(rirEntries, rirData, rdapData, bgpHeData);

    const duration = Date.now() - start;

    const routingInfo = await computeRoutingInfo(prefixes, visibilityData, prefixSizeData);

    const { rpkiCrossCheck, prefixCrossCheck, neighbourCrossCheck, dataQuality } =
      await computeCrossChecks(asn, allPrefixes, rpkiStatuses, prefixes, neighbours, bgpHeData);

    const result = {
      meta: {
        service: "PeerCortex",
        version: "0.6.9",
        query: "AS" + asn,
        duration_ms: duration,
        sources: ["PeeringDB", "RIPE Stat", "bgp.he.net", "Cloudflare RPKI", "RIPE RPKI Validator", "Route Views"],
        timestamp: new Date().toISOString(),
        rpki_prefixes_checked: rpkiTotal,
        total_prefixes: prefixes.length,
        degraded_sources: degradedSources,
      },
      network: {
        asn: parseInt(asn),
        name: net.name || overview?.holder || (bgpHeData && bgpHeData.name_from_title) || "Unknown",
        aka: net.aka || "",
        org_name: (net.org && net.org.name) ? net.org.name : "",
        website: net.website || "",
        type: net.info_type || "",
        policy: net.policy_general || "",
        traffic: net.info_traffic || "",
        ratio: net.info_ratio || "",
        scope: net.info_scope || "",
        notes: net.notes ? net.notes.substring(0, 500) : "",
        peeringdb_id: netId || null,
        rir: rir,
        country: country,
        city: net.city || "",
        latitude: (net.latitude != null) ? net.latitude : null,
        longitude: (net.longitude != null) ? net.longitude : null,
        looking_glass: net.looking_glass || "",
        route_server: net.route_server || "",
        info_prefixes4: net.info_prefixes4 || 0,
        info_prefixes6: net.info_prefixes6 || 0,
        status: net.status || "",
        peeringdb_created: net.created ? net.created.slice(0, 10) : "",
        peeringdb_updated: net.updated ? net.updated.slice(0, 10) : "",
      },
      prefixes: {
        total: prefixes.length,
        ipv4: prefixes.filter((p) => !p.prefix.includes(":")).length,
        ipv6: prefixes.filter((p) => p.prefix.includes(":")).length,
        list: prefixes.map((p) => p.prefix),
        cross_check: prefixCrossCheck,
      },
      rpki: {
        coverage_percent: rpkiCoverage,
        valid: rpkiValid,
        invalid: rpkiInvalid,
        not_found: rpkiNotFound,
        unavailable: rpkiUnavailable,
        checked: rpkiTotal,
        details: rpkiStatuses,
        cross_check: rpkiCrossCheck,
      },
      neighbours: {
        total: neighbours.length,
        upstream_count: upstreams.length,
        downstream_count: downstreams.length,
        peer_count: peers.length,
        upstreams: upstreams.slice(0, 20),
        downstreams: downstreams.slice(0, 20),
        peers: peers.slice(0, 20),
        cross_check: neighbourCrossCheck,
      },
      ix_presence: {
        total_connections: ixConnections.length,
        unique_ixps: [...new Set(ixConnections.map((ix) => ix.ix_id))].length,
        connections: ixConnections,
      },
      ix_locations: ixLocations,
      facilities: {
        total: facilities.length,
        list: facilities,
      },
      routing: routingInfo,
      resilience_score: computeResilienceScore(upstreams, peers, ixConnections, prefixes),
      route_leak: computeRouteLeakDetection(upstreams, downstreams, peers),
      bgp_he_net: bgpHeData || null,
      atlas: {
        total_probes: atlasProbes.length,
        connected: atlasConnected.length,
        disconnected: atlasProbes.length - atlasConnected.length,
        anchors: atlasAnchors.length,
        probes: atlasProbes.slice(0, 100).map(p => ({
          id: p.id,
          status: p.status_name || p.status || "Unknown",
          is_anchor: p.is_anchor || false,
          country: p.country_code || "",
          prefix_v4: p.prefix_v4 || "",
          prefix_v6: p.prefix_v6 || "",
          description: p.description || "",
        })),
      },
      data_quality: dataQuality,
      source_timing: sourceTiming,
      contacts: (() => {
        const pocs = (pocData && pocData.data) ? pocData.data : [];
        return pocs.slice(0, 20).map(p => ({
          role: p.role || "",
          name: p.name || "",
          email: p.email || "",
          url: p.url || "",
          visible: p.visible || "",
        }));
      })(),
      registration: (() => {
        const events = (rdapData && rdapData.events) ? rdapData.events : [];
        const created   = (events.find(e => e.eventAction === "registration")  || {}).eventDate || "";
        const lastChg   = (events.find(e => e.eventAction === "last changed")  || {}).eventDate || "";
        return {
          created:       created   ? created.slice(0, 10)   : "",
          last_modified: lastChg   ? lastChg.slice(0, 10)   : "",
          rir:           rir       || "",
          handle:        (rdapData && rdapData.handle) ? rdapData.handle : ("AS" + asn),
          rdap_source:   (rdapData && rdapData.port43) ? rdapData.port43 : "",
        };
      })(),
      _provenance: {
        prefixes:       { source: "RIPE Stat announced-prefixes", validation: "cross-validated", confidence: "high",   note: "Cross-checked with bgp.he.net prefix count daily" },
        neighbours:     { source: "RIPE Stat asn-neighbours",     validation: "cross-validated", confidence: "high",   note: "Cross-checked with bgp.he.net peer count daily" },
        rpki:           { source: "Cloudflare RPKI + RIPE Validator", validation: "cross-validated", confidence: "high", note: "Two independent RPKI sources compared" },
        ix_presence:    { source: "PeeringDB netixlan (local mirror)", validation: "cross-validated", confidence: "high", note: "PeeringDB mirror refreshed daily" },
        facilities:     { source: "PeeringDB netfac (local mirror)",  validation: "single-source",  confidence: "medium" },
        bgp_he_net:     { source: "bgp.he.net HTML scrape",           validation: "single-source",  confidence: "medium", note: "HTML scrape, no official API — may have parsing drift" },
        atlas:          { source: "RIPE Atlas API",                   validation: "single-source",  confidence: "medium", note: "Probe availability varies by region" },
        resilience_score: { source: "Computed from RIPE Stat + PeeringDB", validation: "computed",  confidence: "high",   note: "All inputs cross-validated daily" },
        route_leak:     { source: "RIPE Stat asn-neighbours heuristic", validation: "heuristic",    confidence: "medium", note: "Pattern-based, not real-time — false positives possible" },
        registration:   { source: "RDAP (RIR registry)",              validation: "single-source",  confidence: "high" },
        contacts:       { source: "PeeringDB POC API",                validation: "single-source",  confidence: "medium", note: "Subject to PeeringDB rate limiting" },
      },
    };

    // Update duration to include cross-check time
    result.meta.duration_ms = Date.now() - start;

    // A DB hiccup shouldn't get cached as this ASN's answer for the full 5min --
    // retry soon instead of repeating a degraded result to every visitor.
    cacheSet(cacheKey, result, degradedSources.length > 0 ? CACHE_TTL_LOOKUP_DEGRADED : CACHE_TTL_LOOKUP);
    res.end(JSON.stringify(result, null, 2));
  } catch (err) {
    const duration = Date.now() - start;
    res.writeHead(500);
    res.end(JSON.stringify({ error: "Lookup failed", message: err.message, duration_ms: duration }));
  }
  return;
}

module.exports = { handleLookup };
