/**
 * Configurable API client factory.
 * Creates a fetch wrapper with base URL, auth token injection, error handling,
 * timeout, and retry with exponential backoff for idempotent requests.
 */

import type { ApiClient, ApiClientConfig, ApiResult } from './types.js';

const IDEMPOTENT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);

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

/**
 * Create an API client with a base URL and optional auth token.
 *
 * @example
 * ```ts
 * const api = createApiClient({
 *   baseUrl: "/api",
 *   getToken: () => localStorage.getItem("access_token"),
 * });
 *
 * // Throws on error
 * const users = await api.fetch<User[]>("/users");
 *
 * // Never throws
 * const { data, error } = await api.safeFetch<User[]>("/users");
 * ```
 */
export function createApiClient(config: ApiClientConfig): ApiClient {
  const {
    baseUrl,
    getToken,
    defaultHeaders,
    timeoutMs = 10_000,
    retries = 2,
    retryDelayMs = 500,
  } = config;

  function buildHeaders(options?: RequestInit): HeadersInit {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
      'x-request-id':
        typeof globalThis.crypto?.randomUUID === 'function'
          ? globalThis.crypto.randomUUID()
          : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
      ...defaultHeaders,
    };

    if (getToken) {
      const token = getToken();
      if (token) {
        headers['Authorization'] = `Bearer ${token}`;
      }
    }

    if (options?.headers) {
      const extra: Record<string, string> = {};
      if (options.headers instanceof Headers) {
        options.headers.forEach((value, key) => {
          extra[key] = value;
        });
      } else if (Array.isArray(options.headers)) {
        Object.assign(extra, Object.fromEntries(options.headers));
      } else {
        Object.assign(extra, options.headers);
      }
      Object.assign(headers, extra);
    }

    return headers;
  }

  function buildInit(options?: RequestInit): RequestInit {
    const init: RequestInit = {
      ...options,
      headers: buildHeaders(options),
    };

    // AbortController timeout (skip if caller already supplies a signal or timeoutMs is 0)
    if (timeoutMs > 0 && !options?.signal) {
      const controller = new AbortController();
      init.signal = controller.signal;
      setTimeout(() => controller.abort(), timeoutMs);
    }

    return init;
  }

  function isRetryable(method: string | undefined): boolean {
    return IDEMPOTENT_METHODS.has((method ?? 'GET').toUpperCase());
  }

  async function fetchWithRetry(url: string, init: RequestInit): Promise<Response> {
    const maxAttempts = isRetryable(init.method) ? retries + 1 : 1;
    let lastError: unknown;

    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        const res = await globalThis.fetch(url, init);
        // Only retry on 502/503/504 for idempotent methods
        if (res.status >= 502 && res.status <= 504 && attempt < maxAttempts - 1) {
          await sleep(retryDelayMs * 2 ** attempt);
          continue;
        }
        return res;
      } catch (err) {
        lastError = err;
        if (attempt < maxAttempts - 1) {
          await sleep(retryDelayMs * 2 ** attempt);
          continue;
        }
      }
    }

    throw lastError;
  }

  return {
    async fetch<T>(path: string, options?: RequestInit): Promise<T> {
      const init = buildInit(options);
      const res = await fetchWithRetry(`${baseUrl}${path}`, init);

      if (!res.ok) {
        const body = await res.json().catch(() => ({ error: res.statusText }));
        throw new Error(body.error || `HTTP ${res.status}`);
      }

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

    async safeFetch<T>(path: string, options?: RequestInit): Promise<ApiResult<T>> {
      try {
        const init = buildInit(options);
        const res = await fetchWithRetry(`${baseUrl}${path}`, init);

        if (!res.ok) {
          const body = await res.json().catch(() => ({ error: res.statusText }));
          return { data: null, error: body.error || `HTTP ${res.status}` };
        }

        const data = (await res.json()) as T;
        return { data, error: null };
      } catch {
        return { data: null, error: 'API unavailable' };
      }
    },
  };
}
