/**
 * IPv4/IPv6 private-range detection.
 *
 * Lifted out of `domain.ts` so leaf modules like `allowedAddresses.ts` can
 * depend on `isPrivateIP` without forming a cycle (`domain` re-exports the
 * `isPrivateIP` symbol below for backward compatibility with existing
 * callers, but this is the canonical location).
 *
 * Coverage:
 *  - IPv4: 0.0.0.0/8, 10/8, 100.64/10 (CGNAT), 127/8, 169.254/16,
 *    172.16/12, 192.0.0/24 (RFC 5736), 192.168/16, 198.18/15 (benchmarking),
 *    224/3 (multicast/reserved).
 *  - IPv6: ::1, ::, fc00::/7 (unique-local), fe80::/10 (link-local).
 *  - 4-in-6 mappings: ::ffff:A.B.C.D and the hex form ::ffff:HHHH:HHHH.
 *  - Embedded private IPv4 in 6to4 (2002::/16), NAT64 (64:ff9b::/96), and
 *    Teredo (2001::/32) addresses.
 */

/** Checks if IPv4 octets fall within private, reserved, or non-routable ranges */
export function isPrivateIPv4(a: number, b: number, c: number): boolean {
  if (a === 0) {
    return true;
  }
  if (a === 10) {
    return true;
  }
  if (a === 127) {
    return true;
  }
  if (a === 100 && b >= 64 && b <= 127) {
    return true;
  }
  if (a === 169 && b === 254) {
    return true;
  }
  if (a === 172 && b >= 16 && b <= 31) {
    return true;
  }
  if (a === 192 && b === 168) {
    return true;
  }
  if (a === 192 && b === 0 && c === 0) {
    return true;
  }
  if (a === 198 && (b === 18 || b === 19)) {
    return true;
  }
  if (a >= 224) {
    return true;
  }
  return false;
}

/** Checks if a pre-normalized (lowercase, bracket-stripped) IPv6 address falls within fe80::/10 */
function isIPv6LinkLocal(ipv6: string): boolean {
  if (!ipv6.includes(':')) {
    return false;
  }
  const firstHextet = ipv6.split(':', 1)[0];
  if (!firstHextet || !/^[0-9a-f]{1,4}$/.test(firstHextet)) {
    return false;
  }
  const hextet = parseInt(firstHextet, 16);
  // /10 mask (0xffc0) preserves top 10 bits: fe80 = 1111_1110_10xx_xxxx
  return (hextet & 0xffc0) === 0xfe80;
}

/** Checks if an IPv6 address embeds a private IPv4 via 6to4, NAT64, or Teredo */
function hasPrivateEmbeddedIPv4(ipv6: string): boolean {
  if (!ipv6.startsWith('2002:') && !ipv6.startsWith('64:ff9b::') && !ipv6.startsWith('2001::')) {
    return false;
  }
  const segments = ipv6.split(':').filter((s) => s !== '');

  if (ipv6.startsWith('2002:') && segments.length >= 3) {
    const hi = parseInt(segments[1], 16);
    const lo = parseInt(segments[2], 16);
    if (!isNaN(hi) && !isNaN(lo)) {
      return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff);
    }
  }

  if (ipv6.startsWith('64:ff9b::')) {
    const lastTwo = segments.slice(-2);
    if (lastTwo.length === 2) {
      const hi = parseInt(lastTwo[0], 16);
      const lo = parseInt(lastTwo[1], 16);
      if (!isNaN(hi) && !isNaN(lo)) {
        return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff);
      }
    }
  }

  // RFC 4380: Teredo stores external IPv4 as bitwise complement in last 32 bits
  if (ipv6.startsWith('2001::')) {
    const lastTwo = segments.slice(-2);
    if (lastTwo.length === 2) {
      const hi = parseInt(lastTwo[0], 16);
      const lo = parseInt(lastTwo[1], 16);
      if (!isNaN(hi) && !isNaN(lo)) {
        return isPrivateIPv4((~hi >> 8) & 0xff, ~hi & 0xff, (~lo >> 8) & 0xff);
      }
    }
  }

  return false;
}

/**
 * Checks if an IP address belongs to a private, reserved, or link-local range.
 * Handles IPv4, IPv6, and IPv4-mapped IPv6 addresses (::ffff:A.B.C.D).
 */
export function isPrivateIP(ip: string): boolean {
  const normalized = ip
    .toLowerCase()
    .trim()
    .replace(/^\[|\]$/g, '');

  const mappedMatch = normalized.match(/^::ffff:(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (mappedMatch) {
    const [, a, b, c] = mappedMatch.map(Number);
    return isPrivateIPv4(a, b, c);
  }

  const hexMappedMatch = normalized.match(/^(?:::ffff:|::)([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
  if (hexMappedMatch) {
    const hi = parseInt(hexMappedMatch[1], 16);
    const lo = parseInt(hexMappedMatch[2], 16);
    return isPrivateIPv4((hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff);
  }

  const ipv4Match = normalized.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
  if (ipv4Match) {
    const [, a, b, c] = ipv4Match.map(Number);
    return isPrivateIPv4(a, b, c);
  }

  if (
    normalized === '::1' ||
    normalized === '::' ||
    normalized.startsWith('fc') || // fc00::/7 — exactly prefixes 'fc' and 'fd'
    normalized.startsWith('fd') ||
    isIPv6LinkLocal(normalized) // fe80::/10 — spans 0xfe80–0xfebf; bitwise check required
  ) {
    return true;
  }

  if (hasPrivateEmbeddedIPv4(normalized)) {
    return true;
  }

  return false;
}
