import { API_AUTH_PATHS, API_BASE_URL, API_CONTENT_TYPES, API_HEADERS } from '@/shared/constants/api';
import { ApiErrorBody, ApiRequestBody } from '@/shared/types/api';

export class ApiError extends Error {
  readonly status: number;
  readonly code?: string;
  readonly details?: ApiErrorBody['details'];

  constructor(status: number, body: ApiErrorBody) {
    super(body.message);
    this.name = 'ApiError';
    this.status = status;
    this.code = body.code;
    this.details = body.details;
  }
}

export class AuthExpiredError extends Error {
  constructor() {
    super('Authentication expired');
    this.name = 'AuthExpiredError';
  }
}

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

interface RequestOptions {
  readonly method?: HttpMethod;
  readonly body?: ApiRequestBody;
  readonly headers?: HeadersInit;
  readonly skipAuthRefresh?: boolean;
}

function createUrl(path: string): string {
  const normalizedBaseUrl = API_BASE_URL.replace(/\/$/, '');
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;

  return `${normalizedBaseUrl}${normalizedPath}`;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function isJsonValue(value: unknown): value is ApiErrorBody['details'] {
  if (
    value === null
    || typeof value === 'string'
    || typeof value === 'number'
    || typeof value === 'boolean'
  ) {
    return true;
  }

  if (Array.isArray(value)) {
    return value.every(isJsonValue);
  }

  if (isRecord(value)) {
    return Object.values(value).every(isJsonValue);
  }

  return false;
}

async function parseErrorBody(response: Response): Promise<ApiErrorBody> {
  const contentType = response.headers.get(API_HEADERS.contentType);

  if (contentType?.includes(API_CONTENT_TYPES.json)) {
    const body: unknown = await response.json();
    const message = isRecord(body) && typeof body.message === 'string'
      ? body.message
      : response.statusText || 'Request failed';
    const code = isRecord(body) && typeof body.code === 'string'
      ? body.code
      : undefined;
    const details = isRecord(body) && isJsonValue(body.details)
      ? body.details
      : undefined;

    return {
      message,
      code,
      details,
    };
  }

  const message = await response.text();

  return {
    message: message || response.statusText || 'Request failed',
  };
}

async function refreshSession(): Promise<void> {
  const response = await fetch(createUrl(API_AUTH_PATHS.refresh), {
    credentials: 'include',
    method: 'POST',
    headers: {
      [API_HEADERS.contentType]: API_CONTENT_TYPES.json,
    },
  });

  if (response.status === 401 || response.status === 403) {
    throw new AuthExpiredError();
  }

  if (!response.ok) {
    throw new ApiError(response.status, await parseErrorBody(response));
  }
}

function canRefresh(path: string, options: RequestOptions): boolean {
  return !options.skipAuthRefresh
    && path !== API_AUTH_PATHS.refresh
    && path !== API_AUTH_PATHS.signIn
    && path !== API_AUTH_PATHS.signOut;
}

async function sendRequest(path: string, options: RequestOptions): Promise<Response> {
  const headers = new Headers(options.headers);

  headers.set(API_HEADERS.contentType, API_CONTENT_TYPES.json);

  return fetch(createUrl(path), {
    credentials: 'include',
    method: options.method || 'GET',
    headers,
    body: options.body ? JSON.stringify(options.body) : undefined,
  });
}

export function apiRequest<TResponse>(
  path: string,
  options?: RequestOptions,
): Promise<TResponse>;

export async function apiRequest(
  path: string,
  options: RequestOptions = {},
): Promise<unknown> {
  let response = await sendRequest(path, options);
  let retriedAfterRefresh = false;

  // Only 401 (unauthenticated) drives the refresh/expiry path. A 403 means the
  // user is authenticated but lacks permission for this request; it must surface
  // as a normal ApiError (handled by the UI), never a session refresh or logout.
  if (response.status === 401 && canRefresh(path, options)) {
    await refreshSession();
    retriedAfterRefresh = true;
    response = await sendRequest(path, {
      ...options,
      skipAuthRefresh: true,
    });
  }

  if (response.status === 401 && (options.skipAuthRefresh || retriedAfterRefresh)) {
    throw new AuthExpiredError();
  }

  if (!response.ok) {
    throw new ApiError(response.status, await parseErrorBody(response));
  }

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

  const responseBody: unknown = await response.json();
  return responseBody;
}
