const { fetchRipeStatCached } = require("../services/ripe-stat");
const { hasAsSet, verifyUpstream, verifyDownstream, detectValleys, collapsePrepends, calculateAspaReadinessScore } = require("../aspa-verification/engine");
const { resolveASNames } = require("../resolve-as-names");
const { ensureAspaCache, lookupAspaFromRpki, rpkiAspaMap, validateRPKIWithCache } = require("../services/rpki");
const { aspaResultCache, resultCacheGet, resultCacheSet } = require("../caches/response-caches");

// ASPA Deep Verification endpoint: /api/aspa/verify?asn=X
async function handleAspaVerify(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 cachedVerify = resultCacheGet(aspaResultCache, "verify:" + rawAsn);
    if (cachedVerify !== undefined) {
      res.writeHead(200, { "Content-Type": "application/json" });
      return res.end(JSON.stringify(cachedVerify));
    }
    const targetAsn = parseInt(rawAsn);
    const start = Date.now();

    try {
      // Fetch neighbour and prefix data first
      const [neighbourData, prefixData] = await Promise.all([
        fetchRipeStatCached("https://stat.ripe.net/data/asn-neighbours/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
        fetchRipeStatCached("https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS" + rawAsn, { timeout: 5000 }),
      ]);

      // Use looking-glass with actual prefixes to get BGP paths
      const announcedPrefixes = prefixData?.data?.prefixes || [];
      const samplePrefixes = announcedPrefixes.slice(0, 3).map((p) => p.prefix); // reduced 5→3

      // Fetch looking-glass data for up to 3 prefixes in parallel (3s timeout each)
      const lgResults = await Promise.all(
        samplePrefixes.map((pfx) =>
          fetchRipeStatCached("https://stat.ripe.net/data/looking-glass/data.json?resource=" + encodeURIComponent(pfx), { timeout: 3000 }).catch(() => null)
        )
      );

      // Extract AS paths from looking glass results
      const allPaths = [];
      const pathNeighbourCount = new Map(); // Count how often each AS appears next to target in paths

      lgResults.forEach((lgData) => {
        const rrcs = lgData?.data?.rrcs || [];
        rrcs.forEach((rrc) => {
          const peers = rrc.peers || [];
          peers.forEach((peer) => {
            const rawPath = peer.as_path || "";
            const pathArr = rawPath.split(" ").map(Number).filter(Boolean);
            if (pathArr.length > 1) {
              allPaths.push({
                rrc: rrc.rrc,
                path: pathArr,
                rawPath: rawPath,
                prefix: peer.prefix || "",
                hasAsSet: hasAsSet(rawPath),
              });
              const idx = pathArr.indexOf(targetAsn);
              if (idx > 0) {
                const neighbour = pathArr[idx - 1];
                pathNeighbourCount.set(neighbour, (pathNeighbourCount.get(neighbour) || 0) + 1);
              }
            }
          });
        });
      });

      // Provider detection: ONLY use RIPE Stat "left" neighbours (verified upstreams)
      // AS-path analysis is used for frequency/confirmation, NOT as standalone provider source
      const neighbours = neighbourData?.data?.neighbours || [];
      const leftNeighbours = neighbours.filter((n) => n.type === "left");
      const upstreamSet = new Set();
      leftNeighbours.forEach((n) => upstreamSet.add(n.asn));

      // Classify left neighbours: high-power = likely upstream, low-power = likely peer
      const maxPower = leftNeighbours.reduce((m, n) => Math.max(m, n.power || 0), 1);
      const detectedProviders = [...upstreamSet].map((asn) => {
        const nb = leftNeighbours.find((n) => n.asn === asn);
        const power = nb ? (nb.power || 0) : 0;
        const powerPct = Math.round((power / maxPower) * 100);
        const classification = powerPct >= 10 ? "likely_upstream" : "likely_peer";
        return { asn, name: nb && nb.as_name ? nb.as_name : "", power, power_pct: powerPct, classification };
      });

      await resolveASNames(detectedProviders);

      // Count how often each provider appears in paths
      const providerFrequency = new Map();
      allPaths.forEach((p) => {
        const idx = p.path.indexOf(targetAsn);
        if (idx > 0) {
          const prov = p.path[idx - 1];
          providerFrequency.set(prov, (providerFrequency.get(prov) || 0) + 1);
        }
      });

      // Check Cloudflare RPKI feed for ASPA object
      await ensureAspaCache();
      const aspaLookup = lookupAspaFromRpki(targetAsn);
      const aspaObjectExists = aspaLookup.exists;
      const aspaDeclaredProviders = aspaLookup.providers;

      // Build ASPA store from RPKI feed data (real ASPA objects only). If the
      // target has no real ASPA object, its hops must show "NoAttestation" --
      // NOT a fabricated declaration built from heuristically-detected BGP
      // neighbours, which would contradict the aspa_object_exists:false this
      // same response reports and falsely present unattested hops as
      // "ProviderPlus"/Valid. hopCheck() already returns "NoAttestation" for
      // any ASN with no entry in aspaStore, so simply not setting one here is
      // correct and sufficient -- no fallback needed.
      const aspaStore = new Map();
      if (aspaObjectExists) {
        aspaStore.set(targetAsn, new Set(aspaDeclaredProviders));
      }
      // Also populate store with all known ASPA objects from the RPKI feed
      // for providers that have their own ASPA objects (enables full path verification)
      for (const [cas, provSet] of rpkiAspaMap) {
        if (!aspaStore.has(cas)) {
          aspaStore.set(cas, provSet);
        }
      }
      // Deliberately NOT adding empty Set() entries for detected-but-unattested
      // providers here: aspaStore.get() returning undefined for them makes
      // hopCheck() correctly report "NoAttestation". An empty (but present) Set
      // would make providers.has(x) return false, misreporting "NotProviderPlus"
      // (implying a real ASPA object that denies this hop) instead of "we simply
      // don't know" -- see the 2026-07-16 audit for how this previously caused
      // clean, unattested paths to be misclassified as route leaks.

      // Sample paths for verification (up to 50)
      const samplePaths = allPaths.slice(0, 50);
      const pathResults = samplePaths.map((p) => {
        const upstream = verifyUpstream(p.path, aspaStore, p.rawPath);
        const downstream = verifyDownstream(p.path, aspaStore, p.rawPath);
        const valleys = detectValleys(p.path, aspaStore);

        return {
          rrc: p.rrc,
          prefix: p.prefix,
          path: p.path.map((a) => "AS" + a).join(" "),
          collapsed_path: collapsePrepends(p.path).map((a) => "AS" + a).join(" "),
          has_as_set: p.hasAsSet,
          upstream_verification: upstream,
          downstream_verification: downstream,
          valleys: valleys,
          overall: p.hasAsSet
            ? "Invalid"
            : upstream.result === "Valid" && downstream.result === "Valid"
            ? "Valid"
            : upstream.result === "Invalid" || downstream.result === "Invalid"
            ? "Invalid"
            : "Unknown",
        };
      });

      // Calculate statistics
      const validPaths = pathResults.filter((p) => p.overall === "Valid").length;
      const invalidPaths = pathResults.filter((p) => p.overall === "Invalid").length;
      const unknownPaths = pathResults.filter((p) => p.overall === "Unknown").length;
      const asSetPaths = pathResults.filter((p) => p.has_as_set).length;
      const valleyPaths = pathResults.filter((p) => p.valleys.length > 0).length;

      // For readiness scoring: Valid = full credit, Unknown = partial (no ASPA data is normal),
      // only Invalid actually indicates problems
      const pathNotInvalidPct = pathResults.length > 0
        ? Math.round(((validPaths + unknownPaths) / pathResults.length) * 100)
        : 0;
      const pathValidPct = pathResults.length > 0 ? Math.round((validPaths / pathResults.length) * 100) : 0;

      // Provider audit: compare detected vs declared
      const detectedSet = new Set(detectedProviders.map((p) => p.asn));
      const declaredSet = new Set(aspaDeclaredProviders);

      const missingFromAspa = detectedProviders
        .filter((p) => !declaredSet.has(p.asn))
        .map((p) => ({
          asn: p.asn,
          name: p.name,
          frequency: providerFrequency.get(p.asn) || 0,
          frequency_pct: allPaths.length > 0
            ? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
            : 0,
        }))
        .sort((a, b) => b.frequency - a.frequency);

      const extraInAspa = aspaDeclaredProviders
        .filter((asn) => !detectedSet.has(asn))
        .map((asn) => ({
          asn,
          name: "",
          seen_in_paths: false,
        }));

      const providerCompleteness = detectedProviders.length > 0
        ? Math.round(
            (detectedProviders.filter((p) => declaredSet.has(p.asn)).length /
              detectedProviders.length) *
              100
          )
        : aspaObjectExists ? 100 : 0;

      // Get RPKI coverage for readiness score
      // Validate ALL prefixes using local RPKI data (Cloudflare feed - all 5 RIRs)
      await ensureAspaCache();
      const rpkiBatch = announcedPrefixes.map((p) => p.prefix);
      const rpkiResults = await Promise.all(rpkiBatch.map((pfx) => validateRPKIWithCache(rawAsn, pfx)));
      // Exclude "unavailable" (DB error) from the coverage denominator -- a DB hiccup
      // must not lower a network's displayed ASPA readiness score.
      const rpkiChecked = rpkiResults.filter((r) => r.status !== "unavailable");
      const rpkiValid = rpkiChecked.filter((r) => r.status === "valid").length;
      const rpkiCoverage = rpkiChecked.length > 0 ? Math.round((rpkiValid / rpkiChecked.length) * 100) : 0;

      // Calculate readiness score
      const readinessScore = calculateAspaReadinessScore({
        rpkiCoverage,
        aspaObjectExists,
        providerCompleteness,
        pathValidationPct: pathNotInvalidPct,
      });

      const duration = Date.now() - start;
      const verifyResult = {
        meta: {
          query: "AS" + rawAsn,
          duration_ms: duration,
          timestamp: new Date().toISOString(),
          paths_analyzed: pathResults.length,
          total_paths_seen: allPaths.length,
        },
        asn: targetAsn,
        readiness_score: readinessScore,
        aspa_object_exists: aspaObjectExists,
        aspa_feed_healthy: aspaLookup.feedLoaded,
        detected_providers: detectedProviders.map((p) => ({
          ...p,
          frequency: providerFrequency.get(p.asn) || 0,
          frequency_pct: allPaths.length > 0
            ? Math.round(((providerFrequency.get(p.asn) || 0) / allPaths.length) * 100)
            : 0,
        })),
        provider_audit: {
          declared_count: aspaDeclaredProviders.length,
          detected_count: detectedProviders.length,
          completeness_pct: providerCompleteness,
          missing_from_aspa: missingFromAspa,
          extra_in_aspa: extraInAspa,
        },
        path_verification: {
          total: pathResults.length,
          valid: validPaths,
          invalid: invalidPaths,
          unknown: unknownPaths,
          as_set_flagged: asSetPaths,
          valley_detected: valleyPaths,
          valid_pct: pathValidPct,
          not_invalid_pct: pathNotInvalidPct,
          results: pathResults,
        },
        rpki_coverage: rpkiCoverage,
      };
      resultCacheSet(aspaResultCache, "verify:" + rawAsn, verifyResult);
      return res.end(JSON.stringify(verifyResult, null, 2));
    } catch (err) {
      res.writeHead(500);
      return res.end(JSON.stringify({ error: "ASPA verification failed", message: err.message }));
    }

}

module.exports = { handleAspaVerify };
