const API_BASE = '/api/v1';

export interface ApiError extends Error {
  status?: number;
  retryAfter?: number;
}

async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
  const headers: Record<string, string> = {};
  if (body !== undefined) {
    headers['Content-Type'] = 'application/json';
  }

  const response = await fetch(API_BASE + path, {
    method,
    headers,
    credentials: 'include',
    body: body !== undefined ? JSON.stringify(body) : undefined,
  });

  if (!response.ok) {
    let errorMessage = 'Request failed: ' + response.status;
    let retryAfter: number | undefined;
    try {
      const errorData = await response.json();
      if (typeof errorData?.message === 'string') {
        errorMessage = errorData.message;
      } else if (typeof errorData?.error === 'string') {
        errorMessage = errorData.error;
      }
      if (response.status === 429 && typeof errorData?.retry_after === 'number') {
        retryAfter = errorData.retry_after;
      }
    } catch {
      // ignore parse error
    }
    const err = new Error(errorMessage) as ApiError;
    err.status = response.status;
    if (response.status === 429) {
      const ra = response.headers.get('Retry-After');
      err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined);
    }
    throw err;
  }

  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
}

export const api = {
  get: <T>(path: string) => request<T>('GET', path),
  post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
  put: <T>(path: string, body?: unknown) => request<T>('PUT', path, body),
  patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
  delete: <T>(path: string) => request<T>('DELETE', path),
};

export default api;
