import { useCallback, useEffect, useReducer } from "react";

type FetchTransition<P> =
  | {
      type: "fetch";
    }
  | {
      type: "fetchOK";
      data: P;
    }
  | {
      type: "fetchError";
      error: Error;
    };

type FetchState<P> =
  | DryState
  | DryErrorState
  | FetchingState
  | RefetchingState<P>
  | FreshState<P>
  | StaleState<P>;

interface DryState {
  data: null;
  error: null;
  isLoading: false;
  type: "dry";
}

interface DryErrorState {
  data: null;
  error: Error;
  isLoading: false;
  type: "dryError";
}

interface FetchingState {
  data: null;
  error: null;
  isLoading: true;
  type: "fetching";
}

interface RefetchingState<P> {
  data: P;
  error: null;
  isLoading: true;
  type: "refetching";
}

interface FreshState<P> {
  data: P;
  error: null;
  isLoading: false;
  type: "fresh";
}

interface StaleState<P> {
  data: P;
  error: Error;
  isLoading: false;
  type: "stale";
}

export const stateFactory = {
  dry(): DryState {
    return {
      data: null,
      error: null,
      isLoading: false,
      type: "dry",
    };
  },
  dryError(error: Error): DryErrorState {
    return {
      data: null,
      error,
      isLoading: false,
      type: "dryError",
    };
  },
  fetching(): FetchingState {
    return {
      data: null,
      error: null,
      isLoading: true,
      type: "fetching",
    };
  },
  fresh<P>(data: P): FreshState<P> {
    return {
      data,
      error: null,
      isLoading: false,
      type: "fresh",
    };
  },
  refetching<P>(cachedData: P): RefetchingState<P> {
    return {
      data: cachedData,
      error: null,
      isLoading: true,
      type: "refetching",
    };
  },
  stale<P>(error: Error, cachedData: P): StaleState<P> {
    return {
      data: cachedData,
      error,
      isLoading: false,
      type: "stale",
    };
  },
};

export function fetchReducer<P>(
  state: FetchState<P>,
  transition: FetchTransition<P>
): FetchState<P> {
  if (state.type === "dry" && transition.type === "fetch") {
    return stateFactory.fetching();
  }
  if (state.type === "dryError" && transition.type === "fetch") {
    return stateFactory.fetching();
  }
  if (state.type === "fetching" && transition.type === "fetchOK") {
    return stateFactory.fresh(transition.data);
  }
  if (state.type === "fetching" && transition.type === "fetchError") {
    return stateFactory.dryError(transition.error);
  }
  if (state.type === "fresh" && transition.type === "fetch") {
    return stateFactory.refetching(state.data);
  }
  if (state.type === "refetching" && transition.type === "fetchOK") {
    return stateFactory.fresh(transition.data);
  }
  if (state.type === "refetching" && transition.type === "fetchError") {
    return stateFactory.stale(transition.error, state.data);
  }
  if (state.type === "stale" && transition.type === "fetch") {
    return stateFactory.refetching(state.data);
  }
  console.warn(
    `Unauthorized transition ${transition.type} while in state ${state.type}`
  );
  return state;
}

export default function useFetch<P>({
  url,
  autoFetch,
}: {
  url: string;
  autoFetch: boolean;
}) {
  const [state, dispatch] = useReducer(fetchReducer, void 0, stateFactory.dry);

  const launchFetch = useCallback(() => {
    if (!url) return;
    dispatch({ type: "fetch" });
  }, [url]);

  useEffect(
    function runFetch() {
      let canceled = false;
      if (state.isLoading) {
        (async () => {
          try {
            const response = await fetch(url);
            if (canceled) return;
            if (!response.ok) {
              throw new Error(response.statusText);
            }
            const data = await response.json();
            if (canceled) return;
            dispatch({ type: "fetchOK", data });
          } catch (error) {
            if (canceled) return;
            dispatch({ type: "fetchError", error });
          }
        })();
      }
      return () => {
        canceled = true;
      };
    },
    [url, state.isLoading]
  );

  useEffect(() => {
    if (autoFetch) {
      launchFetch();
    }
  }, [autoFetch, launchFetch]);

  return { state: state as FetchState<P>, fetch };
}