const { fetchJSON, fetchBgproutesVisibility } = require("../../services/http-helpers");
const { fetchRipeStatCached } = require("../../services/ripe-stat");
const { fetchPeeringDB } = require("../../services/peeringdb");
const { validateRPKIWithCache } = require("../../services/rpki");
const { ensureManrsCache, checkManrsMembership } = require("../../services/manrs");

// 12. IRR Validation
// fetchJSON resolves(null) on timeout/network-error/parse-failure rather than
// rejecting, so the .catch() below never sees the common failure mode -- a
// dead upstream would otherwise silently read as "0 entries, no mismatches, pass".
function checkIRR(rawAsn) {
  return fetchJSON("https://irrexplorer.nlnog.net/api/prefixes/asn/" + rawAsn).then(function(irrData) {
    if (irrData === null) {
      return { status: "info", message: "IRR Explorer unavailable (fetch failed)", total_entries: 0, mismatches: [], mismatch_count: 0 };
    }
    var entries = Array.isArray(irrData) ? irrData : [];
    var mismatches = entries.filter(function(e) {
      if (!e.bgpOrigins && !e.bgp_origins) return false;
      if (!e.irrRoutes && !e.irr_origins) return false;
      var bgpArr = e.bgpOrigins || e.bgp_origins || [];
      var irrArr = e.irrRoutes || e.irr_origins || [];
      var bgpSet = {};
      bgpArr.forEach(function(a) { bgpSet[String(typeof a === "object" ? a.asn : a)] = true; });
      var match = false;
      irrArr.forEach(function(a) { if (bgpSet[String(typeof a === "object" ? a.asn : a)]) match = true; });
      return Object.keys(bgpSet).length > 0 && irrArr.length > 0 && !match;
    });
    return {
      status: mismatches.length === 0 ? "pass" : "warning",
      total_entries: entries.length,
      mismatches: mismatches.slice(0, 10).map(function(e) { return { prefix: e.prefix, bgp_origins: e.bgpOrigins || e.bgp_origins, irr_origins: e.irrRoutes || e.irr_origins }; }),
      mismatch_count: mismatches.length,
    };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 13. RPKI ROA Completeness (local validation against Cloudflare RPKI feed - all RIRs)
// Caller must await ensureAspaCache() before calling this.
function checkRpkiCompleteness(rawAsn, allPrefixes) {
  return Promise.all(
    allPrefixes.map(function(pfx) { return validateRPKIWithCache(rawAsn, pfx); })
  ).then(function(rpkiResults) {
    var checkedResults = rpkiResults.filter(function(r) { return r.status !== "unavailable"; });
    var unavailableCount = rpkiResults.length - checkedResults.length;
    var withRoa = checkedResults.filter(function(r) { return r.status === "valid"; });
    var coverage = checkedResults.length > 0 ? Math.round((withRoa.length / checkedResults.length) * 100) : 0;
    var overSpecific = checkedResults.filter(function(r) {
      var mask = parseInt((r.prefix || "").split("/")[1] || "0");
      return !r.prefix.includes(":") && mask >= 25 && r.status !== "valid";
    });
    // If the local RPKI DB was unreachable for every prefix, we haven't actually
    // checked anything -- say so instead of reporting a misleading pass/fail.
    var allUnavailable = rpkiResults.length > 0 && checkedResults.length === 0;
    return {
      status: allUnavailable ? "info" : coverage >= 90 ? "pass" : coverage >= 50 ? "warning" : "fail",
      coverage_pct: coverage,
      total_checked: checkedResults.length,
      db_unavailable_count: unavailableCount,
      with_roa: withRoa.length,
      over_specific: overSpecific.map(function(r) { return r.prefix; }),
      details: rpkiResults,
    };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 14. Abuse Contact Validation
function checkAbuseContact(rawAsn) {
  return fetchRipeStatCached("https://stat.ripe.net/data/abuse-contact-finder/data.json?resource=AS" + rawAsn).then(function(data) {
    if (data === null) return { status: "info", message: "abuse-contact-finder unavailable (fetch failed)", contacts: [], has_valid_email: false };
    var contacts = data && data.data && data.data.abuse_contacts ? data.data.abuse_contacts : [];
    var hasEmail = contacts.length > 0 && contacts.some(function(c) { return c && c.includes("@"); });
    return { status: hasEmail ? "pass" : "fail", contacts: contacts, has_valid_email: hasEmail };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 15. Spamhaus DROP/Blocklist
function checkBlocklist(samplePrefixes) {
  return Promise.all(
    samplePrefixes.slice(0, 5).map(function(pfx) {
      return fetchRipeStatCached("https://stat.ripe.net/data/blocklist/data.json?resource=" + encodeURIComponent(pfx)).then(function(data) {
        // fetchRipeStatCached resolves(null) on timeout/network-error rather than
        // throwing, so the .catch() below never sees that case -- distinguish it
        // explicitly instead of letting a failed lookup read as "not listed".
        if (data === null) return { prefix: pfx, listed: false, checked: false };
        var sources = data && data.data && data.data.sources ? data.data.sources : [];
        var listed = sources.filter(function(s) { return s.prefix_count > 0 || (s.entries && s.entries.length > 0); });
        return { prefix: pfx, listed: listed.length > 0, checked: true, sources: listed.map(function(s) { return s.source || s.name || "unknown"; }) };
      }).catch(function() { return { prefix: pfx, listed: false, checked: false }; });
    })
  ).then(function(results) {
    var checkedResults = results.filter(function(r) { return r.checked; });
    var listedPrefixes = checkedResults.filter(function(r) { return r.listed; });
    var allUnchecked = results.length > 0 && checkedResults.length === 0;
    return {
      status: allUnchecked ? "info" : (listedPrefixes.length === 0 ? "pass" : "fail"),
      checked: checkedResults.length,
      unavailable: results.length - checkedResults.length,
      listed_prefixes: listedPrefixes,
    };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 16. MANRS Compliance — scraped from public participants list (24h cache)
function checkManrs(rawAsn) {
  return ensureManrsCache().then(function() {
    return checkManrsMembership(rawAsn);
  }).catch(function(e) { return { status: "info", participant: "unknown", message: "MANRS check unavailable: " + e.message, note: "https://www.manrs.org/netops/participants/" }; });
}

// 17. Reverse DNS Coverage (3 prefix sample — more causes semaphore starvation on large ASNs)
function checkRdns(samplePrefixes) {
  var rdnsSampleSize = Math.min(3, samplePrefixes.length);
  return Promise.all(
    samplePrefixes.slice(0, rdnsSampleSize).map(function(pfx) {
      return fetchRipeStatCached("https://stat.ripe.net/data/reverse-dns-consistency/data.json?resource=" + encodeURIComponent(pfx), { timeout: 4000 }).then(function(data) {
        if (data === null) return { prefix: pfx, has_rdns: false, details: [], error: true };
        var pfxData = data && data.data && data.data.prefixes ? data.data.prefixes : {};
        var hasDelegation = false;
        var details = [];
        // API returns { ipv4: { "prefix": { complete, domains } }, ipv6: { ... } }
        ["ipv4", "ipv6"].forEach(function(af) {
          var afData = pfxData[af] || {};
          Object.keys(afData).forEach(function(p) {
            var entry = afData[p];
            if (entry && entry.complete) hasDelegation = true;
            if (entry && entry.domains) {
              entry.domains.forEach(function(d) {
                if (d.found) hasDelegation = true;
                details.push({ domain: d.domain, found: !!d.found });
              });
            }
          });
        });
        // Fallback: old array format
        if (Array.isArray(pfxData)) {
          pfxData.forEach(function(p) {
            if (p.ipv4 || p.ipv6 || (p.delegations && p.delegations.length > 0)) hasDelegation = true;
          });
        }
        return { prefix: pfx, has_rdns: hasDelegation, details: details };
      }).catch(function() { return { prefix: pfx, has_rdns: false, error: true }; });
    })
  ).then(function(results) {
    var checkedResults = results.filter(function(r) { return !r.error; });
    var withRdns = checkedResults.filter(function(r) { return r.has_rdns; });
    var coverage = checkedResults.length > 0 ? Math.round((withRdns.length / checkedResults.length) * 100) : 0;
    var failedPrefixes = checkedResults.filter(function(r) { return !r.has_rdns; }).map(function(r) { return r.prefix; });
    var allUnavailable = results.length > 0 && checkedResults.length === 0;
    return {
      status: allUnavailable ? "info" : (coverage >= 80 ? "pass" : coverage >= 50 ? "warning" : "fail"),
      coverage_pct: coverage,
      checked: checkedResults.length,
      unavailable: results.length - checkedResults.length,
      results: results,
      failed_prefixes: failedPrefixes,
    };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 18. BGP Visibility (uses routing-status API which is more reliable than visibility API)
function checkVisibility(rawAsn, samplePrefixes) {
  return fetchRipeStatCached("https://stat.ripe.net/data/routing-status/data.json?resource=AS" + rawAsn, { timeout: 8000 }).then(function(rsData) {
    var vis = rsData && rsData.data && rsData.data.visibility ? rsData.data.visibility : {};
    var v4 = vis.v4 || {};
    var v6 = vis.v6 || {};
    var totalPeers = (v4.total_ris_peers || 0) + (v6.total_ris_peers || 0);
    var seeingPeers = (v4.ris_peers_seeing || 0) + (v6.ris_peers_seeing || 0);
    var score = totalPeers > 0 ? Math.round((seeingPeers / totalPeers) * 100) : 0;
    var observedNeighbours = rsData && rsData.data ? (rsData.data.observed_neighbours || 0) : 0;
    // If routing-status returned no data, try bgproutes.io
    if (totalPeers === 0 && samplePrefixes[0]) {
      return fetchBgproutesVisibility(samplePrefixes[0]).then(function(bgprFb) {
        if (bgprFb && bgprFb.vps_seeing > 0) {
          seeingPeers = bgprFb.vps_seeing;
          totalPeers = Math.max(bgprFb.vps_seeing, 300);
          score = Math.round((seeingPeers / totalPeers) * 100);
        }
        return { status: score >= 80 ? "pass" : score >= 50 ? "warning" : "fail", visibility_score: score, total_ris_peers: totalPeers, seen_by: seeingPeers, v4_seeing: v4.ris_peers_seeing || 0, v4_total: v4.total_ris_peers || 0, v6_seeing: v6.ris_peers_seeing || 0, v6_total: v6.total_ris_peers || 0, observed_neighbours: observedNeighbours, source: "bgproutes.io_fallback" };
      }).catch(function() {
        // Neither RIPE routing-status nor the bgproutes.io fallback returned data --
        // this network hasn't actually been measured, not confirmed at 0% visibility.
        return { status: "info", message: "visibility could not be measured (both data sources unavailable)", visibility_score: 0, total_ris_peers: 0, seen_by: 0, source: "unavailable" };
      });
    }
    return { status: score >= 80 ? "pass" : score >= 50 ? "warning" : "fail", visibility_score: score, total_ris_peers: totalPeers, seen_by: seeingPeers, v4_seeing: v4.ris_peers_seeing || 0, v4_total: v4.total_ris_peers || 0, v6_seeing: v6.ris_peers_seeing || 0, v6_total: v6.total_ris_peers || 0, observed_neighbours: observedNeighbours, source: "ripe_routing_status" };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 19. BGP Communities Analysis
function checkCommunities(samplePrefixes) {
  return (samplePrefixes.length > 0
    ? (function() {
        var now = new Date();
        var end = now.toISOString().replace(/\.\d+Z/, "");
        var startTime = new Date(now.getTime() - 3600000).toISOString().replace(/\.\d+Z/, "");
        return fetchRipeStatCached("https://stat.ripe.net/data/bgp-updates/data.json?resource=" + encodeURIComponent(samplePrefixes[0]) + "&starttime=" + startTime + "&endtime=" + end);
      })()
    : Promise.resolve(null)
  ).then(function(data) {
    var updates = data && data.data && data.data.updates ? data.data.updates : [];
    var communityMap = {};
    var wellKnown = { "65535:0": "GRACEFUL_SHUTDOWN", "65535:65281": "NO_EXPORT", "65535:65282": "NO_ADVERTISE", "65535:666": "BLACKHOLE" };
    updates.forEach(function(u) {
      var attrs = u.attrs || {};
      var communities = attrs.community || [];
      communities.forEach(function(c) {
        var key = Array.isArray(c) ? c.join(":") : String(c);
        if (!communityMap[key]) communityMap[key] = { community: key, count: 0, well_known: wellKnown[key] || null };
        communityMap[key].count++;
      });
    });
    var sorted = Object.values(communityMap).sort(function(a, b) { return b.count - a.count; });
    var hasBlackhole = sorted.some(function(c) { return c.well_known === "BLACKHOLE"; });
    return { status: hasBlackhole ? "warning" : "pass", total_updates: updates.length, unique_communities: sorted.length, top_communities: sorted.slice(0, 20), well_known_detected: sorted.filter(function(c) { return c.well_known; }) };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 20. Geolocation Verification
function checkGeolocation(samplePrefixes) {
  return (samplePrefixes.length > 0
    ? fetchRipeStatCached("https://stat.ripe.net/data/maxmind-geo-lite-pfx/data.json?resource=" + encodeURIComponent(samplePrefixes[0]))
    : Promise.resolve(null)
  ).then(function(data) {
    var locatedPfxs = data && data.data && data.data.located_resources ? data.data.located_resources : [];
    var countries = {};
    locatedPfxs.forEach(function(l) { var locs = l.locations || []; locs.forEach(function(loc) { if (loc.country) countries[loc.country] = true; }); });
    return { status: Object.keys(countries).length > 0 ? "pass" : "warning", geo_countries: Object.keys(countries), sample_prefix: samplePrefixes[0] || null, located_resources: locatedPfxs.length };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 21. RPSL/IRR Object Validation (query all 5 RIRs in parallel)
function checkRpsl(rawAsn) {
  // Try RIPE first (has richest policy data), then RDAP for other RIRs
  var ripePromise = fetchJSON("https://rest.db.ripe.net/lookup/ripe/aut-num/AS" + rawAsn + ".json", { timeout: 5000 }).then(function(data) {
    var objects = data && data.objects && data.objects.object ? data.objects.object : [];
    if (objects.length === 0) return null;
    var attrs = objects[0] && objects[0].attributes && objects[0].attributes.attribute ? objects[0].attributes.attribute : [];
    var hasImport = attrs.some(function(a) { return a.name === "import" || a.name === "mp-import"; });
    var hasExport = attrs.some(function(a) { return a.name === "export" || a.name === "mp-export"; });
    var hasRemarks = attrs.some(function(a) { return a.name === "remarks"; });
    return { status: (hasImport || hasExport) ? "pass" : "warning", exists: true, has_import: hasImport, has_export: hasExport, has_remarks: hasRemarks, has_policy: hasImport || hasExport, source: "RIPE" };
  }).catch(function() { return null; });

  var rdapEndpoints = [
    { name: "APNIC", url: "https://rdap.apnic.net/autnum/" + rawAsn },
    { name: "ARIN", url: "https://rdap.arin.net/registry/autnum/" + rawAsn },
    { name: "LACNIC", url: "https://rdap.lacnic.net/rdap/autnum/" + rawAsn },
    { name: "AFRINIC", url: "https://rdap.afrinic.net/rdap/autnum/" + rawAsn },
  ];
  var rdapPromises = rdapEndpoints.map(function(ep) {
    return fetchJSON(ep.url, { timeout: 5000 }).then(function(data) {
      if (!data || data.errorCode || !data.handle) return null;
      var hasRemarks = !!(data.remarks && data.remarks.length > 0);
      var name = data.name || "";
      return { status: hasRemarks ? "pass" : "warning", exists: true, has_import: false, has_export: false, has_remarks: hasRemarks, has_policy: false, source: ep.name, rdap_name: name, rdap_handle: data.handle || "" };
    }).catch(function() { return null; });
  });

  return Promise.all([ripePromise].concat(rdapPromises)).then(function(results) {
    // Take first successful result
    for (var ri = 0; ri < results.length; ri++) {
      if (results[ri] !== null) return results[ri];
    }
    return { status: "warning", exists: false, has_policy: false };
  });
}

// 22. IXP Route Server Participation (Bug 5 fix: fair scoring for bilateral peering)
// Always use asn= for netixlan (more reliable than net_id when PDB rate-limits)
function checkIxRouteServer(rawAsn) {
  var ixRsQueryUrl = "/netixlan?asn=" + rawAsn;
  return fetchPeeringDB(ixRsQueryUrl).then(function(ixData) {
    var connections = ixData && ixData.data ? ixData.data : [];
    var rsParticipants = connections.filter(function(c) { return c.is_rs_peer === true; });
    var totalIx = connections.length;
    var rsCount = rsParticipants.length;
    var rsPct = totalIx > 0 ? Math.round((rsCount / totalIx) * 100) : 0;
    var status, note;

    if (totalIx > 0 && rsCount > 0) {
      // Using route servers - good
      status = "pass";
      note = rsCount + " of " + totalIx + " IX connections use route servers (" + rsPct + "%)";
    } else if (totalIx >= 10 && rsCount === 0) {
      // Network with 10+ IX connections but no RS = deliberate bilateral peering policy
      status = "pass";
      note = "Bilateral peering policy — " + totalIx + " IX connections, all bilateral (no route server usage)";
    } else if (totalIx < 3 && rsCount === 0) {
      // Very small IX presence and no RS
      status = "warning";
      note = "Only " + totalIx + " IX connection(s) and no route server usage";
    } else {
      // Small-medium network (3-9 IX) without RS - informational
      status = "info";
      note = totalIx + " IX connections without route server usage — consider enabling RS for broader reachability";
    }

    return { status: status, total_ix_connections: totalIx, rs_peer_count: rsCount, rs_peer_pct: rsPct, note: note };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// 23. Resource Certification (local RPKI validation - all prefixes, all RIRs)
function checkResourceCert(rawAsn, allPrefixes) {
  return Promise.all(
    allPrefixes.map(function(pfx) { return validateRPKIWithCache(rawAsn, pfx); })
  ).then(function(results) {
    var checked = results.filter(function(r) { return r.status !== "unavailable"; });
    var hasRoa = checked.some(function(r) { return r.status === "valid" || r.validating_roas > 0; });
    var allUnavailable = results.length > 0 && checked.length === 0;
    return { status: allUnavailable ? "info" : hasRoa ? "pass" : "fail", has_roas: hasRoa, checked: checked.length, roa_count: checked.filter(function(r) { return r.status === "valid"; }).length };
  }).catch(function(e) { return { status: "error", error: String(e) }; });
}

// Geolocation cross-ref with PeeringDB facilities
function fetchFacCountries(netId) {
  return netId
    ? fetchPeeringDB("/netfac?net_id=" + netId).then(function(facData) {
        return (facData && facData.data ? facData.data : []).map(function(f) { return f.country; }).filter(Boolean);
      }).catch(function() { return []; })
    : Promise.resolve([]);
}

module.exports = {
  checkIRR,
  checkRpkiCompleteness,
  checkAbuseContact,
  checkBlocklist,
  checkManrs,
  checkRdns,
  checkVisibility,
  checkCommunities,
  checkGeolocation,
  checkRpsl,
  checkIxRouteServer,
  checkResourceCert,
  fetchFacCountries,
};
