// Pure RDAP helpers (RFC 9083 responses, RFC 9224 bootstrap). No network here.

/**
 * Registries that run an RDAP server but are missing from IANA's bootstrap file.
 * Only consulted for endings the bootstrap file doesn't list, so a later IANA
 * entry always takes over. Each one was verified by hand.
 */
export const BUILT_IN_RDAP_SERVERS: Readonly<Record<string, string>> = {
  io: "https://rdap.identitydigital.services/rdap/",
  me: "https://rdap.identitydigital.services/rdap/",
  ch: "https://rdap.nic.ch/", // answers, but never publishes an expiry date
  li: "https://rdap.nic.ch/",
};

/** TLD → RDAP base URL (always ending in "/"), from IANA's bootstrap file. */
export function parseBootstrap(json: unknown): Map<string, string> {
  const services = (json as { services?: unknown })?.services;
  if (!Array.isArray(services)) throw new Error("Not an RDAP bootstrap file");

  const servers = new Map<string, string>();
  for (const service of services) {
    if (!Array.isArray(service) || !Array.isArray(service[0]) || !Array.isArray(service[1])) continue;
    const urls = service[1].filter((url): url is string => typeof url === "string");
    // Prefer HTTPS when a registry lists both.
    const url = urls.find((u) => u.startsWith("https://")) ?? urls[0];
    if (!url) continue;
    for (const tld of service[0]) {
      if (typeof tld === "string") servers.set(tld.toLowerCase(), url.endsWith("/") ? url : `${url}/`);
    }
  }
  return servers;
}

/** The RDAP server for a TLD: IANA's bootstrap first, then the built-in list; null when none is known. */
export function rdapServerFor(tld: string, bootstrap: Map<string, string>): string | null {
  return bootstrap.get(tld) ?? BUILT_IN_RDAP_SERVERS[tld] ?? null;
}

export function rdapDomainUrl(server: string, domain: string): string {
  return `${server}domain/${encodeURIComponent(domain)}`;
}

export type RdapDomainRecord = {
  registeredAt: Date | null;
  expiresAt: Date | null;
  lastChangedAt: Date | null;
  registryUpdatedAt: Date | null;
  statuses: string[];
};

function toDate(value: unknown): Date | null {
  if (typeof value !== "string") return null;
  const time = Date.parse(value);
  return Number.isNaN(time) ? null : new Date(time);
}

/**
 * Reads the dates from a domain response's `events` array:
 *   registration → registeredAt, expiration → expiresAt,
 *   last changed → lastChangedAt, last update of RDAP database → registryUpdatedAt.
 * Throws when the body is not an RDAP domain object at all.
 */
export function parseRdapDomain(json: unknown): RdapDomainRecord {
  const body = json as { objectClassName?: unknown; ldhName?: unknown; events?: unknown; status?: unknown } | null;
  if (typeof body !== "object" || body === null || (body.objectClassName !== "domain" && typeof body.ldhName !== "string")) {
    throw new Error("The registry's answer is not an RDAP domain record");
  }

  const dates = new Map<string, Date>();
  for (const event of Array.isArray(body.events) ? body.events : []) {
    const action = typeof event?.eventAction === "string" ? event.eventAction.trim().toLowerCase() : "";
    const date = toDate(event?.eventDate);
    // Some registries repeat an event; the first one is the current value.
    if (action && date && !dates.has(action)) dates.set(action, date);
  }

  const statuses = Array.isArray(body.status)
    ? body.status.filter((s): s is string => typeof s === "string").map((s) => s.trim().toLowerCase())
    : [];

  return {
    registeredAt: dates.get("registration") ?? null,
    expiresAt: dates.get("expiration") ?? null,
    lastChangedAt: dates.get("last changed") ?? null,
    registryUpdatedAt: dates.get("last update of rdap database") ?? null,
    statuses,
  };
}
