import { useRef } from "react";
import {
  ExtractErrorType,
  ExtractResponseType,
  RequestInstance,
  ProgressType,
  ResponseReturnType,
  RequestEventType,
  ResponseDetailsType,
  RequestLoadingEventType,
  ExtractAdapterType,
} from "@hyper-fetch/core";
import { useWillUnmount } from "@better-hooks/lifecycle";

import {
  OnErrorCallbackType,
  OnStartCallbackType,
  OnSuccessCallbackType,
  OnProgressCallbackType,
  OnFinishedCallbackType,
  UseRequestEventsDataMap,
  UseRequestEventsReturnType,
  UseRequestEventsPropsType,
  UseRequestEventsLifecycleMap,
} from "helpers";

/**
 * This is helper hook that handles main Hyper-Fetch event/data flow
 * @internal
 * @param options
 * @returns
 */
export const useRequestEvents = <T extends RequestInstance>({
  request,
  dispatcher,
  logger,
  actions,
  setCacheData,
  getIsDataProcessing,
}: UseRequestEventsPropsType<T>): UseRequestEventsReturnType<T> => {
  const { responseMapper } = request;
  const { cache, requestManager } = request.client;

  // ******************
  // Callbacks
  // ******************

  const onSuccessCallback = useRef<null | OnSuccessCallbackType<T>>(null);
  const onErrorCallback = useRef<null | OnErrorCallbackType<T>>(null);
  const onAbortCallback = useRef<null | OnErrorCallbackType<T>>(null);
  const onOfflineErrorCallback = useRef<null | OnErrorCallbackType<T>>(null);
  const onFinishedCallback = useRef<null | OnFinishedCallbackType<T>>(null);
  const onRequestStartCallback = useRef<null | OnStartCallbackType<T>>(null);
  const onResponseStartCallback = useRef<null | OnStartCallbackType<T>>(null);
  const onDownloadProgressCallback = useRef<null | OnProgressCallbackType>(null);
  const onUploadProgressCallback = useRef<null | OnProgressCallbackType>(null);

  // ******************
  // Listeners unmounting
  // ******************

  const lifecycleEvents = useRef<UseRequestEventsLifecycleMap>(new Map());
  const dataEvents = useRef<UseRequestEventsDataMap | null>(null);

  const removeLifecycleListener = (requestId: string) => {
    const event = lifecycleEvents.current.get(requestId);
    event?.unmount();
    lifecycleEvents.current.delete(requestId);
  };

  const clearLifecycleListeners = () => {
    const events = lifecycleEvents.current;
    const listeners = Array.from(events.values());
    listeners.forEach((value) => {
      value.unmount();
    });
    events.clear();
  };

  // ******************
  // Response handlers
  // ******************

  const handleResponseCallbacks = (
    cmd: T,
    response: ResponseReturnType<ExtractResponseType<T>, ExtractErrorType<T>, ExtractAdapterType<T>>,
    details: ResponseDetailsType,
  ) => {
    const { data, error, success } = response;
    const { isOffline, isCanceled } = details;
    if (request.offline && isOffline && !success) {
      logger.debug("Performing offline error callback", { data, details });
      onOfflineErrorCallback.current?.({ response: error, request: cmd, details });
    } else if (isCanceled) {
      logger.debug("Performing abort callback", { data, details });
      onAbortCallback.current?.({ response: error, request: cmd, details });
    } else if (success) {
      logger.debug("Performing success callback", { data, details });
      onSuccessCallback.current?.({ response: data, request: cmd, details });
    } else {
      logger.debug("Performing error callback", { data, details });
      onErrorCallback.current?.({ response: error, request: cmd, details });
    }
    onFinishedCallback.current?.({ response, request: cmd, details });
  };

  // ******************
  // Lifecycle
  // ******************

  const handleGetLoadingEvent = (req: T) => {
    return ({ loading }: RequestLoadingEventType) => {
      const isProcessing = getIsDataProcessing(req.cacheKey);

      // When we process the cache data, we don't want to change the loading state during it
      // This prevents the UI from flickering with { data: null, loading: false }
      if (isProcessing) return;

      const canDisableLoading = !loading && !dispatcher.hasRunningRequests(req.queueKey);
      if (loading || canDisableLoading) {
        actions.setLoading(loading, false);
      }
    };
  };

  const handleDownloadProgress = (progress: ProgressType, details: RequestEventType<T>) => {
    onDownloadProgressCallback.current?.(progress, details);
  };

  const handleUploadProgress = (progress: ProgressType, details: RequestEventType<T>) => {
    onUploadProgressCallback.current?.(progress, details);
  };

  const handleRequestStart = (cmd: T) => {
    return (details: RequestEventType<T>) => {
      onRequestStartCallback.current?.({ request: cmd, details });
    };
  };
  const handleResponseStart = (cmd: T) => {
    return (details: RequestEventType<T>) => {
      onResponseStartCallback.current?.({ request: cmd, details });
    };
  };

  const handleResponse = (req: T) => {
    return (
      response: ResponseReturnType<ExtractResponseType<T>, ExtractErrorType<T>, ExtractAdapterType<T>>,
      details: ResponseDetailsType,
    ) => {
      const data = responseMapper ? responseMapper(response) : response;

      if (data instanceof Promise) {
        return (async () => {
          const promiseData = await data;
          handleResponseCallbacks(req, promiseData, details);
        })();
      }
      return handleResponseCallbacks(req, data, details);
    };
  };

  const handleRemove = ({ requestId }: RequestEventType<T>) => {
    removeLifecycleListener(requestId);
  };

  // ******************
  // Data Listeners
  // ******************

  const clearCacheDataListener = () => {
    dataEvents.current?.unmount();
    dataEvents.current = null;
  };

  const addCacheDataListener = (req: T) => {
    // Data handlers
    const loadingUnmount = requestManager.events.onLoading(req.queueKey, handleGetLoadingEvent(req));
    const getResponseUnmount = cache.events.onData<ExtractResponseType<T>, ExtractErrorType<T>, ExtractAdapterType<T>>(
      req.cacheKey,
      setCacheData,
    );

    const unmount = () => {
      loadingUnmount();
      getResponseUnmount();
    };

    clearCacheDataListener();
    dataEvents.current = { unmount };

    return unmount;
  };

  // ******************
  // Lifecycle Listeners
  // ******************

  const addLifecycleListeners = (req: T, requestId?: string) => {
    /**
     * useFetch handles requesting by general keys
     * This makes it possible to deduplicate requests from different places and share data
     */
    if (!requestId) {
      // It's important to clear previously attached listeners to not cause some additional response/request
      // events to be triggered during lifecycle
      clearLifecycleListeners();
      const { queueKey, cacheKey } = req;
      const requestStartUnmount = requestManager.events.onRequestStart(queueKey, handleRequestStart(req));
      const responseStartUnmount = requestManager.events.onResponseStart(queueKey, handleResponseStart(req));
      const uploadUnmount = requestManager.events.onUploadProgress(queueKey, handleUploadProgress);
      const downloadUnmount = requestManager.events.onDownloadProgress(queueKey, handleDownloadProgress);
      const responseUnmount = requestManager.events.onResponse(cacheKey, handleResponse(req));

      const unmount = () => {
        downloadUnmount();
        uploadUnmount();
        requestStartUnmount();
        responseStartUnmount();
        responseUnmount();
      };

      lifecycleEvents.current.set(queueKey, { unmount });

      return unmount;
    }
    /**
     * useSubmit handles requesting by requestIds, this makes it possible to track single requests
     */
    const requestRemove = requestManager.events.onRemoveById(requestId, handleRemove);
    const requestStartUnmount = requestManager.events.onRequestStartById(requestId, handleRequestStart(req));
    const responseStartUnmount = requestManager.events.onResponseStartById(requestId, handleResponseStart(req));
    const responseUnmount = requestManager.events.onResponseById(requestId, handleResponse(req));
    const uploadUnmount = requestManager.events.onUploadProgressById(requestId, handleUploadProgress);
    const downloadUnmount = requestManager.events.onDownloadProgressById(requestId, handleDownloadProgress);

    const unmount = () => {
      requestRemove();
      downloadUnmount();
      uploadUnmount();
      requestStartUnmount();
      responseStartUnmount();
      responseUnmount();
    };

    lifecycleEvents.current.set(requestId, { unmount });

    return unmount;
  };

  // ******************
  // Abort
  // ******************

  const abort = () => {
    const { abortKey } = request;
    const requests = dispatcher.getAllRunningRequest();
    requests.forEach((requestData) => {
      if (requestData.request.abortKey === abortKey) {
        dispatcher.delete(requestData.request.queueKey, requestData.requestId, abortKey);
      }
    });
  };

  // ******************
  // Lifecycle
  // ******************

  /**
   * On unmount we want to clear all the listeners to prevent memory leaks
   */
  useWillUnmount(() => {
    // Unmount listeners
    clearLifecycleListeners();
    clearCacheDataListener();
  });

  return [
    {
      abort,
      onSuccess: (callback: OnSuccessCallbackType<T>) => {
        onSuccessCallback.current = callback;
      },
      onError: (callback: OnErrorCallbackType<T>) => {
        onErrorCallback.current = callback;
      },
      onAbort: (callback: OnErrorCallbackType<T>) => {
        onAbortCallback.current = callback;
      },
      onOfflineError: (callback: OnErrorCallbackType<T>) => {
        onOfflineErrorCallback.current = callback;
      },
      onFinished: (callback: OnFinishedCallbackType<T>) => {
        onFinishedCallback.current = callback;
      },
      onRequestStart: (callback: OnStartCallbackType<T>) => {
        onRequestStartCallback.current = callback;
      },
      onResponseStart: (callback: OnStartCallbackType<T>) => {
        onResponseStartCallback.current = callback;
      },
      onDownloadProgress: (callback: OnProgressCallbackType) => {
        onDownloadProgressCallback.current = callback;
      },
      onUploadProgress: (callback: OnProgressCallbackType) => {
        onUploadProgressCallback.current = callback;
      },
    },
    {
      addCacheDataListener,
      clearCacheDataListener,
      addLifecycleListeners,
      removeLifecycleListener,
      clearLifecycleListeners,
    },
  ];
};
