const https = require("https");
const localDb = require("../../local-db-client");
const { UA } = require("../data/constants");
const { fetchJSON } = require("./http-helpers");
const { roaStore } = require("../caches/roa-store");
const { recordAspaAdoptionSnapshot } = require("../aspa-adoption/tracker");

// RPKI ASPA + ROA Cache from Cloudflare RPKI JSON feed
const rpkiAspaMap = new Map(); // customer_asid -> Set<provider_asn>
let rpkiAspaLastFetch = 0;
let rpkiAspaFetching = false;

function fetchRpkiAspaFeed() {
  if (rpkiAspaFetching) return Promise.resolve();
  rpkiAspaFetching = true;
  console.log("[RPKI] Fetching Cloudflare RPKI feed (ASPA + ROA)...");
  return new Promise((resolve) => {
    const options = {
      headers: { "User-Agent": UA },
      timeout: 120000,
    };
    https.get("https://rpki.cloudflare.com/rpki.json", options, (res) => {
      let data = "";
      res.on("data", (chunk) => (data += chunk));
      res.on("end", () => {
        try {
          const parsed = JSON.parse(data);

          // Load ASPA objects
          const aspas = parsed.aspas || [];
          rpkiAspaMap.clear();
          aspas.forEach((a) => {
            const customerAsid = Number(a.customer_asid);
            const providers = (a.providers || []).map(Number);
            rpkiAspaMap.set(customerAsid, new Set(providers));
          });

          // Load ROA objects into local store (eliminates RIPE Stat per-prefix calls)
          const roas = parsed.roas || [];
          roaStore.build(roas);
          roaStore.saveToDisk("/opt/peercortex-app/.roa-cache.json");

          // Track ASPA adoption — persist to disk + update in-memory trend
          recordAspaAdoptionSnapshot(rpkiAspaMap.size, roaStore.count, rpkiAspaMap);

          rpkiAspaLastFetch = Date.now();
          console.log("[RPKI] Loaded " + rpkiAspaMap.size + " ASPA objects + " + roaStore.count + " ROAs from Cloudflare feed");
        } catch (e) {
          console.error("[RPKI] Failed to parse RPKI feed:", e.message);
        }
        rpkiAspaFetching = false;
        resolve();
      });
    }).on("error", (e) => {
      console.error("[RPKI] Fetch failed:", e.message);
      rpkiAspaFetching = false;
      resolve();
    });
  });
}

// Ensure ASPA + ROA cache is fresh
async function ensureAspaCache() {
  if (Date.now() - rpkiAspaLastFetch > 4 * 60 * 60 * 1000) {
    await fetchRpkiAspaFeed();
  }
}

// Lookup ASPA object for a given ASN from the RPKI feed cache. feedLoaded
// distinguishes "the feed loaded fine and this ASN genuinely has no ASPA object"
// from "the Cloudflare feed fetch has never succeeded, so we don't actually know" --
// rpkiAspaLastFetch is only set on a successful parse (see fetchRpkiAspaFeed), so
// 0 means every attempt so far has failed and exists:false here is not trustworthy.
function lookupAspaFromRpki(asn) {
  const asnNum = Number(asn);
  const feedLoaded = rpkiAspaLastFetch > 0;
  if (rpkiAspaMap.has(asnNum)) {
    const providers = rpkiAspaMap.get(asnNum);
    return { exists: true, providers: [...providers].sort((a, b) => a - b), feedLoaded };
  }
  return { exists: false, providers: [], feedLoaded };
}

// Validate RPKI for a prefix — uses local PostgreSQL database (sub-10ms, zero external API calls)
// Returns: { prefix, status: "valid"|"invalid"|"not_found"|"unavailable", validating_roas: N }
// "not_found" means the DB was queried and genuinely has no covering ROA -- a real result.
// "unavailable" means the DB query itself failed (connection/timeout/error) -- we don't know.
// Conflating these used to make DB hiccups masquerade as "no ROA published" (a real, worse-
// sounding finding). Callers computing coverage percentages should exclude "unavailable".
async function validateRPKIWithCache(asn, prefix) {
  try {
    const result = await localDb.validateRpki(prefix, asn);
    if (result.status === 'valid') {
      return { prefix, status: "valid", validating_roas: 1 };
    } else if (result.status === 'invalid') {
      return { prefix, status: "invalid", validating_roas: 1 };
    } else if (result.status === 'unknown') {
      return { prefix, status: "unavailable", validating_roas: 0 };
    } else {
      // 'not-found': genuinely no covering ROA
      return { prefix, status: "not_found", validating_roas: 0 };
    }
  } catch (_e) {
    console.error("[RPKI] Error validating " + prefix + ":", _e.message);
    return { prefix, status: "unavailable", validating_roas: 0 };
  }
}

// Feature 30: RIPE NCC RPKI Validator cross-check (max 5 prefixes)
async function fetchRipeRpkiValidator(asn, prefix) {
  try {
    const encoded = encodeURIComponent(prefix);
    const url = "https://rpki-validator.ripe.net/api/v1/validity/AS" + asn + "/" + encoded;
    const result = await fetchJSON(url, { timeout: 5000 });
    if (result && result.validated_route) {
      return {
        prefix: prefix,
        validity: result.validated_route.validity || {},
        state: (result.validated_route.validity && result.validated_route.validity.state) || "unknown",
      };
    }
    return { prefix: prefix, state: "unknown", error: "no_data" };
  } catch (_e) {
    return { prefix: prefix, state: "error", error: "timeout_or_unavailable" };
  }
}

// Cross-check a sample of prefixes against RIPE RPKI Validator (max 5, in parallel).
// agreement_pct only counts pairs where BOTH sources actually returned a real
// verdict -- a zero-sample or all-RIPE-lookups-failed result reports agreement_pct:
// null (not a fabricated 100), since nothing was actually cross-checked.
async function crossCheckRpki(asn, prefixes, localResults) {
  const sample = prefixes.slice(0, 5);
  if (sample.length === 0) return { cloudflare_valid: 0, ripe_valid: 0, agreement_pct: null, disagreements: [], sample_size: 0, comparable_size: 0 };

  const ripeResults = await Promise.all(
    sample.map((pfx) => fetchRipeRpkiValidator(asn, pfx))
  );

  const localMap = new Map();
  for (const lr of localResults) {
    localMap.set(lr.prefix, lr.status);
  }

  let cloudflareValid = 0;
  let ripeValid = 0;
  let agreements = 0;
  let comparable = 0;
  const disagreements = [];

  for (let i = 0; i < sample.length; i++) {
    const pfx = sample[i];
    const cfStatus = localMap.get(pfx) || "not_found";
    const ripeState = ripeResults[i].state;

    const cfIsValid = cfStatus === "valid";
    const ripeIsValid = ripeState === "valid" || ripeState === "VALID";

    if (cfIsValid) cloudflareValid++;
    if (ripeIsValid) ripeValid++;

    // A failed/unknown RIPE lookup isn't a comparison at all -- exclude it from
    // the agreement percentage instead of counting it as a silent "agreement".
    if (ripeState === "error" || ripeState === "unknown") {
      continue;
    }

    comparable++;
    if (cfIsValid === ripeIsValid) {
      agreements++;
    } else {
      disagreements.push({
        prefix: pfx,
        cloudflare: cfStatus,
        ripe: ripeState,
      });
    }
  }

  const agreementPct = comparable > 0 ? Math.round((agreements / comparable) * 100) : null;
  return { cloudflare_valid: cloudflareValid, ripe_valid: ripeValid, agreement_pct: agreementPct, disagreements: disagreements, sample_size: sample.length, comparable_size: comparable };
}

// rpkiAspaLastFetch is a reassigned `let`, not a mutated object -- a bare
// export would only capture its value (0) at require time. Expose it via a
// getter so callers (e.g. /api/health) always see the current value.
function getRpkiAspaLastFetch() {
  return rpkiAspaLastFetch;
}

module.exports = {
  rpkiAspaMap,
  getRpkiAspaLastFetch,
  fetchRpkiAspaFeed,
  ensureAspaCache,
  lookupAspaFromRpki,
  validateRPKIWithCache,
  fetchRipeRpkiValidator,
  crossCheckRpki,
};
