import { redirect } from "next/navigation";
import { getToken } from "@/lib/session";

const API_URL = process.env.NEXT_PUBLIC_API_URL;

export class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public errors?: Record<string, string[]>,
  ) {
    super(message);
  }
}

/**
 * Server-side fetch helper for Server Components — calls the Laravel API
 * directly. Pass `auth: true` to attach the httpOnly-cookie Bearer token
 * (see lib/session.ts) *and* auto-redirect to `/login` on 401 — every
 * `auth: true` call site lives inside a layout that already requires login
 * to render at all (admin/organizer/petugas/dashboard/checkout), so a 401
 * here only ever means "the session died mid-browse" (expired/rotated
 * token on a client-side navigation, where the layout itself doesn't
 * re-run) — letting it bubble up as an uncaught ApiError previously crashed
 * the page with a raw runtime error instead of sending the user back to
 * login. Pass `auth: "optional"` instead for the one legitimate case where
 * a 401 is expected and should be handled by the caller, not redirected
 * (getCurrentUser() — used on public pages to probe "is anyone logged in"
 * without forcing a login wall).
 *
 * Not cached by default (`cache: "no-store"`): Cache Components is not
 * enabled in this project (see next.config.ts), so this behaves the same
 * as a plain uncached fetch would in Next 14/15 — no `use cache` or
 * mandatory <Suspense> wrapping needed for this to work.
 *
 * Pass `revalidate` (seconds) for public, non-personalized endpoints to opt
 * into Next's shared fetch cache instead — every visitor within that window
 * gets served the same cached response instead of triggering a fresh PHP-FPM
 * process on the shared-hosting backend for each request. Never pass this
 * alongside `auth: true`/session-dependent data, since the cached response
 * would leak across users.
 *
 * Any `revalidate`d fetch is also eligible for build-time prerendering
 * (`next build` calls it directly to seed the static shell), which means it
 * needs the backend to be reachable *from the build machine* — unlike a
 * `no-store` fetch, which Next always defers to request time. The
 * shared-hosting backend occasionally answers with a transient 502/503 when
 * its own process limit is momentarily maxed (see kolabora-master hosting
 * notes), so those specific statuses get a couple of short retries below —
 * without this, one flaky moment during `next build` fails the entire build,
 * not just one request.
 */
const TRANSIENT_STATUS = new Set([502, 503, 504]);
const RETRY_DELAYS_MS = [300, 800];

function delay(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

export async function apiFetch<T>(
  path: string,
  options: RequestInit & { auth?: boolean | "optional"; revalidate?: number } = {},
): Promise<T> {
  const { auth, headers, revalidate, ...rest } = options;
  const requestHeaders = new Headers(headers);
  requestHeaders.set("Accept", "application/json");

  if (auth) {
    const token = await getToken();
    if (token) {
      requestHeaders.set("Authorization", `Bearer ${token}`);
    }
  }

  const fetchInit: RequestInit = {
    ...rest,
    headers: requestHeaders,
    ...(revalidate !== undefined ? { next: { revalidate } } : { cache: "no-store" }),
  };

  let res: Response | undefined;
  for (let attempt = 0; ; attempt++) {
    try {
      res = await fetch(`${API_URL}${path}`, fetchInit);
    } catch (e) {
      // Next.js's App Router instruments the global fetch() to throw its own
      // internal control-flow signal (digest "DYNAMIC_SERVER_USAGE") when a
      // no-store fetch happens inside a route it's attempting to prerender
      // statically at build time — that's how it detects "this page needs to
      // be dynamic" and bails out gracefully. It is NOT a real network
      // failure and must propagate unmodified, or Next.js never sees its own
      // signal and the build crashes instead of just marking the route
      // dynamic (exactly what happened before this fix — every page that
      // renders Navbar/Footer, i.e. all of them, failed to prerender).
      if (e instanceof Error && "digest" in e && typeof e.digest === "string" && e.digest.startsWith("DYNAMIC_SERVER_USAGE")) {
        throw e;
      }

      // Network failure (backend unreachable, DNS, offline) — distinct from an
      // HTTP error response, which is handled below. status 0 is never a real
      // HTTP status, so `error.status === 404/403` checks elsewhere correctly
      // fall through to the generic error boundary instead of misfiring.
      throw new ApiError("Unable to reach the server. Please try again in a moment.", 0);
    }

    if (TRANSIENT_STATUS.has(res.status) && attempt < RETRY_DELAYS_MS.length) {
      await delay(RETRY_DELAYS_MS[attempt]);
      continue;
    }
    break;
  }

  if (res.status === 401 && auth === true) {
    redirect("/login");
  }

  const body = await res.json().catch(() => null);

  if (!res.ok) {
    throw new ApiError(body?.message ?? "Terjadi kesalahan.", res.status, body?.errors);
  }

  return body as T;
}
