/**
 * @module aspa/validator
 * draft-ietf-sidrops-aspa-verification — ASPA-based AS path validation algorithm.
 *
 * Implements the Autonomous System Provider Authorization (ASPA) path
 * validation procedure as defined in draft-ietf-sidrops-aspa-verification. ASPA enables detection
 * of route leaks and unauthorized path segments by verifying that each
 * AS in a BGP path has authorized its upstream provider relationship.
 *
 * @see https://datatracker.ietf.org/doc/draft-ietf-sidrops-aspa-verification/ (still an IETF draft, not yet an RFC)
 *
 * @example
 * ```typescript
 * const aspaObjects = new Map<number, ASPAObject>();
 * aspaObjects.set(64501, {
 *   customerAsn: 64501,
 *   providers: [{ asn: 64500, afi: ["ipv4", "ipv6"] }],
 * });
 *
 * const result = validatePath(
 *   [13335, 64501, 64500, 174],
 *   aspaObjects,
 *   "upstream"
 * );
 * console.log(result.status); // "valid" | "invalid" | "unknown" | "unverifiable"
 * ```
 */

// ── Interfaces ──────────────────────────────────────────

/**
 * An ASPA object as registered in the RPKI.
 *
 * Maps a customer AS to its authorized upstream providers,
 * optionally scoped to specific address families.
 *
 * @see draft-ietf-sidrops-aspa-profile Section 3 (object format, still an IETF draft)
 */
export interface ASPAObject {
  /** The customer AS that created this authorization */
  readonly customerAsn: number;
  /** Authorized upstream provider ASNs with address family scope */
  readonly providers: ReadonlyArray<{
    readonly asn: number;
    readonly afi: ReadonlyArray<"ipv4" | "ipv6">;
  }>;
}

/**
 * Result of ASPA path validation.
 *
 * Contains the validation status, the analyzed path, any violations
 * found, and whether a route leak was detected.
 */
export interface ASPAValidationResult {
  /** Overall validation status per draft-ietf-sidrops-aspa-verification */
  readonly status: "valid" | "invalid" | "unknown" | "unverifiable";
  /** The AS path that was validated */
  readonly path: ReadonlyArray<number>;
  /** List of specific violations found in the path */
  readonly violations: ReadonlyArray<ASPAViolation>;
  /** Whether the path exhibits a route leak pattern */
  readonly leakDetected: boolean;
  /** The ASN responsible for the leak, if detected */
  readonly leakingAsn?: number;
  /** Confidence score from 0.0 to 1.0 based on ASPA coverage of the path */
  readonly confidence: number;
}

/**
 * A specific ASPA violation at a position in the AS path.
 *
 * Indicates that a hop in the path was not authorized by the
 * customer's ASPA object.
 */
export interface ASPAViolation {
  /** Zero-based position in the AS path where the violation occurs */
  readonly position: number;
  /** The ASN at this position */
  readonly asn: number;
  /** ASNs that are authorized providers for this ASN */
  readonly expectedProviders: ReadonlyArray<number>;
  /** The actual next-hop ASN in the path */
  readonly actualNextHop: number;
  /** Human-readable explanation of the violation */
  readonly reason: string;
}

// ── Helper Functions ────────────────────────────────────

/**
 * Check whether `providerAsn` is an authorized provider of `customerAsn`.
 *
 * @param customerAsn - The customer ASN to check
 * @param providerAsn - The candidate provider ASN
 * @param aspaObjects - Map of all known ASPA objects
 * @param afi - Address family to check ("ipv4" or "ipv6")
 * @returns "provider" if authorized, "not-provider" if explicitly not listed,
 *          or "no-attestation" if the customer has no ASPA object
 *
 * @see draft-ietf-sidrops-aspa-verification — Verification of Provider Authorization
 */
function checkProviderAuthorization(
  customerAsn: number,
  providerAsn: number,
  aspaObjects: ReadonlyMap<number, ASPAObject>,
  afi: "ipv4" | "ipv6" = "ipv4"
): "provider" | "not-provider" | "no-attestation" {
  const aspa = aspaObjects.get(customerAsn);

  if (!aspa) {
    return "no-attestation";
  }

  const isAuthorized = aspa.providers.some(
    (p) => p.asn === providerAsn && p.afi.includes(afi)
  );

  return isAuthorized ? "provider" : "not-provider";
}

/**
 * Remove consecutive duplicate ASNs from a path (AS path prepending).
 *
 * BGP speakers may prepend their own ASN multiple times for traffic
 * engineering. For ASPA validation, consecutive duplicates are collapsed.
 *
 * @param path - The raw AS path
 * @returns The path with consecutive duplicates removed
 */
function deduplicatePath(path: ReadonlyArray<number>): ReadonlyArray<number> {
  return path.filter((asn, index) => index === 0 || asn !== path[index - 1]);
}

// ── Core Validation Functions ───────────────────────────

/**
 * Validate an AS path in the upstream direction per draft-ietf-sidrops-aspa-verification.
 *
 * Walks the path from the origin AS (rightmost) toward the validating AS
 * (leftmost). For each pair (customer, provider), verifies that the
 * customer has authorized the provider via an ASPA object.
 *
 * The upstream validation procedure (draft-ietf-sidrops-aspa-verification):
 * - If the path has 0 or 1 unique ASNs, the result is "valid".
 * - Walk from index N-1 (origin) toward index 0.
 * - At each hop, check if path[i] authorizes path[i-1] as its provider.
 * - If any hop yields "not-provider", the path is "invalid".
 * - If all hops yield "provider", the path is "valid".
 * - Otherwise the path is "unknown".
 *
 * @param path - AS path to validate (leftmost = closest to validator)
 * @param aspaObjects - Map of customer ASN to ASPA object
 * @returns Validation result with status, violations, and confidence
 */
export function validateUpstream(
  path: ReadonlyArray<number>,
  aspaObjects: ReadonlyMap<number, ASPAObject>
): ASPAValidationResult {
  const dedupedPath = deduplicatePath(path);

  // Trivial paths are always valid
  if (dedupedPath.length <= 1) {
    return {
      status: "valid",
      path: [...dedupedPath],
      violations: [],
      leakDetected: false,
      confidence: 1.0,
    };
  }

  const violations: ASPAViolation[] = [];
  let hasNoAttestation = false;
  let coveredHops = 0;
  const totalHops = dedupedPath.length - 1;

  // Walk from origin (rightmost) toward the validator (leftmost).
  // path[i] is the customer; path[i-1] is the alleged provider.
  for (let i = dedupedPath.length - 1; i >= 1; i--) {
    const customerAsn = dedupedPath[i];
    const providerAsn = dedupedPath[i - 1];

    const authResult = checkProviderAuthorization(
      customerAsn,
      providerAsn,
      aspaObjects
    );

    if (authResult === "provider") {
      coveredHops++;
    } else if (authResult === "not-provider") {
      coveredHops++;
      const aspa = aspaObjects.get(customerAsn);
      violations.push({
        position: i,
        asn: customerAsn,
        expectedProviders: aspa
          ? aspa.providers.map((p) => p.asn)
          : [],
        actualNextHop: providerAsn,
        reason: `AS${customerAsn} has an ASPA object but does not list AS${providerAsn} as an authorized provider. ` +
          `This indicates a potential route leak or unauthorized path segment.`,
      });
    } else {
      hasNoAttestation = true;
    }
  }

  const confidence = totalHops > 0 ? coveredHops / totalHops : 1.0;

  // Determine overall status per draft-ietf-sidrops-aspa-verification
  if (violations.length > 0) {
    const leakingViolation = violations[0];
    return {
      status: "invalid",
      path: [...dedupedPath],
      violations,
      leakDetected: true,
      leakingAsn: leakingViolation.asn,
      confidence,
    };
  }

  if (hasNoAttestation) {
    return {
      status: "unknown",
      path: [...dedupedPath],
      violations: [],
      leakDetected: false,
      confidence,
    };
  }

  return {
    status: "valid",
    path: [...dedupedPath],
    violations: [],
    leakDetected: false,
    confidence,
  };
}

/**
 * Validate an AS path in the downstream direction per
 * draft-ietf-sidrops-aspa-verification Section 5.5.
 *
 * Unlike upstream validation, 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. A naive "reverse the path and reapply upstream
 * validation" does not implement this: it would reject any path with a
 * legitimate down-ramp segment. This computes the four ramp lengths the
 * draft actually defines (max/min up-ramp, max/min down-ramp) from two
 * independent scans and applies the draft's three halt conditions in order.
 *
 * Verified 2026-07-16 against a worked example fetched from the draft (an
 * N=4 path with a single unattested "kink" resolves to valid; a path with
 * definite authorization failures on both ends resolves to invalid).
 *
 * @param path - AS path to validate (leftmost = closest to validator)
 * @param aspaObjects - Map of customer ASN to ASPA object
 * @returns Validation result with status, violations, and confidence
 */
export function validateDownstream(
  path: ReadonlyArray<number>,
  aspaObjects: ReadonlyMap<number, ASPAObject>
): ASPAValidationResult {
  const dedupedPath = deduplicatePath(path);
  const N = dedupedPath.length;

  if (N <= 2) {
    return {
      status: "valid",
      path: [...dedupedPath],
      violations: [],
      leakDetected: false,
      confidence: 1.0,
    };
  }

  // Up-ramp scan: walk from the origin (index N-1) toward the validator
  // (index 0). dedupedPath[k] is the customer, dedupedPath[k-1] the
  // provider it's declaring -- authorized(A(I), A(I+1)) in draft terms.
  let maxUpRamp = N;
  let minUpRamp = N;
  let firstUpViolation: { position: number; asn: number; nextHop: number } | undefined;
  for (let k = N - 1; k >= 1; k--) {
    const check = checkProviderAuthorization(dedupedPath[k], dedupedPath[k - 1], aspaObjects);
    const I = N - k;
    if (check === "not-provider" && maxUpRamp === N) {
      maxUpRamp = I;
      firstUpViolation = { position: k, asn: dedupedPath[k], nextHop: dedupedPath[k - 1] };
    }
    if (check !== "provider" && minUpRamp === N) minUpRamp = I;
    if (maxUpRamp !== N && minUpRamp !== N) break;
  }

  // Down-ramp scan: the mirror image, walking from the validator (index 0)
  // toward the origin -- authorized(A(J), A(J-1)) in draft terms.
  let maxDownRamp = N;
  let minDownRamp = N;
  let firstDownViolation: { position: number; asn: number; nextHop: number } | undefined;
  for (let k = 0; k <= N - 2; k++) {
    const check = checkProviderAuthorization(dedupedPath[k], dedupedPath[k + 1], aspaObjects);
    const rampLen = k + 1;
    if (check === "not-provider" && maxDownRamp === N) {
      maxDownRamp = rampLen;
      firstDownViolation = { position: k, asn: dedupedPath[k], nextHop: dedupedPath[k + 1] };
    }
    if (check !== "provider" && minDownRamp === N) minDownRamp = rampLen;
    if (maxDownRamp !== N && minDownRamp !== N) break;
  }

  // Confidence: fraction of hops that returned a definitive (non-"no-attestation") result.
  const totalHops = N - 1;
  let attestedHops = 0;
  for (let k = N - 1; k >= 1; k--) {
    if (checkProviderAuthorization(dedupedPath[k], dedupedPath[k - 1], aspaObjects) !== "no-attestation") {
      attestedHops++;
    }
  }
  const confidence = totalHops > 0 ? attestedHops / totalHops : 1.0;

  if (maxUpRamp + maxDownRamp < N) {
    const violation = firstUpViolation ?? firstDownViolation;
    const violations: ASPAViolation[] = violation
      ? [
          {
            position: violation.position,
            asn: violation.asn,
            expectedProviders: (aspaObjects.get(violation.asn)?.providers ?? []).map((p) => p.asn),
            actualNextHop: violation.nextHop,
            reason:
              `AS${violation.asn} has an ASPA object but does not list AS${violation.nextHop} as an authorized provider. ` +
              `No valid up-ramp/down-ramp split covers this path (max_up_ramp=${maxUpRamp}, max_down_ramp=${maxDownRamp}, N=${N}).`,
          },
        ]
      : [];
    return {
      status: "invalid",
      path: [...dedupedPath],
      violations,
      leakDetected: true,
      leakingAsn: violation?.asn,
      confidence,
    };
  }

  if (minUpRamp + minDownRamp < N) {
    return {
      status: "unknown",
      path: [...dedupedPath],
      violations: [],
      leakDetected: false,
      confidence,
    };
  }

  return {
    status: "valid",
    path: [...dedupedPath],
    violations: [],
    leakDetected: false,
    confidence,
  };
}

/**
 * Validate an AS path against ASPA objects.
 *
 * This is the main entry point for ASPA path validation. It dispatches
 * to either upstream or downstream validation based on the direction
 * parameter.
 *
 * @param path - The AS path to validate. Leftmost ASN is closest to the
 *               validating router; rightmost is the origin.
 * @param aspaObjects - Map of customer ASN to its ASPA object
 * @param direction - "upstream" when receiving from a provider,
 *                    "downstream" when receiving from a customer
 * @returns Full validation result including status, violations, leak
 *          detection, and confidence score
 *
 * @see draft-ietf-sidrops-aspa-verification — Procedure for Verifying the AS_PATH Attribute
 *
 * @example
 * ```typescript
 * // Upstream validation: AS174 -> AS13335 -> AS64501 (origin)
 * const result = validatePath(
 *   [174, 13335, 64501],
 *   aspaObjects,
 *   "upstream"
 * );
 *
 * if (result.leakDetected) {
 *   console.log(`Route leak by AS${result.leakingAsn}`);
 * }
 * ```
 */
export function validatePath(
  path: ReadonlyArray<number>,
  aspaObjects: ReadonlyMap<number, ASPAObject>,
  direction: "upstream" | "downstream"
): ASPAValidationResult {
  if (path.length === 0) {
    return {
      status: "unverifiable",
      path: [],
      violations: [],
      leakDetected: false,
      confidence: 0,
    };
  }

  return direction === "upstream"
    ? validateUpstream(path, aspaObjects)
    : validateDownstream(path, aspaObjects);
}
