import { useRegisterActions } from 'kbar';
import { last } from 'lodash';
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { config } from '@grafana/runtime';

import { CommandPaletteAction } from '../types';

import { ScopesRow } from './ScopesRow';
import { useRecentScopesActions } from './recentScopesActions';
import {
  getScopesParentAction,
  mapScopeNodeToAction,
  mapScopesNodesTreeToActions,
  useScopeServicesState,
} from './scopesUtils';

export function useRegisterRecentScopesActions() {
  const recentScopesActions = useRecentScopesActions();
  useRegisterActions(recentScopesActions, [recentScopesActions]);
}

/**
 * Special actions for scopes. Scopes are already hierarchical and loaded dynamically, so we create actions based on
 * them as we load them. This also returns an additional component to be shown with selected actions and a button to
 * apply the selection.
 * @param searchQuery
 * @param onApply
 * @param parentId
 */
export function useRegisterScopesActions(
  searchQuery: string,
  onApply: () => void,
  parentId?: string | null
): { scopesRow?: ReactNode } {
  // Call hooks unconditionally to follow React Hooks rules
  const globalScopeActions = useGlobalScopesSearch(searchQuery, parentId);
  const scopeTreeActions = useScopeTreeActions(searchQuery, parentId);
  const { scopesRow } = useScopesRow(onApply);

  // If we have global search actions we use those. Inside the hook the search should be conditional based on where
  // in the command palette we are.
  const nodesActions = config.featureToggles.scopeFilters ? globalScopeActions || scopeTreeActions : [];

  // Register actions unconditionally (empty array if feature toggle is off)
  useRegisterActions(nodesActions, [nodesActions]);

  // Returns a component to show what scopes are selected or applied (undefined if feature toggle is off)
  return { scopesRow: config.featureToggles.scopeFilters ? scopesRow : undefined };
}

/**
 * Register actions based on the scopes tree structure. This handles the scope service updates and uses it as the
 * source of truth.
 * @param searchQuery
 * @param parentId
 */
function useScopeTreeActions(searchQuery: string, parentId?: string | null) {
  const { filterNode, selectScope, resetSelection, nodes, tree, selectedScopes } = useScopeServicesState();

  // Initialize the scopes the first time this runs and reset the scopes that were selected on unmount.
  useEffect(() => {
    filterNode('', '');
    resetSelection();
    return () => {
      resetSelection();
    };
  }, [filterNode, resetSelection]);

  // Load the next level of scopes when the parentId changes.
  useEffect(() => {
    const parentScopeId = !parentId || parentId === 'scopes' ? '' : last(parentId.split('/'))!;
    filterNode(parentScopeId, searchQuery);
  }, [filterNode, searchQuery, parentId]);

  return useMemo(
    () => mapScopesNodesTreeToActions(nodes, tree, selectedScopes, selectScope),
    [nodes, tree, selectedScopes, selectScope]
  );
}

/**
 * Returns an element to add to the command palette in case some scopes are selected, showing them and an apply
 * button.
 * @param onApply
 */
function useScopesRow(onApply: () => void) {
  const { nodes, scopes, selectedScopes, appliedScopes, deselectScope, apply } = useScopeServicesState();

  // Check if we have a different selection than what is already applied. Used to show the apply button.
  const isDirty =
    appliedScopes
      .map((t) => t.scopeId)
      .sort()
      .join('') !==
    selectedScopes
      .map((s) => s.scopeId)
      .sort()
      .join('');

  const finalApply = useCallback(() => {
    apply();
    onApply();
  }, [apply, onApply]);

  // Add a keyboard shortcut to apply the selection.
  useEffect(() => {
    function handler(event: KeyboardEvent) {
      if (isDirty && event.key === 'Enter' && event.metaKey) {
        event.preventDefault();
        finalApply();
      }
    }
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [isDirty, finalApply]);

  return {
    scopesRow:
      isDirty || selectedScopes.length ? (
        <ScopesRow
          nodes={nodes}
          scopes={scopes}
          deselectScope={deselectScope}
          selectedScopes={selectedScopes}
          apply={finalApply}
          isDirty={isDirty}
        />
      ) : null,
  };
}

/**
 * Register actions based on global search call. This returns actions that are separate from the scope service tree
 * and are just flat list without updating the scope service state.
 * @param searchQuery
 * @param parentId
 */
function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) {
  const { selectScope, searchAllNodes, getScopeNodes } = useScopeServicesState();
  const [actions, setActions] = useState<CommandPaletteAction[] | undefined>(undefined);
  const searchQueryRef = useRef<string>(undefined);

  useEffect(() => {
    if ((!parentId || parentId === 'scopes') && searchQuery && config.featureToggles.scopeSearchAllLevels) {
      // We only search globally if there is no parentId
      searchQueryRef.current = searchQuery;
      searchAllNodes(searchQuery, 10).then((nodes) => {
        if (searchQueryRef.current === searchQuery) {
          // Only show leaf nodes because otherwise there are issues with navigating to a category without knowing
          // where in the tree it is.

          const parentNodesMap = new Map<string | undefined, string>();

          if (config.featureToggles.useMultipleScopeNodesEndpoint) {
            // Make sure we only request unqiue parent node names
            const uniqueParentNodeNames = [
              ...new Set(nodes.map((node) => node.spec.parentName).filter((name) => name !== undefined)),
            ];
            getScopeNodes(uniqueParentNodeNames).then((parentNodes) => {
              for (const parentNode of parentNodes) {
                parentNodesMap.set(parentNode.metadata.name, parentNode.spec.title);
              }

              const leafNodes = nodes.filter((node) => node.spec.nodeType === 'leaf');
              const actions = [getScopesParentAction()];
              for (const node of leafNodes) {
                const parentName = parentNodesMap.get(node.spec.parentName);
                actions.push(mapScopeNodeToAction(node, selectScope, parentId || undefined, parentName || undefined));
              }
              setActions(actions);
            });
          } else {
            const leafNodes = nodes.filter((node) => node.spec.nodeType === 'leaf');
            const actions = [getScopesParentAction()];
            for (const node of leafNodes) {
              actions.push(mapScopeNodeToAction(node, selectScope, parentId || undefined));
            }
            setActions(actions);
          }
        }
      });
    } else {
      searchQueryRef.current = undefined;
      setActions(undefined);
    }
  }, [searchAllNodes, searchQuery, parentId, selectScope, getScopeNodes]);

  return actions;
}
