import "server-only";

import { topLevelDomain } from "@/lib/domains/name";
import { parseBootstrap, parseRdapDomain, rdapDomainUrl, rdapServerFor, type RdapDomainRecord } from "@/lib/domains/rdap";
import { env } from "../env";
import { logger } from "../logger";

const TIMEOUT_MS = 20_000;
const RETRY_DELAY_MS = 5_000;
const MAX_RETRY_AFTER_MS = 30_000;
const MAX_BODY_BYTES = 1_000_000; // real answers are a few KB
const BOOTSTRAP_TTL_MS = 7 * 24 * 60 * 60_000; // IANA updates it rarely
const HEADERS = { Accept: "application/rdap+json, application/json", "User-Agent": "BTS-Apps-Dashboard (domain expiry check)" };

export type LookupOutcome =
  | { result: "FOUND"; server: string; record: RdapDomainRecord }
  | { result: "NOT_FOUND"; server: string }
  | { result: "UNSUPPORTED"; server: null; error: string }
  | { result: "FAILED"; server: string | null; error: string };

// ---------------------------------------------------------------------------
// Bootstrap (which server answers for which ending), cached per process
// ---------------------------------------------------------------------------

const globalForBootstrap = globalThis as unknown as { btsRdapBootstrap?: { servers: Map<string, string>; fetchedAt: number } };

class BootstrapUnavailableError extends Error {}

async function bootstrap(): Promise<Map<string, string>> {
  const cached = globalForBootstrap.btsRdapBootstrap;
  if (cached && Date.now() - cached.fetchedAt < BOOTSTRAP_TTL_MS) return cached.servers;

  try {
    const res = await fetch(env.RDAP_BOOTSTRAP_URL, { headers: HEADERS, signal: AbortSignal.timeout(TIMEOUT_MS) });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const servers = parseBootstrap(await res.json());
    globalForBootstrap.btsRdapBootstrap = { servers, fetchedAt: Date.now() };
    return servers;
  } catch (error) {
    logger.warn("domain.bootstrap.failed", { url: env.RDAP_BOOTSTRAP_URL, error });
    // A stale list is far better than none: registries almost never move.
    if (cached) return cached.servers;
    throw new BootstrapUnavailableError(`Could not load the RDAP server list (${describe(error)})`);
  }
}

// ---------------------------------------------------------------------------
// One lookup
// ---------------------------------------------------------------------------

function describe(error: unknown): string {
  if (error instanceof Error) {
    if (error.name === "TimeoutError") return `no answer within ${TIMEOUT_MS / 1000} s`;
    const code = (error.cause as { code?: string } | undefined)?.code;
    return code ? `${error.message} (${code})` : error.message;
  }
  return String(error);
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

function retryDelay(res: Response | null): number {
  const seconds = Number(res?.headers.get("retry-after"));
  return Number.isFinite(seconds) && seconds > 0 ? Math.min(seconds * 1000, MAX_RETRY_AFTER_MS) : RETRY_DELAY_MS;
}

async function readJson(res: Response): Promise<unknown> {
  const text = await res.text();
  if (text.length > MAX_BODY_BYTES) throw new Error("The registry's answer is unexpectedly large");
  try {
    return JSON.parse(text);
  } catch {
    throw new Error("The registry's answer is not JSON");
  }
}

/** One request; null means "worth one retry" (rate limit, server error, network). */
async function attempt(url: string, server: string): Promise<{ outcome: LookupOutcome | null; res: Response | null; error?: string }> {
  let res: Response;
  try {
    res = await fetch(url, { headers: HEADERS, redirect: "follow", signal: AbortSignal.timeout(TIMEOUT_MS) });
  } catch (error) {
    return { outcome: null, res: null, error: describe(error) };
  }

  // Release the connection on every path that doesn't read the body.
  if (!res.ok) await res.body?.cancel().catch(() => {});
  if (res.status === 404) return { outcome: { result: "NOT_FOUND", server }, res };
  if (res.status === 429 || res.status >= 500) return { outcome: null, res, error: `the registry answered HTTP ${res.status}` };
  if (!res.ok) return { outcome: { result: "FAILED", server, error: `The registry answered HTTP ${res.status}` }, res };

  try {
    return { outcome: { result: "FOUND", server, record: parseRdapDomain(await readJson(res)) }, res };
  } catch (error) {
    return { outcome: { result: "FAILED", server, error: describe(error) }, res };
  }
}

/** Look one registered domain up at its registry. Never throws. */
export async function lookupDomain(domain: string): Promise<LookupOutcome> {
  let server: string | null;
  try {
    server = rdapServerFor(topLevelDomain(domain), await bootstrap());
  } catch (error) {
    return { result: "FAILED", server: null, error: describe(error) };
  }
  if (!server) {
    return { result: "UNSUPPORTED", server: null, error: `No RDAP server is known for .${topLevelDomain(domain)} domains` };
  }

  const url = rdapDomainUrl(server, domain);
  const first = await attempt(url, server);
  if (first.outcome) return first.outcome;

  await sleep(retryDelay(first.res));
  const second = await attempt(url, server);
  return second.outcome ?? { result: "FAILED", server, error: `Lookup failed twice: ${second.error}` };
}
