/**
 * Central typed API client.
 *
 * This is the single point of contact between the frontend and the BanGUI
 * backend. Components and hooks never call `fetch` directly — they use the
 * functions exported from domain-specific API modules (e.g. `api/bans.ts`),
 * which call the helpers exported from this file.
 *
 * All request and response types are defined in `src/types/` and used here
 * to guarantee type safety at the API boundary.
 */

import { ENDPOINTS } from "./endpoints";

/** Base URL for all API calls. Falls back to `/api` in production. */
const BASE_URL: string = import.meta.env.VITE_API_URL ?? "/api";

// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------

/** Thrown by the API client when the server returns a non-2xx response. */
export class ApiError extends Error {
  /** HTTP status code returned by the server. */
  public readonly status: number;

  /** Raw response body text as returned by the server. */
  public readonly body: string;

  /**
   * @param status - The HTTP status code.
   * @param body   - The raw response body text.
   */
  constructor(status: number, body: string) {
    super(`API error ${String(status)}: ${body}`);
    this.name = "ApiError";
    this.status = status;
    this.body = body;
  }
}

/** Custom event name emitted when the backend signals an expired or invalid session. */
export const SESSION_EXPIRED_EVENT = "bangui:session-expired";

/**
 * Returns `true` when the error represents an expired or unauthorized session.
 *
 * @param err - The error returned from the API client.
 */
export function isAuthError(err: unknown): err is ApiError {
  return err instanceof ApiError && (err.status === 401 || err.status === 403);
}

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/**
 * Execute a `fetch` call and return the parsed JSON body as `T`.
 *
 * @param url     - Fully-qualified URL.
 * @param options - Standard `RequestInit` options.
 * @returns Parsed JSON response cast to `T`.
 * @throws {ApiError} When the server returns a non-2xx status code.
 */
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
  const response: Response = await fetch(url, {
    ...options,
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
      ...(options.headers as Record<string, string> | undefined),
    },
  });

  if (!response.ok) {
    const body: string = await response.text();

    if (response.status === 401 || response.status === 403) {
      if (typeof window !== "undefined") {
        window.dispatchEvent(new Event(SESSION_EXPIRED_EVENT));
      }
    }

    throw new ApiError(response.status, body);
  }

  // 204 No Content — return undefined cast to T.
  if (response.status === 204) {
    return undefined as unknown as T;
  }

  return (await response.json()) as T;
}

// ---------------------------------------------------------------------------
// Public HTTP verb helpers
// ---------------------------------------------------------------------------

/**
 * Perform a GET request to the given path.
 *
 * @param path - API path relative to `BASE_URL`, e.g. `"/jails"`.
 * @returns Parsed response body typed as `T`.
 */
export async function get<T>(path: string, signal?: AbortSignal): Promise<T> {
  return request<T>(`${BASE_URL}${path}`, { signal });
}

/**
 * Perform a POST request with a JSON body.
 *
 * @param path - API path relative to `BASE_URL`.
 * @param body - Request payload to serialise as JSON.
 * @param signal - Optional abort signal for request cancellation.
 * @returns Parsed response body typed as `T`.
 */
export async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
  return request<T>(`${BASE_URL}${path}`, {
    method: "POST",
    body: JSON.stringify(body),
    signal,
  });
}

/**
 * Perform a PUT request with a JSON body.
 *
 * @param path - API path relative to `BASE_URL`.
 * @param body - Request payload to serialise as JSON.
 * @param signal - Optional abort signal for request cancellation.
 * @returns Parsed response body typed as `T`.
 */
export async function put<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {
  return request<T>(`${BASE_URL}${path}`, {
    method: "PUT",
    body: JSON.stringify(body),
    signal,
  });
}

/**
 * Perform a DELETE request, optionally with a JSON body.
 *
 * @param path - API path relative to `BASE_URL`.
 * @param body - Optional request payload.
 * @param signal - Optional abort signal for request cancellation.
 * @returns Parsed response body typed as `T`.
 */
export async function del<T>(path: string, body?: unknown, signal?: AbortSignal): Promise<T> {
  return request<T>(`${BASE_URL}${path}`, {
    method: "DELETE",
    body: body !== undefined ? JSON.stringify(body) : undefined,
    signal,
  });
}

/** Convenience namespace bundling all HTTP helpers. */
export const api = { get, post, put, del } as const;

// Re-export endpoints so callers only need to import from `client.ts` if desired.
export { ENDPOINTS };
