import { API_BASE_URL } from "../config/env";

export class ApiError extends Error {
  status: number;
  data: unknown;

  constructor(status: number, message: string, data: unknown) {
    super(message);
    this.name = "ApiError";
    this.status = status;
    this.data = data;
  }
}

type RequestOptions = {
  headers?: Record<string, string>;
  query?: Record<string, string | number | boolean | undefined>;
  body?: unknown;
  token?: string | null;
  signal?: AbortSignal;
};

type RequestInterceptor = (
  input: {
    method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
    path: string;
    options: RequestOptions;
  },
) =>
  | {
      method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
      path: string;
      options: RequestOptions;
    }
  | Promise<{
      method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
      path: string;
      options: RequestOptions;
    }>;
type ResponseInterceptor = (response: Response) => Response | Promise<Response>;

let authTokenGetter: (() => string | null | undefined) | null = null;
const requestInterceptors: RequestInterceptor[] = [];
const responseInterceptors: ResponseInterceptor[] = [];

export function setAuthTokenGetter(getter: (() => string | null | undefined) | null) {
  authTokenGetter = getter;
}

export function addRequestInterceptor(interceptor: RequestInterceptor) {
  requestInterceptors.push(interceptor);
}

export function addResponseInterceptor(interceptor: ResponseInterceptor) {
  responseInterceptors.push(interceptor);
}

function buildUrl(path: string, query?: RequestOptions["query"]) {
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
  const url = new URL(`${API_BASE_URL}${normalizedPath}`);

  if (query) {
    Object.entries(query).forEach(([key, value]) => {
      if (value !== undefined) url.searchParams.set(key, String(value));
    });
  }

  return url.toString();
}

async function parseResponse<T>(response: Response): Promise<T> {
  const contentType = response.headers.get("content-type") || "";
  const data = contentType.includes("application/json")
    ? await response.json()
    : await response.text();

  if (!response.ok) {
    const message =
      typeof data === "object" && data !== null && "message" in data
        ? String((data as { message: unknown }).message)
        : `Request failed with status ${response.status}`;

    throw new ApiError(response.status, message, data);
  }

  return data as T;
}

async function request<T>(
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
  path: string,
  options: RequestOptions = {},
): Promise<T> {
  let requestInput = { method, path, options };
  for (const interceptor of requestInterceptors) {
    requestInput = await interceptor(requestInput);
  }

  const { headers = {}, query, body, token, signal } = requestInput.options;

  let response = await fetch(buildUrl(requestInput.path, query), {
    method: requestInput.method,
    headers: {
      "Content-Type": "application/json",
      ...(token ? { Authorization: `Bearer ${token}` } : {}),
      ...headers,
    },
    body: body === undefined ? undefined : JSON.stringify(body),
    signal,
  });

  for (const interceptor of responseInterceptors) {
    response = await interceptor(response);
  }

  return parseResponse<T>(response);
}

export const apiClient = {
  get: <T>(path: string, options?: Omit<RequestOptions, "body">) =>
    request<T>("GET", path, options),
  post: <T>(path: string, body?: unknown, options?: RequestOptions) =>
    request<T>("POST", path, { ...options, body }),
  put: <T>(path: string, body?: unknown, options?: RequestOptions) =>
    request<T>("PUT", path, { ...options, body }),
  patch: <T>(path: string, body?: unknown, options?: RequestOptions) =>
    request<T>("PATCH", path, { ...options, body }),
  delete: <T>(path: string, options?: Omit<RequestOptions, "body">) =>
    request<T>("DELETE", path, options),
};

// Default interceptor setup: attach bearer token if configured.
addRequestInterceptor((input) => {
  const token = input.options.token ?? authTokenGetter?.() ?? null;
  return {
    ...input,
    options: {
      ...input.options,
      token,
    },
  };
});
