import { cloneDeep } from 'lodash';
import {
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useUpdatingRef } from '../../../hooks/useUpdatingRef/useUpdatingRef';
import { Data } from '../../../types/data';
import { ErrorTypeToStationError } from '../../../utils/ErrorTypeToStationError';
import { FilterValues } from '../../Filters/Filters.model';
import { SortData } from '../../List/List.model';
import { ErrorType } from '../../models';
import {
  ExplorerDataProvider,
  ExplorerDataProviderConnection,
} from '../Explorer.model';
import { StationMessage } from './useStationMessage';

interface DataProviderReturnType<T> {
  readonly data: T[];
  readonly isLoading: boolean;
  readonly resultCount: ResultCounts;
  readonly hasMoreData: boolean;
  readonly onReloadData: () => void;
  readonly onSortChanged: (sort: SortData<T>) => void;
  readonly onRequestMoreData: () => void;
}

export interface ResultCounts {
  total: number;
  filtered?: number;
}

interface DataProviderArgumentType<T extends Data> {
  dataProvider: ExplorerDataProvider<T>;
  explorerRef: React.ForwardedRef<ExplorerDataProviderConnection<T>>;
  defaultSortOrder?: SortData<T>;
  activeFilters?: FilterValues<T>;
  keyProperty?: keyof T;
  setStationMessage: React.Dispatch<
    React.SetStateAction<StationMessage | undefined>
  >;
}

export function useDataProvider<T extends Data>({
  dataProvider,
  explorerRef,
  setStationMessage,
  defaultSortOrder,
  activeFilters,
  keyProperty,
}: DataProviderArgumentType<T>): DataProviderReturnType<T> {
  const dataProviderRef = useUpdatingRef(dataProvider);

  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [data, setData] = useState<T[]>([]);
  const [resultCount, setResultCount] = useState<ResultCounts>({
    total: 0,
    filtered: 0,
  });
  const hasMoreData = useRef<boolean>(true);
  const pagingInfo = useRef<unknown>();
  const lastAttemptedPagingInfo = useRef<unknown>();

  const sorting = useRef<SortData<T> | undefined>(defaultSortOrder);
  const filterValues = useRef<FilterValues<T>>(activeFilters || {});

  const loadData = useCallback(
    async (
      isFirstPage = true,
      pagingInformation: unknown = undefined,
    ): Promise<void> => {
      if (isFirstPage === true) {
        // Reset pagingInfo.
        pagingInfo.current = undefined;
        hasMoreData.current = true;
        // Clean out data, because we're anyway loading the data new from scratch.
        // This way the error message will not appear way down on the list but in sight.
        setData(() => []);
      }

      // If there is no more data, exit.
      if (hasMoreData.current === false) {
        return;
      }

      setIsLoading(true);
      setStationMessage(undefined);

      // Remembering the attempted paging information to use on retry.
      lastAttemptedPagingInfo.current = pagingInformation;
      try {
        const result = await dataProviderRef.current.loadData({
          pagingInformation: cloneDeep(pagingInfo.current),
          sorting: cloneDeep(sorting.current),
          filters: cloneDeep(filterValues.current),
        });

        setResultCount({
          total: result.totalCount,
          filtered: result.filteredCount ?? 0,
        });

        // Set new paging info.
        if (!isFirstPage) {
          setData((data) => [...data, ...result.data]);
        } else {
          setData(() => [...result.data]);
        }
        pagingInfo.current = result.pagingInformation;
        hasMoreData.current = result.hasMoreData;
      } catch (error) {
        setStationMessage({
          ...ErrorTypeToStationError(
            error as ErrorType,
            'An error occurred when trying to load data.',
          ),
          canClose: true,
          type: 'error',
          onRetry: () => loadData(false, lastAttemptedPagingInfo.current),
        });
      } finally {
        setIsLoading(false);
      }
    },
    [dataProviderRef, setStationMessage],
  );

  useEffect(() => {
    if (activeFilters !== filterValues.current) {
      // Re-enable more data requests
      hasMoreData.current = true;
      // Set new filters.
      filterValues.current = activeFilters ?? {};
      loadData();
    }
  }, [activeFilters, loadData]);

  const onSortChanged = useCallback(
    (sort: SortData<T>): void => {
      // Re-enable more data requests
      hasMoreData.current = true;
      // Set new sorting order.
      sorting.current = sort;
      loadData();
    },
    [loadData],
  );

  const onRequestMoreData = useCallback((): void => {
    loadData(false, pagingInfo.current);
  }, [loadData]);

  const onReloadData = useCallback((): void => {
    loadData();
  }, [loadData]);

  useEffect(() => {
    (async () => {
      await loadData();
    })();
  }, [loadData]);

  useConnection({
    dataProvider,
    data,
    setData,
    keyProperty,
    explorerRef,
    setStationMessage,
    onReloadData,
    setResultCount,
  });

  return {
    isLoading,
    resultCount,
    onReloadData,
    onSortChanged,
    onRequestMoreData,
    data,
    hasMoreData: hasMoreData.current,
  } as const;
}

interface ConnectionArgumentType<T extends Data> {
  setData: React.Dispatch<React.SetStateAction<T[]>>;
  keyProperty?: keyof T;
  dataProvider: ExplorerDataProvider<T>;
  data: T[];
  explorerRef: React.ForwardedRef<ExplorerDataProviderConnection<T>>;
  setStationMessage: React.Dispatch<
    React.SetStateAction<StationMessage | undefined>
  >;
  setResultCount: React.Dispatch<React.SetStateAction<ResultCounts>>;
  onReloadData: () => void;
}

const useConnection = <T extends Data>({
  dataProvider,
  explorerRef,
  setData,
  keyProperty,
  setStationMessage,
  setResultCount,
  onReloadData,
  data,
}: ConnectionArgumentType<T>): ExplorerDataProviderConnection<T> => {
  const dataRef = useUpdatingRef(data);
  const dataProviderRef = useUpdatingRef(dataProvider);

  const connection = useMemo<ExplorerDataProviderConnection<T>>(
    () => ({
      change: (id, patch) => {
        setData((data) =>
          data.map((d) =>
            d[keyProperty ?? 'id'] === id ? { ...d, ...patch } : d,
          ),
        );
      },
      add: (_data) => {
        setStationMessage({
          canClose: false,
          title:
            'The current data is not up to date. Please refresh the view to get the latest state.',
          type: 'warning',
          onRetry: onReloadData,
        });
      },
      remove: (id) => {
        if (dataRef.current.find((i) => i[keyProperty ?? 'id'] === id)) {
          // if we have loaded that item already, we will inform the user about the outdated values
          setStationMessage({
            canClose: false,
            title:
              'The current data is not up to date. Please refresh the view to get the latest state.',
            type: 'warning',
            onRetry: onReloadData,
          });
        } else {
          // if the item is not yet loaded, we can ignore the event
          // we will reduce the total amount by one though
          setResultCount((c) => ({
            ...c,
            total: c.total - 1,
          }));
        }
      },
    }),
    [
      dataRef,
      keyProperty,
      onReloadData,
      setData,
      setStationMessage,
      setResultCount,
    ],
  );

  useImperativeHandle(explorerRef, () => connection, [connection]);

  useEffect(() => {
    const connectionDisposer = dataProviderRef.current.connect?.(connection);

    return () => {
      if (connectionDisposer) {
        connectionDisposer();
      }
    };
  }, [connection, dataProviderRef]);

  return connection;
};
