/**
 * Auth-aware fetch wrapper.
 *
 * Every authenticated backend call in the SPA goes through `authedFetch`.
 * Responsibilities:
 *
 *   1. Attach `credentials: "include"` so the `cyclone_session` HttpOnly
 *      cookie rides along on cross-origin XHR calls.
 *   2. Tag every request with `Accept: application/json` by default so
 *      the FastAPI backend knows to return its structured error shape
 *      (`{"error": "...", "detail": "..."}`) on failures.
 *   3. On a 401 from anything OTHER than the auth endpoints themselves,
 *      redirect to `/login?next=<current>` so the operator sees a real
 *      sign-in screen instead of an infinite stream of failing queries.
 *      401 from `/api/auth/*` is the normal "bad password" path — let
 *      the caller handle it.
 *   4. On any other non-2xx, parse the JSON body and throw an `ApiError`
 *      carrying `status`, the backend's `error` code, and the `detail`
 *      string. The Login page and hooks both branch on `.code`.
 *   5. On 204, return `undefined` (so callers can `await` without
 *      blowing up on `res.json()` of an empty body).
 *   6. Otherwise return the parsed JSON.
 *
 * `authApi` is the typed wrapper around the three auth endpoints
 * (`/api/auth/login`, `/api/auth/me`, `/api/auth/logout`).
 */

const BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined) ?? "";

function joinUrl(path: string): string {
  if (BASE_URL) return `${BASE_URL.replace(/\/$/, "")}${path}`;
  return path;
}

/**
 * Error thrown for any non-2xx `authedFetch` response. Carries the HTTP
 * `status`, the backend's `error` code (so callers can branch on
 * `err.code === "invalid_credentials"`, etc.), and the optional `detail`
 * string for surfacing in toasts.
 *
 * Distinct from the `ApiError` in `src/lib/api.ts` — that one only
 * carries `status` and is used by the existing pages; this one is the
 * richer auth-aware variant that the Login page + hooks depend on.
 */
export class ApiError extends Error {
  status: number;
  code: string;
  detail?: string;
  constructor(status: number, code: string, detail?: string) {
    super(detail ?? code);
    this.name = "ApiError";
    this.status = status;
    this.code = code;
    this.detail = detail;
  }
}

function redirectToLogin() {
  const next = encodeURIComponent(window.location.pathname + window.location.search);
  window.location.href = `/login?next=${next}`;
}

export async function authedFetch<T = unknown>(
  path: string,
  init?: RequestInit
): Promise<T> {
  const res = await fetch(joinUrl(path), {
    credentials: "include",
    headers: { Accept: "application/json", ...(init?.headers ?? {}) },
    ...init,
  });
  if (res.status === 401 && !path.startsWith("/api/auth/")) {
    redirectToLogin();
    throw new ApiError(401, "session_expired");
  }
  if (!res.ok) {
    let body: any = null;
    try {
      body = await res.json();
    } catch {
      /* no body */
    }
    const code = body?.error ?? "error";
    const detail = body?.detail ?? res.statusText;
    throw new ApiError(res.status, code, detail);
  }
  if (res.status === 204) return undefined as T;
  return (await res.json()) as T;
}

/**
 * Like `authedFetch` but returns the response body as text instead of
 * parsed JSON. Used by `serializeClaim837` (the SP8 endpoint returns
 * `text/x12`, not JSON). Same 401-redirect + error-shape behavior as
 * the JSON variant.
 */
export async function authedFetchText(path: string, init?: RequestInit): Promise<string> {
  const res = await fetch(joinUrl(path), {
    credentials: "include",
    ...init,
  });
  if (res.status === 401 && !path.startsWith("/api/auth/")) {
    redirectToLogin();
    throw new ApiError(401, "session_expired");
  }
  if (!res.ok) {
    let body: any = null;
    try {
      body = await res.json();
    } catch {
      /* no body */
    }
    throw new ApiError(
      res.status,
      body?.error ?? "error",
      body?.detail ?? res.statusText
    );
  }
  return res.text();
}

/**
 * Like `authedFetch` but returns the raw `Response` so the caller can
 * stream the body (NDJSON). Used by `parse837` / `parse835`. 401 still
 * redirects; the caller is responsible for reading the body.
 */
export async function authedFetchResponse(path: string, init?: RequestInit): Promise<Response> {
  const res = await fetch(joinUrl(path), {
    credentials: "include",
    ...init,
  });
  if (res.status === 401 && !path.startsWith("/api/auth/")) {
    redirectToLogin();
    throw new ApiError(401, "session_expired");
  }
  return res;
}

// ---------------------------------------------------------------------------
// Auth-specific endpoints. `login` and `logout` use raw `fetch` because
// they bypass the 401-redirect behavior on purpose — a 401 from
// /api/auth/login is "wrong password", not "session expired".
// ---------------------------------------------------------------------------

export const authApi = {
  async login(username: string, password: string) {
    const res = await fetch(joinUrl("/api/auth/login"), {
      method: "POST",
      credentials: "include",
      headers: {
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: JSON.stringify({ username, password }),
    });
    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      throw new ApiError(
        res.status,
        body.error ?? "error",
        body.detail ?? res.statusText
      );
    }
    return res.json();
  },
  async me() {
    return authedFetch("/api/auth/me");
  },
  async logout() {
    // Fire-and-forget. A network error here is fine — the server has
    // already cleared the cookie or the operator is signing out because
    // they're about to be disconnected. Either way, the client should
    // drop the user into the unauthenticated state.
    await fetch(joinUrl("/api/auth/logout"), {
      method: "POST",
      credentials: "include",
    }).catch(() => undefined);
  },
};
