class ApiState {
  showSessionExpiredAlert = false;
  showLogoutAlert = false;
  getTokenFn: (() => string | null) | null = null;
  refreshFn: (() => Promise<boolean>) | null = null;
  refreshPromise: Promise<boolean> | null = null;

  reset() {
    this.showSessionExpiredAlert = false;
    this.showLogoutAlert = false;
    this.getTokenFn = null;
    this.refreshFn = null;
    this.refreshPromise = null;
  }
}

const state = new ApiState();

export const resetApiState = (): void => {
  state.reset();
};

export const shouldShowSessionExpiredAlert = (): boolean => {
  if (state.showSessionExpiredAlert) {
    state.showSessionExpiredAlert = false;
    return true;
  }
  return false;
};

export const setSessionExpired = (): void => {
  state.showSessionExpiredAlert = true;
};

export const shouldShowLogoutAlert = (): boolean => {
  if (state.showLogoutAlert) {
    state.showLogoutAlert = false;
    return true;
  }
  return false;
};

export const setLogoutAlert = (): void => {
  state.showLogoutAlert = true;
};

export const setTokenGetter = (fn: () => string | null): void => {
  state.getTokenFn = fn;
};

export const setRefreshFn = (fn: () => Promise<boolean>): void => {
  state.refreshFn = fn;
};

export const apiFetch = async (
  url: string,
  options: RequestInit = {},
): Promise<Response> => {
  // Honour abort signals up-front — TanStack Query may cancel a stale
  // request before we've done any work.
  if (options.signal?.aborted) {
    return new Response(null, { status: 499 });
  }

  let token: string | null = null;
  try {
    token = state.getTokenFn ? state.getTokenFn() : null;
  } catch {
    // token retrieval failed
  }

  // No usable token yet (e.g. fresh page load where the silent refresh
  // kicked off in useEffect hasn't populated the in-memory access token).
  // Refresh up front, BEFORE firing the request, so the caller never sees
  // a 401 in the network log on the first attempt. Concurrent callers join
  // the same in-flight refresh via state.refreshPromise.
  if (!token && state.refreshFn) {
    try {
      if (!state.refreshPromise) {
        state.refreshPromise = state.refreshFn().finally(() => {
          state.refreshPromise = null;
        });
      }
      const refreshed = await state.refreshPromise;
      if (options.signal?.aborted) {
        return new Response(null, { status: 499 });
      }
      if (refreshed) {
        try {
          token = state.getTokenFn ? state.getTokenFn() : null;
        } catch {
          token = null;
        }
      }
    } catch {
      // Refresh itself failed — fall through and let the request fire
      // without a token. The 401 handler below will set a session-expired
      // flag so the UI can react.
    }
  }

  const headers: Record<string, string> = {
    ...(options.headers as Record<string, string>),
  };

  if (
    !headers["Content-Type"] &&
    options.body &&
    !(options.body instanceof FormData)
  ) {
    headers["Content-Type"] = "application/json";
  }

  if (token) {
    headers["Authorization"] = `Bearer ${token}`;
  }

  let response = await fetch(url, {
    ...options,
    headers,
    credentials: "include",
  });

  if (response.status === 401 && state.refreshFn) {
    // If the caller already unmounted (TanStack Query aborted the request),
    // do not refresh + retry. Doing so would fire a global "session expired"
    // toast on a page the user has already left and leak a fetch+refresh pair.
    if (options.signal?.aborted) {
      return response;
    }
    try {
      if (!state.refreshPromise) {
        state.refreshPromise = state.refreshFn().finally(() => {
          state.refreshPromise = null;
        });
      }
      const refreshed = await state.refreshPromise;
      // Re-check after the await: the caller may have unmounted while the
      // refresh was in flight (refresh + abort race).
      if (options.signal?.aborted) {
        return response;
      }
      if (refreshed) {
        token = state.getTokenFn ? state.getTokenFn() : null;
        if (token) {
          headers["Authorization"] = `Bearer ${token}`;
        }
        const { signal: _signal, ...retryOptions } = options;
        response = await fetch(url, {
          ...retryOptions,
          headers,
          credentials: "include",
        });
      } else {
        setSessionExpired();
      }
    } catch {
      setSessionExpired();
    }
  }

  return response;
};

export const getAccessToken = (): string | null => {
  try {
    return state.getTokenFn ? state.getTokenFn() : null;
  } catch {
    return null;
  }
};

export default apiFetch;
