import { createSelector } from '@reduxjs/toolkit';
import { QueryStatus } from '@reduxjs/toolkit/query';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { t } from '@grafana/i18n';
import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1';
import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types';
import { useDispatch, useSelector } from 'app/types/store';

import { ManagerKind } from '../../../features/apiserver/types';
import { PAGE_SIZE } from '../../../features/browse-dashboards/api/services';
import { getPaginationPlaceholders } from '../../../features/browse-dashboards/state/utils';

import { UseFoldersQueryProps } from './useFoldersQuery';
import { getRootFolderItem } from './utils';

type GetFolderChildrenQuery = ReturnType<
  ReturnType<typeof dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select>
>;
type GetFolderChildrenRequest = {
  unsubscribe: () => void;
};

const rootFolderToken = 'general';
const sharedWithMeFolderToken = 'sharedwithme';
const collator = new Intl.Collator();

/**
 * Returns a loaded folder hierarchy as a flat list and a function to load folders.
 * This version uses the getFolderChildren API from the folder v1beta1 API. Compared to legacy API, the v1beta1 API
 * does not have pagination at the moment.
 */
export function useFoldersQueryAppPlatform({
  isBrowsing,
  openFolders,
  /* rootFolderUID: configure which folder to start browsing from */
  rootFolderUID,
  rootFolderItem,
  permission,
}: UseFoldersQueryProps) {
  const dispatch = useDispatch();

  // Keep a list of all request subscriptions so we can unsubscribe from them when the component is unmounted
  const requestsRef = useRef<GetFolderChildrenRequest[]>([]);

  // Set of UIDs for which children were requested but were empty.
  const [emptyFolders, setEmptyFolders] = useState<Set<string>>(new Set());
  function addEmptyFolder(folderUid: string) {
    setEmptyFolders((prev) => new Set(prev).add(folderUid));
  }

  // Keep a list of selectors for dynamic state selection
  const [selectors, setSelectors] = useState<
    Array<ReturnType<typeof dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select>>
  >([]);

  // This is an aggregated dynamic selector of all the selectors for all the request issued while loading the folder
  // tree and returns the whole tree that was loaded so far.
  const listAllFoldersSelector = useMemo(() => {
    return createSelector(selectors, (...responses) => {
      // Returns loading true if any of the responses is still loading
      let isLoading = false;

      const responseByParent: Record<string, GetFolderChildrenQuery> = {};

      for (const response of responses) {
        if (response.status === QueryStatus.pending) {
          isLoading = true;
        }

        const parentName = response.originalArgs?.folder;
        if (parentName) {
          responseByParent[parentName] = response;
        }
      }

      return {
        isLoading,
        responseByParent,
      };
    });
  }, [selectors]);

  const state = useSelector(listAllFoldersSelector);

  // Loads folders for the given parent UID
  const requestNextPage = useCallback(
    (parentUid: string | undefined) => {
      const finalParentUid = parentUid ?? rootFolderToken;
      const response = state.responseByParent[finalParentUid];
      const isLoading = response?.status === QueryStatus.pending;

      // If already loading, don't request again
      if (isLoading) {
        return;
      }

      const args = { folder: finalParentUid, type: 'folder', permission } as const;

      // Make a request
      const subscription = dispatch(dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.initiate(args));

      // Add selector for the response to the list so we can then have an aggregated selector for all the folders
      const selector = dashboardAPIv0alpha1.endpoints.searchDashboardsAndFolders.select(args);
      setSelectors((selectors) => selectors.concat(selector));

      // the subscriptions are saved in a ref so they can be unsubscribed on unmount
      requestsRef.current = requestsRef.current.concat([subscription]);
    },
    [state, dispatch, permission]
  );

  // Unsubscribe from all requests when the component is unmounted
  useEffect(() => {
    return () => {
      for (const req of requestsRef.current) {
        req.unsubscribe();
      }
    };
  }, []);

  // Convert the individual responses into a flat list of folders, with level indicating
  // the depth in the hierarchy.
  const treeList = useMemo(() => {
    if (!isBrowsing) {
      return [];
    }

    function createFlatList(
      parentUid: string | undefined,
      response: GetFolderChildrenQuery | undefined,
      level: number
    ): Array<DashboardsTreeItem<DashboardViewItemWithUIItems>> {
      let folders = response?.data?.hits ? [...response.data.hits] : [];
      folders.sort((a, b) => collator.compare(a.title, b.title));

      // Add virtual "Shared with me" folder under the top-level "Dashboards" root.
      // This is backed by the same search endpoint, using `folder=sharedwithme`.
      // Should show whatever folders are not accessible by traversing from the general/root folder.
      if (parentUid === rootFolderToken && rootFolderUID === undefined) {
        folders = [makeSharedWithMeFolder(), ...folders];
      }

      const list: DashboardsTreeItem[] = folders.flatMap((item) => {
        const name = item.name;
        const folderIsOpen = openFolders[name];
        const flatItem: DashboardsTreeItem = {
          isOpen: Boolean(folderIsOpen),
          level: level,
          disabled: item.name === sharedWithMeFolderToken,
          item: {
            kind: 'folder' as const,
            title: item.title,
            // We use resource name as UID because well, not sure what metadata.uid would be used for now as you cannot
            // query by it.
            uid: name,
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            managedBy: item.managedBy?.kind as ManagerKind | undefined,
            parentUID: item.folder,
          },
        };

        const childResponse = folderIsOpen && state.responseByParent[name];
        if (childResponse) {
          // If we finished loading and there are no children add folder to empty folders list so we don't show
          // the caret next to the folder anymore
          if (isEmptyResponse(childResponse)) {
            addEmptyFolder(name);
          }
          const childFlatItems = createFlatList(name, childResponse, level + 1);
          return [flatItem, ...childFlatItems];
        }

        return flatItem;
      });

      // We could return early but we are adding the "shared with me" folder statically, so even if response is empty
      // there could be a folder to process
      if (!response) {
        // The pagination placeholders are what actually triggers the call to the next page. So if there is no response,
        // meaning to request for some children, we add these placeholders, and they will trigger the load.
        list.push(...getPaginationPlaceholders(PAGE_SIZE, parentUid, level));
      }
      return list;
    }

    const startingToken = rootFolderUID ?? rootFolderToken;
    const rootFlatTree = createFlatList(startingToken, state.responseByParent[startingToken], 1);
    rootFlatTree.unshift(rootFolderItem || getRootFolderItem());

    return rootFlatTree;
  }, [state, isBrowsing, openFolders, rootFolderUID, rootFolderItem]);

  return {
    emptyFolders,
    items: treeList,
    isLoading: state.isLoading,
    requestNextPage,
  };
}

function makeSharedWithMeFolder() {
  return {
    name: sharedWithMeFolderToken,
    title: t('browse-dashboards.folder-picker.shared-with-me', 'Shared with me'),
    folder: rootFolderToken,
    resource: 'folder',
  };
}

function isEmptyResponse(response: { data?: { hits: unknown[] }; status: QueryStatus }) {
  return response.data && response.status !== QueryStatus.pending && response.data.hits.length === 0;
}
