import { runInAction } from "mobx";

type mobxFetchOptions<RequestType, ResponseType, Store> = {
  store: Store;
  value?: keyof Store;
  values?: Array<keyof Store>;
  loading?: keyof Store;
  error?: keyof Store;
  fn: RequestType extends void
    ? (signal?: AbortSignal) => Promise<ResponseType>
    : (request: RequestType, signal?: AbortSignal) => Promise<ResponseType>;

  pollingInterval?: number;
  resetValue?: boolean;
  transform?: (response: ResponseType) => Partial<Record<string, any>>;
  onSuccess?: (response: ResponseType) => void;
};

type FetchFunction<RequestType, ResponseType> = RequestType extends void
  ? {
      (): Promise<ResponseType | null>;
      stopPolling?: () => void;
    }
  : {
      (request: RequestType): Promise<ResponseType | null>;
      stopPolling?: () => void;
    };

export function mobxFetch<ResponseType, Store extends Record<string, any>>(
  options: mobxFetchOptions<void, ResponseType, Store>
): FetchFunction<void, ResponseType>;

export function mobxFetch<
  RequestType,
  ResponseType,
  Store extends Record<string, any>,
>(
  options: mobxFetchOptions<RequestType, ResponseType, Store>
): FetchFunction<RequestType, ResponseType>;

export function mobxFetch<
  RequestType,
  ResponseType,
  Store extends Record<string, any>,
>(
  options: mobxFetchOptions<RequestType, ResponseType, Store>
): FetchFunction<RequestType, ResponseType> {
  const {
    store,
    value,
    values,
    loading,
    error,
    fn,
    pollingInterval,
    resetValue,
    transform,
    onSuccess,
  } = options;

  let abortController: AbortController | undefined;
  let pollingTimer: ReturnType<typeof setInterval> | undefined;
  let currentRequest: RequestType | undefined;

  const stopPolling = () => {
    if (pollingTimer) {
      clearInterval(pollingTimer);
      pollingTimer = undefined;
    }
    abortController?.abort();
  };

  const fetch = async (request?: RequestType): Promise<ResponseType | null> => {
    abortController?.abort();
    abortController = new AbortController();
    currentRequest = request as RequestType;

    runInAction(() => {
      if (value) {
        (store[value] as any) = resetValue ? null : store[value];
      }

      if (values) {
        values.forEach((key) => {
          (store[key] as any) = resetValue ? null : store[key];
        });
      }

      if (error) {
        (store[error] as any) = null;
      }

      if (loading) {
        (store[loading] as any) = true;
      }
    });

    try {
      const result = await (
        fn as (
          request?: RequestType,
          signal?: AbortSignal
        ) => Promise<ResponseType>
      )(request, abortController.signal);

      runInAction(() => {
        if (values && transform) {
          const transformed = transform(result) as Record<string, any>;
          values.forEach((key) => {
            const k = key as string;
            if (k in transformed) {
              (store[key] as any) = transformed[k];
            }
          });
        } else if (value) {
          (store[value] as any) = result as ResponseType;
        }

        if (loading) {
          (store[loading] as any) = false;
        }

        if (error) {
          (store[error] as any) = null;
        }
      });

      if (pollingInterval && !pollingTimer) {
        pollingTimer = setInterval(() => {
          if (currentRequest !== undefined) {
            fetch(currentRequest);
          } else {
            fetch();
          }
        }, pollingInterval);
      }

      if (onSuccess) {
        onSuccess(result);
      }

      return result;
    } catch (err) {
      if (!(err instanceof Error && err.name === "CanceledError")) {
        runInAction(() => {
          if (error) {
            (store[error] as any) =
              err instanceof Error ? err.message : String(err);
          }

          if (loading) {
            (store[loading] as any) = false;
          }

          if (value) {
            (store[value] as any) = null;
          }

          if (values) {
            values.forEach((key) => {
              (store[key] as any) = null;
            });
          }
        });

        throw err;
      }

      return null;
    }
  };

  const fetchWithStopPolling = fetch as FetchFunction<
    RequestType,
    ResponseType
  >;

  if (pollingInterval) {
    fetchWithStopPolling.stopPolling = stopPolling;
  }

  return fetchWithStopPolling;
}
