/**
 * Mocked rpki_query_service for Playwright: intercepts every /api/v1 request
 * and answers from the fixture dataset. Filter/query params are honored so
 * tests can assert both request construction and rendered results.
 */
import type { Page, Route } from "@playwright/test";
import {
  CER_PROJECTION,
  CHAIN_EDGES,
  CRL_PROJECTION,
  CRL_REVOKED,
  EXPLAIN_RECORD,
  HEALTH,
  MFT_FILES,
  MFT_PROJECTION,
  OBJECTS,
  PPS,
  REPOS,
  ROA_PROJECTION,
  RUNS,
  RUN_2,
  SERVICE_INFO,
  STATS_OBJECT_TYPES,
  STATS_OVERVIEW,
  STATS_REASONS,
  STATS_VALIDATION,
  VALIDATION_SUMMARY,
  VRPS,
} from "./fixtures";

export interface MockJob {
  jobId: string;
  runId: string;
  scope: string;
  status: string;
  createdAt: string;
  finishedAt: string | null;
  objectCount: number;
  bytesWritten: number;
  error: string | null;
  polls: number;
}

export interface MockApi {
  jobs: Map<string, MockJob>;
  seedJob: (partial?: Partial<MockJob>) => MockJob;
}

function envelope(data: unknown, nextCursor: string | null = null, limit = 100) {
  return {
    data,
    page: data === null || !Array.isArray(data) ? null : { nextCursor, limit },
    meta: { runId: "run_0002", schemaVersion: 1 },
  };
}

function fulfillJson(route: Route, data: unknown, nextCursor: string | null = null, status = 200) {
  return route.fulfill({
    status,
    contentType: "application/json",
    body: JSON.stringify(envelope(data, nextCursor)),
  });
}

function fulfillError(route: Route, status: number, message: string) {
  return route.fulfill({
    status,
    contentType: "application/json",
    body: JSON.stringify({ error: message }),
  });
}

/** Offset-cursor pagination honoring ?limit / ?cursor. */
function paginate<T>(items: T[], url: URL, defaultLimit = 50): { page: T[]; next: string | null } {
  const limit = Math.max(1, Number(url.searchParams.get("limit") ?? defaultLimit));
  const offset = Number(url.searchParams.get("cursor") ?? 0);
  const page = items.slice(offset, offset + limit);
  const nextOffset = offset + limit;
  return { page, next: nextOffset < items.length ? String(nextOffset) : null };
}

/** VRP lookup pagination with the backend's `vrp:<offset>` cursor format. */
function paginateVrp<T>(items: T[], url: URL): { page: T[]; next: string | null } {
  const limit = Math.max(1, Number(url.searchParams.get("limit") ?? 50));
  const raw = url.searchParams.get("cursor") ?? "";
  const offset = raw.startsWith("vrp:") ? Number(raw.slice(4)) || 0 : 0;
  const page = items.slice(offset, offset + limit);
  const nextOffset = offset + limit;
  return { page, next: nextOffset < items.length ? `vrp:${nextOffset}` : null };
}

function ipv4ToInt(addr: string): number | null {
  const parts = addr.split(".").map(Number);
  if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
  return ((parts[0] * 256 + parts[1]) * 256 + parts[2]) * 256 + parts[3];
}

/**
 * Mock coverage semantics: VRP covers an IP when its prefix contains it;
 * covers a queried prefix when the VRP prefix contains it (shorter or equal
 * length) AND vrp.maxLength >= queried length. IPv6 uses a leading-hextet
 * heuristic — good enough for the fixture dataset.
 */
function vrpCovers(
  vrp: { prefix: string; maxLength: number },
  query: { ip?: string; prefix?: string },
): boolean {
  const [net, lenRaw] = vrp.prefix.split("/");
  const vlen = Number(lenRaw);
  if (vrp.prefix.includes(":")) {
    const head = net.split("::")[0].replace(/:$/, "");
    const target = query.ip ?? query.prefix?.split("/")[0] ?? "";
    if (!target.includes(":")) return false;
    if (query.prefix) {
      const qlen = Number(query.prefix.split("/")[1]);
      if (!(vlen <= qlen && vrp.maxLength >= qlen)) return false;
    }
    return target === net || target.startsWith(`${head}:`);
  }
  if (query.ip) {
    if (query.ip.includes(":")) return false;
    const netInt = ipv4ToInt(net);
    const ipInt = ipv4ToInt(query.ip);
    if (netInt === null || ipInt === null) return false;
    return vlen === 0 || netInt >>> (32 - vlen) === ipInt >>> (32 - vlen);
  }
  if (query.prefix) {
    const [qnet, qlenRaw] = query.prefix.split("/");
    const qlen = Number(qlenRaw);
    const netInt = ipv4ToInt(net);
    const qnetInt = ipv4ToInt(qnet);
    if (netInt === null || qnetInt === null) return false;
    if (!(vlen <= qlen && vrp.maxLength >= qlen)) return false;
    return vlen === 0 || netInt >>> (32 - vlen) === qnetInt >>> (32 - vlen);
  }
  return false;
}

function applyObjectFilters(items: typeof OBJECTS, url: URL): typeof OBJECTS {
  let out = items;
  const type = url.searchParams.get("type")?.toLowerCase();
  const result = url.searchParams.get("result")?.toLowerCase();
  const rejected = url.searchParams.get("rejected");
  const reason = url.searchParams.get("reason")?.toLowerCase();
  const q = url.searchParams.get("q")?.toLowerCase();
  if (type) {
    const alias: Record<string, string> = { mft: "manifest", cer: "certificate", asa: "aspa" };
    const wanted = alias[type] ?? type;
    out = out.filter((o) => o.objectType.toLowerCase() === wanted);
  }
  if (result) out = out.filter((o) => o.result.toLowerCase() === result);
  if (rejected === "true") out = out.filter((o) => o.rejected);
  if (rejected === "false") out = out.filter((o) => !o.rejected);
  if (reason) out = out.filter((o) => o.rejectReason?.toLowerCase().includes(reason));
  if (q) out = out.filter((o) => o.uri.toLowerCase().includes(q));
  return out;
}

export async function installMockApi(
  page: Page,
  options: { failLatestRun?: boolean } = {},
): Promise<MockApi> {
  const jobs = new Map<string, MockJob>();
  let jobSeq = 0;

  /** Simulate async completion: a running job completes on its second read. */
  const advanceJob = (job: MockJob) => {
    job.polls += 1;
    if (job.polls >= 2 && job.status === "running") {
      job.status = "complete";
      job.finishedAt = "2026-07-17T00:10:05Z";
      job.objectCount = 1;
      job.bytesWritten = 2048;
    }
  };

  const seedJob = (partial: Partial<MockJob> = {}): MockJob => {
    jobSeq += 1;
    const job: MockJob = {
      jobId: `job-${String(jobSeq).padStart(4, "0")}`,
      runId: "run_0002",
      scope: "object_set",
      status: "running",
      createdAt: "2026-07-17T00:10:00Z",
      finishedAt: null,
      objectCount: 0,
      bytesWritten: 0,
      error: null,
      polls: 0,
      ...partial,
    };
    jobs.set(job.jobId, job);
    return job;
  };

  await page.route("**/api/v1/**", async (route) => {
    const request = route.request();
    const url = new URL(request.url());
    const path = url.pathname.replace(/^\/api\/v1\/?/, "");
    const segments = path.split("/").filter(Boolean).map(decodeURIComponent);

    // Service-level
    if (segments.length === 0) return fulfillJson(route, SERVICE_INFO);
    if (path === "healthz") return fulfillJson(route, HEALTH);

    if (segments[0] === "runs" || segments[0] === "latest_run") {
      const isLatestAlias = segments[0] === "latest_run";
      const rawRun = isLatestAlias ? "latest" : segments[1];
      const rest = isLatestAlias ? [] : segments.slice(2);

      if (!isLatestAlias && segments.length === 1) {
        const { page: items, next } = paginate(RUNS, url);
        return fulfillJson(route, items, next);
      }

      const run = rawRun === "latest" || rawRun === "latest_run" ? RUN_2 : RUNS.find((r) => r.runId === rawRun);
      if (!run) return fulfillError(route, 404, "run not found");

      if (isLatestAlias || rest.length === 0) {
        if (options.failLatestRun) return fulfillError(route, 500, "index unavailable");
        return fulfillJson(route, run);
      }

      const [section, a, b, c] = rest;

      if (section === "artifacts") return fulfillJson(route, run.artifactPaths);
      if (section === "summary") {
        return fulfillJson(route, {
          publicationPoints: run.counts.publicationPoints,
          objects: run.counts.objects,
          repos: REPOS.length,
          vrps: run.counts.vrps,
          aspas: run.counts.aspas,
        });
      }

      if (section === "repos" && !a) {
        let items = REPOS;
        if (url.searchParams.get("sort") === "objects") {
          const dir = url.searchParams.get("order") === "asc" ? 1 : -1;
          items = [...REPOS].sort((x, y) => dir * (x.objects - y.objects));
        }
        const { page: itemsPage, next } = paginate(items, url);
        return fulfillJson(route, itemsPage, next);
      }
      if (section === "repos" && a) {
        const repo = REPOS.find((r) => r.repoId === a);
        if (!repo) return fulfillError(route, 404, "repo not found");
        if (!b) return fulfillJson(route, repo);
        if (b === "stats") {
          return fulfillJson(route, {
            publicationPoints: repo.publicationPoints,
            objects: repo.objects,
            rejectedObjects: repo.rejectedObjects,
            syncDurationMsTotal: repo.syncDurationMsTotal,
            phases: repo.phases,
            terminalStates: repo.terminalStates,
          });
        }
        if (b === "publication-points") {
          const { page: items, next } = paginate(PPS.filter((p) => p.repoId === a), url);
          return fulfillJson(route, items, next);
        }
        if (b === "objects") {
          const filtered = applyObjectFilters(OBJECTS.filter((o) => o.repoId === a), url);
          const { page: items, next } = paginate(filtered, url);
          return fulfillJson(route, items, next);
        }
      }

      if (section === "publication-points" && !a) {
        const { page: items, next } = paginate(PPS, url);
        return fulfillJson(route, items, next);
      }
      if (section === "publication-points" && a) {
        const pp = PPS.find((p) => p.ppId === a);
        if (!pp) return fulfillError(route, 404, "publication point not found");
        if (!b) return fulfillJson(route, pp);
        if (b === "stats") {
          return fulfillJson(route, {
            objects: pp.objects,
            rejectedObjects: pp.rejectedObjects,
            warnings: pp.warnings,
            repoSyncSource: pp.repoSyncSource,
            repoSyncPhase: pp.repoSyncPhase,
            repoSyncDurationMs: pp.repoSyncDurationMs,
            repoTerminalState: pp.repoTerminalState,
          });
        }
        if (b === "objects") {
          const filtered = applyObjectFilters(OBJECTS.filter((o) => o.ppId === a), url);
          const { page: items, next } = paginate(filtered, url);
          return fulfillJson(route, items, next);
        }
      }

      if (section === "objects" && a === "by-uri") {
        const uri = url.searchParams.get("uri");
        const obj = OBJECTS.find((o) => o.uri === uri);
        if (!obj) return fulfillError(route, 404, "object not found");
        return fulfillJson(route, obj);
      }
      if (section === "objects" && !a) {
        const filtered = applyObjectFilters(OBJECTS, url);
        const { page: items, next } = paginate(filtered, url);
        return fulfillJson(route, items, next);
      }
      if (section === "objects" && a) {
        const obj = OBJECTS.find((o) => o.objectInstanceId === a);
        if (!obj) return fulfillError(route, 404, "object not found");
        if (!b) return fulfillJson(route, obj);
        if (b === "parsed" && !c) {
          if (obj.objectType === "roa") return fulfillJson(route, ROA_PROJECTION);
          if (obj.objectType === "manifest") return fulfillJson(route, MFT_PROJECTION);
          if (obj.objectType === "certificate") return fulfillJson(route, CER_PROJECTION);
          if (obj.objectType === "crl") return fulfillJson(route, CRL_PROJECTION);
          return fulfillJson(route, null);
        }
        if (b === "parsed" && c === "manifest-files") {
          const { page: items, next } = paginate(MFT_FILES, url, 100);
          return fulfillJson(route, items, next);
        }
        if (b === "parsed" && c === "revoked-certs") {
          const { page: items, next } = paginate(obj.objectType === "crl" ? CRL_REVOKED : [], url, 100);
          return fulfillJson(route, items, next);
        }
        if (b === "validation" && !c) return fulfillJson(route, VALIDATION_SUMMARY);
        if (b === "validation" && c === "explain" && request.method() === "POST") {
          return fulfillJson(route, EXPLAIN_RECORD);
        }
        if (b === "chain") return fulfillJson(route, CHAIN_EDGES);
        if (b === "raw") {
          return route.fulfill({
            status: 200,
            contentType: "application/octet-stream",
            body: Buffer.from([0x30, 0x82, 0x01, 0x01, 0x02, 0x01, 0x05]),
          });
        }
      }

      if (section === "issues") {
        let items = OBJECTS.filter((o) => o.rejected || o.result === "error");
        const reason = url.searchParams.get("reason")?.toLowerCase();
        const type = url.searchParams.get("type")?.toLowerCase();
        const repoId = url.searchParams.get("repoId");
        if (reason) items = items.filter((o) => o.rejectReason?.toLowerCase().includes(reason));
        if (type) items = items.filter((o) => o.objectType.toLowerCase() === type);
        if (repoId) items = items.filter((o) => o.repoId === repoId);
        const { page: pageItems, next } = paginate(items, url);
        return fulfillJson(route, pageItems, next);
      }

      if (section === "vrps" && a === "lookup") {
        const ip = url.searchParams.get("ip") ?? undefined;
        const prefix = url.searchParams.get("prefix") ?? undefined;
        if (ip && prefix) return fulfillError(route, 400, "ip and prefix are mutually exclusive");
        if (!ip && !prefix) return fulfillError(route, 400, "ip or prefix query parameter is required");
        const asnRaw = url.searchParams.get("asn");
        let items = VRPS.filter((v) => vrpCovers(v, { ip, prefix }));
        if (asnRaw !== null) {
          const asn = Number(asnRaw);
          if (!Number.isInteger(asn) || asn < 0) {
            return fulfillError(route, 400, `invalid ASN query parameter: ${asnRaw}`);
          }
          items = items.filter((v) => v.asn === asn);
        }
        const { page: vrpPage, next } = paginateVrp(items, url);
        return fulfillJson(route, vrpPage, next);
      }

      if (section === "search") {
        const q = (url.searchParams.get("q") ?? "").trim();
        if (!q) return fulfillError(route, 400, "q query parameter is required");
        const lower = q.toLowerCase();
        const byUri = OBJECTS.find((o) => o.uri === q);
        const byHash = OBJECTS.find((o) => o.sha256 === lower);
        const objects = byUri ? [byUri] : byHash ? [byHash] : [];
        const repos = REPOS.filter(
          (r) => r.host.toLowerCase().includes(lower) || r.uri.toLowerCase().includes(lower),
        );
        const pps = PPS.filter((p) =>
          [p.manifestRsyncUri, p.publicationPointRsyncUri, p.rrdpNotificationUri]
            .filter(Boolean)
            .some((u) => u!.toLowerCase().includes(lower)),
        );
        return fulfillJson(route, { objects, repos, publicationPoints: pps });
      }

      if (section === "stats") {
        if (a === "validation") return fulfillJson(route, STATS_VALIDATION);
        if (a === "object-types") return fulfillJson(route, STATS_OBJECT_TYPES);
        if (a === "reasons") return fulfillJson(route, STATS_REASONS);
        if (a === "overview") return fulfillJson(route, STATS_OVERVIEW);
        if (a === "downloads") return fulfillJson(route, { requests: 42, bytes: 1234567 });
        return fulfillJson(route, {});
      }

      if (section === "exports" && !a) {
        if (request.method() === "POST") {
          const job = seedJob({ objectCount: 0 });
          return fulfillJson(route, job);
        }
        const list = [...jobs.values()].sort((x, y) => y.createdAt.localeCompare(x.createdAt));
        list.forEach(advanceJob);
        return fulfillJson(route, list, null);
      }
      if (section === "exports" && a) {
        const job = jobs.get(a);
        if (!job) return fulfillError(route, 404, "export job not found");
        if (b === "download") {
          if (job.status !== "complete") return fulfillError(route, 409, "export not complete");
          return route.fulfill({
            status: 200,
            contentType: "application/x-tar",
            body: Buffer.from("mock-tar-bytes"),
          });
        }
        advanceJob(job);
        return fulfillJson(route, job);
      }

      return fulfillError(route, 404, `no mock for ${url.pathname}`);
    }

    return fulfillError(route, 404, `no mock for ${url.pathname}`);
  });

  return { jobs, seedJob };
}
