// RFC-Compliant ASPA Verification Engine
// Pure functions, no I/O -- see draft-ietf-sidrops-aspa-verification.

// Check if AS path contains AS_SET segments (curly braces indicate sets)
function hasAsSet(asPath) {
  if (typeof asPath === "string") {
    return asPath.includes("{") || asPath.includes("}");
  }
  return false;
}

// Hop Check function (core of ASPA verification)
// aspaStore = Map<number, Set<number>> (CAS -> provider set)
// hopCheck(asI, asJ): is asJ an attested provider of CUSTOMER asI? Caller must pass
// the customer first, provider second -- see draft-ietf-sidrops-aspa-verification
// section 5.3's authorized(A(I), A(I+1)), where A(I) is always the customer side.
function hopCheck(asI, asJ, aspaStore) {
  const providers = aspaStore.get(asI);
  if (!providers) return "NoAttestation";
  return providers.has(asJ) ? "ProviderPlus" : "NotProviderPlus";
}

// Collapse AS path prepends (remove consecutive duplicates)
function collapsePrepends(path) {
  return path.filter((as, i) => i === 0 || as !== path[i - 1]);
}

// Upstream Verification (RFC Section 6.1)
function verifyUpstream(asPath, aspaStore, rawPathStr) {
  if (rawPathStr && hasAsSet(rawPathStr)) return { result: "Invalid", reason: "Path contains AS_SET" };
  const collapsed = collapsePrepends(asPath);
  if (collapsed.length <= 1) return { result: "Valid", reason: "Single-hop path" };

  const hops = [];
  let hasNoAttestation = false;

  for (let i = 1; i < collapsed.length; i++) {
    // collapsed[i] (closer to origin) is the customer; collapsed[i - 1] (closer to
    // the validator) is the provider it's declaring -- customer goes first in hopCheck.
    const check = hopCheck(collapsed[i], collapsed[i - 1], aspaStore);
    hops.push({
      from: collapsed[i - 1],
      to: collapsed[i],
      result: check,
    });
    if (check === "NotProviderPlus") {
      return { result: "Invalid", reason: "Hop AS" + collapsed[i - 1] + " -> AS" + collapsed[i] + " is NotProviderPlus", hops };
    }
    if (check === "NoAttestation") hasNoAttestation = true;
  }

  return {
    result: hasNoAttestation ? "Unknown" : "Valid",
    reason: hasNoAttestation ? "Some hops lack ASPA attestation" : "All hops verified as ProviderPlus",
    hops,
  };
}

// Downstream Verification -- draft-ietf-sidrops-aspa-verification Section 5.5.
// Unlike upstream verification, a downstream path may legitimately contain both
// an up-ramp (customer->provider hops, near the origin) AND a down-ramp
// (provider->customer hops, near the validator), transitioning at most once --
// e.g. origin -> its providers -> a peering exchange -> validator's providers ->
// validator. Section 5.3 defines four ramp lengths computed from two independent
// scans, verified 2026-07-16 against a worked example fetched from the draft
// (N=4 path with a single unattested/no-attestation "kink" correctly resolves
// to Valid, and a path with definite failures on both ends correctly resolves
// to Invalid, matching this implementation).
function verifyDownstream(asPath, aspaStore, rawPathStr) {
  if (rawPathStr && hasAsSet(rawPathStr)) return { result: "Invalid", reason: "Path contains AS_SET" };
  const collapsed = collapsePrepends(asPath);
  const N = collapsed.length;
  if (N <= 2) return { result: "Valid", reason: "Path length <= 2" };

  const hops = [];
  for (let i = 1; i < N; i++) {
    hops.push({
      from: collapsed[i - 1],
      to: collapsed[i],
      result: hopCheck(collapsed[i], collapsed[i - 1], aspaStore),
    });
  }

  // Up-ramp scan: walk from the origin (collapsed[N-1]) toward the validator
  // (collapsed[0]), i.e. authorized(A(I), A(I+1)) for I = 1..N-1 ascending.
  // maxUpRamp stops at the first DEFINITE violation ("NotProviderPlus");
  // minUpRamp stops at the first hop that isn't confirmed ProviderPlus
  // (including unattested "NoAttestation" hops, which don't disprove a leak
  // but also don't prove the path is clean).
  let maxUpRamp = N;
  let minUpRamp = N;
  for (let k = N - 1; k >= 1; k--) {
    const check = hopCheck(collapsed[k], collapsed[k - 1], aspaStore);
    const I = N - k;
    if (check === "NotProviderPlus" && maxUpRamp === N) maxUpRamp = I;
    if (check !== "ProviderPlus" && minUpRamp === N) minUpRamp = I;
    if (maxUpRamp !== N && minUpRamp !== N) break;
  }

  // Down-ramp scan: the mirror image, walking from the validator (collapsed[0])
  // toward the origin, i.e. authorized(A(J), A(J-1)) for J = N..2 descending.
  let maxDownRamp = N;
  let minDownRamp = N;
  for (let k = 0; k <= N - 2; k++) {
    const check = hopCheck(collapsed[k], collapsed[k + 1], aspaStore);
    const rampLen = k + 1; // = N - J + 1 for the corresponding J
    if (check === "NotProviderPlus" && maxDownRamp === N) maxDownRamp = rampLen;
    if (check !== "ProviderPlus" && minDownRamp === N) minDownRamp = rampLen;
    if (maxDownRamp !== N && minDownRamp !== N) break;
  }

  if (maxUpRamp + maxDownRamp < N) {
    return {
      result: "Invalid",
      reason: "max_up_ramp(" + maxUpRamp + ") + max_down_ramp(" + maxDownRamp + ") < " + N + ": no valid up/down split covers the path",
      hops,
    };
  }
  if (minUpRamp + minDownRamp < N) {
    return {
      result: "Unknown",
      reason: "min_up_ramp(" + minUpRamp + ") + min_down_ramp(" + minDownRamp + ") < " + N + ": unattested hops prevent full confirmation",
      hops,
    };
  }
  return {
    result: "Valid",
    reason: "max_up_ramp(" + maxUpRamp + ") + max_down_ramp(" + maxDownRamp + ") >= " + N + " and min_up_ramp + min_down_ramp >= " + N,
    hops,
  };
}

// Valley Detection: scan path for up-down-up pattern (route leak indicator)
function detectValleys(asPath, aspaStore) {
  const collapsed = collapsePrepends(asPath);
  if (collapsed.length < 4) return [];

  const valleys = [];
  // Walk the path and look at relationship transitions
  const relationships = [];
  for (let i = 1; i < collapsed.length; i++) {
    const fwd = hopCheck(collapsed[i - 1], collapsed[i], aspaStore);
    const rev = hopCheck(collapsed[i], collapsed[i - 1], aspaStore);
    let rel = "unknown";
    if (fwd === "ProviderPlus") rel = "customer-to-provider";
    else if (rev === "ProviderPlus") rel = "provider-to-customer";
    else if (fwd === "NotProviderPlus" && rev === "NotProviderPlus") rel = "peer-to-peer";
    relationships.push({ from: collapsed[i - 1], to: collapsed[i], rel });
  }

  // Detect c2p -> p2c -> c2p pattern
  for (let i = 0; i < relationships.length - 2; i++) {
    if (
      relationships[i].rel === "customer-to-provider" &&
      relationships[i + 1].rel === "provider-to-customer" &&
      relationships[i + 2].rel === "customer-to-provider"
    ) {
      valleys.push({
        position: i,
        path_segment: [
          relationships[i].from,
          relationships[i].to,
          relationships[i + 1].to,
          relationships[i + 2].to,
        ].map((a) => "AS" + a),
        description:
          "Route leak: AS" + relationships[i].from + " -> AS" + relationships[i].to +
          " (c2p) -> AS" + relationships[i + 1].to +
          " (p2c) -> AS" + relationships[i + 2].to + " (c2p)",
      });
    }
  }

  return valleys;
}

// Build ASPA store from detected provider relationships
function buildAspaStore(detectedProviders, targetAsn) {
  const store = new Map();
  // Add the target ASN's providers
  if (detectedProviders.length > 0) {
    const providerSet = new Set(detectedProviders.map((p) => p.asn));
    store.set(targetAsn, providerSet);
  }
  return store;
}

// Calculate ASPA Readiness Score (0-100)
function calculateAspaReadinessScore(params) {
  const { rpkiCoverage, aspaObjectExists, providerCompleteness, pathValidationPct } = params;

  // ROA coverage (0-25 points)
  const roaScore = Math.round((Math.min(rpkiCoverage, 100) / 100) * 25);

  // ASPA object exists (0-25 points)
  const aspaScore = aspaObjectExists ? 25 : 0;

  // Provider completeness (0-25 points)
  const provScore = Math.round((Math.min(providerCompleteness, 100) / 100) * 25);

  // Path validation results (0-25 points)
  const pathScore = Math.round((Math.min(pathValidationPct, 100) / 100) * 25);

  return {
    total: roaScore + aspaScore + provScore + pathScore,
    breakdown: {
      roa_coverage: { score: roaScore, max: 25, value: rpkiCoverage },
      aspa_object: { score: aspaScore, max: 25, value: aspaObjectExists },
      provider_completeness: { score: provScore, max: 25, value: providerCompleteness },
      path_validation: { score: pathScore, max: 25, value: pathValidationPct },
    },
  };
}

module.exports = {
  hasAsSet,
  hopCheck,
  collapsePrepends,
  verifyUpstream,
  verifyDownstream,
  detectValleys,
  buildAspaStore,
  calculateAspaReadinessScore,
};
