const CSRF_HEADER = 'X-CSRF-Token';

let csrfToken: string | null = null;

export class ApiError extends Error {
  status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
  }
}

export interface ApiOptions {
  method?: string;
  body?: unknown;
}

// The token is tied to the server session, so it must be discarded whenever
// that session ends (logout, expiry) or later writes fail with 403.
export function clearCsrf(): void {
  csrfToken = null;
}

async function ensureCsrf(): Promise<string> {
  if (csrfToken) return csrfToken;
  const res = await fetch('/api/csrf', { credentials: 'include' });
  const data = (await res.json()) as { token: string };
  csrfToken = data.token;
  return csrfToken;
}

export async function api<T = unknown>(
  path: string,
  { method = 'GET', body }: ApiOptions = {},
): Promise<T> {
  const headers: Record<string, string> = {};
  if (body !== undefined) headers['Content-Type'] = 'application/json';
  if (method !== 'GET') {
    headers[CSRF_HEADER] = await ensureCsrf();
  }
  const res = await fetch('/api' + path, {
    method,
    headers,
    credentials: 'include',
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });
  if (res.status === 204) return null as T;
  const text = await res.text();
  let data: unknown = null;
  try {
    data = JSON.parse(text);
  } catch {
    /* ignore */
  }
  if (!res.ok) {
    const message =
      data && typeof data === 'object' && 'error' in data && typeof data.error === 'string'
        ? data.error
        : res.statusText;
    throw new ApiError(message, res.status);
  }
  return data as T;
}
