import { DependencyList, Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react";

import { getApiErrorMessage } from "../api";

export type ViewResourceErrorKind = "unauthorized" | "unavailable" | "error";

export type ViewResourceError = {
  kind: ViewResourceErrorKind;
  message: string;
  retryable: boolean;
  status?: number;
};

export type ViewResourceState<T> = {
  data: T;
  loading: boolean;
  refreshing: boolean;
  error: ViewResourceError | null;
  hasLoaded: boolean;
  reload: () => Promise<void>;
  setData: Dispatch<SetStateAction<T>>;
};

function normalizeError(error: any, fallback: string): ViewResourceError {
  const status = error?.response?.status as number | undefined;
  if (status === 401 || status === 403) {
    return {
      kind: "unauthorized",
      message: getApiErrorMessage(error, fallback),
      retryable: false,
      status,
    };
  }

  if (!status || status >= 500) {
    return {
      kind: "unavailable",
      message: getApiErrorMessage(error, fallback),
      retryable: true,
      status,
    };
  }

  return {
    kind: "error",
    message: getApiErrorMessage(error, fallback),
    retryable: true,
    status,
  };
}

export function useViewResource<T>(
  load: () => Promise<T>,
  options: {
    initialData: T;
    errorMessage: string;
    deps?: DependencyList;
    enabled?: boolean;
  },
): ViewResourceState<T> {
  const { initialData, errorMessage, deps = [], enabled = true } = options;
  const [data, setData] = useState<T>(initialData);
  const [loading, setLoading] = useState(enabled);
  const [refreshing, setRefreshing] = useState(false);
  const [hasLoaded, setHasLoaded] = useState(false);
  const [error, setError] = useState<ViewResourceError | null>(null);
  const hasLoadedRef = useRef(hasLoaded);
  const loadRef = useRef(load);
  const requestSequence = useRef(0);

  useEffect(() => {
    hasLoadedRef.current = hasLoaded;
  }, [hasLoaded]);

  useEffect(() => {
    loadRef.current = load;
  }, [load]);

  const reload = useCallback(async () => {
    if (!enabled) return;

    const requestId = ++requestSequence.current;
    const alreadyLoaded = hasLoadedRef.current;
    setLoading(!alreadyLoaded);
    setRefreshing(alreadyLoaded);
    try {
      const next = await loadRef.current();
      if (requestId !== requestSequence.current) return;
      setData(next);
      setError(null);
      setHasLoaded(true);
    } catch (err: any) {
      if (requestId !== requestSequence.current) return;
      setError(normalizeError(err, errorMessage));
      setHasLoaded(true);
    } finally {
      if (requestId !== requestSequence.current) return;
      setLoading(false);
      setRefreshing(false);
    }
  }, [enabled, errorMessage]);

  useEffect(() => {
    if (!enabled) {
      requestSequence.current += 1;
      setLoading(false);
      return;
    }

    setLoading(!hasLoaded);
    void reload();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [enabled, reload, ...deps]);

  return useMemo(() => ({ data, loading, refreshing, error, hasLoaded, reload, setData }), [data, error, hasLoaded, loading, refreshing, reload]);
}
