/**
 * Cliente HTTP de bajo nivel sobre fetch.
 *
 * - Inyecta `Authorization: Bearer <token>` y `X-App-Version`.
 * - Serializa/parsea JSON (excepto multipart, que pasa el FormData tal cual).
 * - Lanza `ApiError` con el status para que las capas superiores decidan.
 * - Notifica al `onUnauthorized` registrado cuando el backend responde 401
 *   (token caducado/revocado → la sesión debe forzar re-login).
 */
import { APP_VERSION, BASE_URL } from '../config';

export class ApiError extends Error {
  constructor(
    public status: number,
    message: string,
    public body?: unknown,
  ) {
    super(message);
    this.name = 'ApiError';
  }
}

type TokenGetter = () => string | null;

let getToken: TokenGetter = () => null;
let onUnauthorized: (() => void) | null = null;

/** La sesión registra de dónde sacar el token y qué hacer ante un 401. */
export function configureClient(opts: {
  getToken: TokenGetter;
  onUnauthorized?: () => void;
}) {
  getToken = opts.getToken;
  onUnauthorized = opts.onUnauthorized ?? null;
}

export interface RequestOptions {
  method?: 'GET' | 'POST';
  /** Cuerpo JSON (ignorado si se pasa `form`). */
  json?: unknown;
  /** Cuerpo multipart/form-data (para /media). */
  form?: FormData;
  /** Parámetros de query; los valores se URL-encodean. */
  query?: Record<string, string | number | undefined>;
  /** Si false, no adjunta el token (p.ej. /login). Por defecto true. */
  auth?: boolean;
  signal?: AbortSignal;
}

function buildUrl(path: string, query?: RequestOptions['query']): string {
  let url = BASE_URL + path;
  if (query) {
    // Construcción manual (el polyfill de URL en RN es incompleto). encodeURIComponent
    // codifica el `+` del offset horario del `since` como %2B, según exige el protocolo.
    const parts: string[] = [];
    for (const [k, v] of Object.entries(query)) {
      if (v !== undefined && v !== null) {
        parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
      }
    }
    if (parts.length) url += `?${parts.join('&')}`;
  }
  return url;
}

export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
  const { method = 'GET', json, form, query, auth = true, signal } = opts;

  const headers: Record<string, string> = {
    Accept: 'application/json',
    'X-App-Version': APP_VERSION,
  };

  if (auth) {
    const token = getToken();
    if (token) headers.Authorization = `Bearer ${token}`;
  }

  let body: BodyInit | undefined;
  if (form) {
    // No fijamos Content-Type: el runtime añade el boundary del multipart.
    body = form as unknown as BodyInit;
  } else if (json !== undefined) {
    headers['Content-Type'] = 'application/json';
    body = JSON.stringify(json);
  }

  const res = await fetch(buildUrl(path, query), { method, headers, body, signal });

  if (res.status === 401 && auth) {
    onUnauthorized?.();
  }

  const text = await res.text();
  const parsed = text ? safeJson(text) : null;

  if (!res.ok) {
    const message =
      (parsed && typeof parsed === 'object' && 'message' in parsed
        ? String((parsed as Record<string, unknown>).message)
        : null) ?? `HTTP ${res.status}`;
    throw new ApiError(res.status, message, parsed ?? text);
  }

  return parsed as T;
}

function safeJson(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch {
    return text;
  }
}
