/**
 * Classify a free-text search query into a VRP lookup target.
 *
 * The backend VRP lookup (`GET /runs/{id}/vrps/lookup`) accepts exactly one of
 * `ip` / `prefix`, plus an optional `asn` filter. An ASN-only query cannot be
 * served by the API — the UI surfaces that explicitly instead of guessing.
 */

export type NetworkQuery =
  | { kind: "ip"; ip: string; asn?: number }
  | { kind: "prefix"; prefix: string; asn?: number }
  | { kind: "asn"; asn: number }
  | { kind: "none" };

const ASN_MAX = 4_294_967_295;

/** "AS45090" / "as45090" / "45090" → ASN, else null. */
export function parseAsn(input: string): number | null {
  const match = /^(?:as)?(\d{1,10})$/i.exec(input.trim());
  if (!match) return null;
  const asn = Number(match[1]);
  return Number.isSafeInteger(asn) && asn <= ASN_MAX ? asn : null;
}

function isIpv4Octets(addr: string): boolean {
  const parts = addr.split(".");
  return (
    parts.length === 4 &&
    parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)
  );
}

function isIpv6(addr: string): boolean {
  if (!/^[0-9a-fA-F:.]+$/.test(addr)) return false;
  if ((addr.match(/::/g) ?? []).length > 1) return false;
  const isGroup = (g: string, isLast: boolean) =>
    isLast && g.includes(".")
      ? isIpv4Octets(g) // embedded IPv4 tail, e.g. "::ffff:192.0.2.1"
      : /^[0-9a-fA-F]{1,4}$/.test(g);
  const ipv4TailWeight = (groups: string[]) =>
    groups.length > 0 && groups[groups.length - 1].includes(".") ? 1 : 0;
  if (addr.includes("::")) {
    const [head, tail] = addr.split("::");
    const headGroups = head ? head.split(":") : [];
    const tailGroups = tail ? tail.split(":") : [];
    // "::" compresses at least one group; an IPv4 tail counts as two groups.
    const effective =
      headGroups.length + tailGroups.length + ipv4TailWeight(tailGroups);
    if (effective > 7) return false;
    return [...headGroups, ...tailGroups].every((g, i, all) =>
      isGroup(g, i === all.length - 1),
    );
  }
  const groups = addr.split(":");
  const expected = groups[groups.length - 1]?.includes(".") ? 7 : 8;
  if (groups.length !== expected) return false;
  return groups.every((g, i) => isGroup(g, i === groups.length - 1));
}

/** Bare IPv4/IPv6 address (no "/") → normalized string, else null. */
export function parseIpAddress(input: string): string | null {
  const s = input.trim();
  if (s.includes("/")) return null;
  if (isIpv4Octets(s)) return s;
  if (s.includes(":") && isIpv6(s)) return s;
  return null;
}

function parsePrefixParts(s: string): [string, number] | null {
  const slash = s.indexOf("/");
  if (slash < 0 || slash !== s.lastIndexOf("/")) return null;
  const addr = s.slice(0, slash);
  const lenRaw = s.slice(slash + 1);
  if (!/^\d{1,3}$/.test(lenRaw)) return null;
  return [addr, Number(lenRaw)];
}

/** "addr/len" CIDR with family-correct length → normalized string, else null. */
export function parsePrefix(input: string): string | null {
  const s = input.trim();
  const parts = parsePrefixParts(s);
  if (!parts) return null;
  const [addr, len] = parts;
  if (isIpv4Octets(addr)) return len <= 32 ? `${addr}/${len}` : null;
  if (addr.includes(":") && isIpv6(addr)) return len <= 128 ? `${addr}/${len}` : null;
  return null;
}

/**
 * Classify a query string. Precedence: CIDR prefix → bare IP → ASN → none.
 * ("192.0.2.1" also parses as digits+ dots only; ASN requires pure digits, so
 * no overlap. A bare number is always treated as an ASN.)
 *
 * Two whitespace-separated tokens are read as a combined query — one network
 * token (IP or prefix) plus one ASN token (`AS13335` or `13335`), in either
 * order — yielding an ip/prefix query carrying an `asn` filter.
 */
export function classifyNetworkQuery(input: string): NetworkQuery {
  const s = input.trim();
  if (!s) return { kind: "none" };
  const tokens = s.split(/\s+/);
  if (tokens.length === 2) {
    for (const [netToken, asnToken] of [
      [tokens[0], tokens[1]],
      [tokens[1], tokens[0]],
    ] as const) {
      const asn = parseAsn(asnToken);
      if (asn === null) continue;
      const prefix = parsePrefix(netToken);
      if (prefix) return { kind: "prefix", prefix, asn };
      const ip = parseIpAddress(netToken);
      if (ip) return { kind: "ip", ip, asn };
    }
    return { kind: "none" };
  }
  if (tokens.length > 2) return { kind: "none" };
  const prefix = parsePrefix(s);
  if (prefix) return { kind: "prefix", prefix };
  const ip = parseIpAddress(s);
  if (ip) return { kind: "ip", ip };
  const asn = parseAsn(s);
  if (asn !== null) return { kind: "asn", asn };
  return { kind: "none" };
}
