import { isIP } from "node:net";

// These are proxy/loopback addresses, not visitors. Never hash them as a network.
const NON_PUBLIC_V4 = [
  ["0.0.0.0", "0.255.255.255"], ["10.0.0.0", "10.255.255.255"],
  ["100.64.0.0", "100.127.255.255"], ["127.0.0.0", "127.255.255.255"],
  ["169.254.0.0", "169.254.255.255"], ["172.16.0.0", "172.31.255.255"],
  ["192.0.0.0", "192.0.0.255"], ["192.0.2.0", "192.0.2.255"],
  ["192.168.0.0", "192.168.255.255"], ["198.18.0.0", "198.19.255.255"],
  ["198.51.100.0", "198.51.100.255"], ["203.0.113.0", "203.0.113.255"],
  ["224.0.0.0", "255.255.255.255"],
] as const;

function ipv4Number(ip: string) {
  return ip.split(".").reduce((value, part) => value * 256 + Number(part), 0);
}

export function normalizeIpAddress(value: unknown): string | null {
  if (typeof value !== "string" || value.length > 128) return null;
  let ip = value.trim();
  const bracketed = /^\[([^\]]+)\](?::\d{1,5})?$/.exec(ip);
  if (bracketed) ip = bracketed[1];
  else if (/^\d{1,3}(?:\.\d{1,3}){3}:\d{1,5}$/.test(ip)) ip = ip.slice(0, ip.lastIndexOf(":"));
  if (ip.includes("%")) return null;
  const version = isIP(ip);
  if (!version) return null;
  if (version === 4) return ip;
  ip = new URL(`http://[${ip}]/`).hostname.slice(1, -1);
  const mapped = /^::ffff:([0-9a-f]+):([0-9a-f]+)$/i.exec(ip);
  if (mapped) {
    const high = parseInt(mapped[1], 16);
    const low = parseInt(mapped[2], 16);
    return [high >> 8, high & 255, low >> 8, low & 255].join(".");
  }
  return ip;
}

export function normalizePublicClientIp(value: unknown) {
  const ip = normalizeIpAddress(value);
  if (!ip) return null;
  if (isIP(ip) === 4) {
    const number = ipv4Number(ip);
    return NON_PUBLIC_V4.some(([first, last]) => number >= ipv4Number(first) && number <= ipv4Number(last)) ? null : ip;
  }
  // Global unicast only; exclude documentation and benchmarking ranges.
  return /^[23]/.test(ip) && !/^2001:(db8|2):/i.test(ip) ? ip : null;
}

export function getRequestClientIp(headers: Pick<Headers, "get">) {
  // The public edge must overwrite X-Real-IP with its socket peer address.
  // Do not trust CF-Connecting-IP or an arbitrary leftmost X-Forwarded-For.
  const realIp = headers.get("x-real-ip");
  if (realIp !== null) return normalizePublicClientIp(realIp);

  // Legacy XFF-only installations must explicitly declare their proxy hops.
  const trusted = new Set((process.env.OFFICIAL_MAIL_TRUSTED_PROXY_IPS ?? "")
    .split(",").map(normalizeIpAddress).filter(Boolean));
  if (!trusted.size) return null;
  const forwarded = headers.get("x-forwarded-for") ?? "";
  if (forwarded.length > 2048) return null;
  const chain = forwarded.split(",").map(normalizeIpAddress);
  if (chain.some((ip) => !ip)) return null;
  if (!trusted.has(chain[chain.length - 1])) return null;
  for (const ip of chain.reverse()) {
    if (trusted.has(ip)) continue;
    return normalizePublicClientIp(ip);
  }
  return null;
}

export function getPublicClientNetwork(value: unknown) {
  const ip = normalizePublicClientIp(value);
  if (!ip) return null;
  if (isIP(ip) === 4) return `${ip.split(".").slice(0, 3).join(".")}.0/24`;
  const [left, right = ""] = ip.split("::");
  const start = left ? left.split(":") : [];
  const end = right ? right.split(":") : [];
  const parts = ip.includes("::") ? [...start, ...Array(8 - start.length - end.length).fill("0"), ...end] : start;
  const prefix = parts.slice(0, 3).map((part) => parseInt(part, 16).toString(16));
  prefix.push((parseInt(parts[3], 16) & 0xff00).toString(16));
  return `${prefix.join(":")}::/56`;
}

export function getStoredClientIp(value: string | null, prefix: string | null) {
  const ip = normalizePublicClientIp(value);
  if (ip) return { ipAddress: ip, ipAddressApproximate: false };
  if (!value && prefix && /\/(24|56)$/.test(prefix) && normalizePublicClientIp(prefix.split("/")[0])) {
    return { ipAddress: prefix, ipAddressApproximate: true };
  }
  return { ipAddress: null, ipAddressApproximate: false };
}

export function publicClientIpSql(column: string) {
  if (!/^[a-z_]+(?:\.[a-z_]+)?$/.test(column)) throw new Error("Invalid IP column");
  const ipv4 = NON_PUBLIC_V4.map(([first, last]) =>
    `INET_ATON(${column}) NOT BETWEEN ${ipv4Number(first)} AND ${ipv4Number(last)}`).join(" AND ");
  return `((IS_IPV4(${column}) AND ${ipv4}) OR (IS_IPV6(${column})
    AND INET6_ATON(${column}) BETWEEN INET6_ATON('2000::') AND INET6_ATON('3fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff')
    AND INET6_ATON(${column}) NOT BETWEEN INET6_ATON('2001:db8::') AND INET6_ATON('2001:db8:ffff:ffff:ffff:ffff:ffff:ffff')
    AND INET6_ATON(${column}) NOT BETWEEN INET6_ATON('2001:2::') AND INET6_ATON('2001:2:ffff:ffff:ffff:ffff:ffff:ffff')))`;
}
