import { useTranslation } from 'react-i18next';
import {
  SearchHeadless,
  QuerySource,
  SearchTypeEnum,
  UniversalLimit,
  useSearchActions,
  useSearchState,
  useSearchUtilities,
  VerticalResults as VerticalResultsData
} from '@yext/search-headless-react';
import classNames from 'classnames';
import React, { Fragment, isValidElement, PropsWithChildren, ReactNode, useCallback, useEffect, useMemo } from 'react';
import { useEntityPreviews } from '../hooks/useEntityPreviews';
import { useRecentSearches } from '../hooks/useRecentSearches';
import { useSearchWithNearMeHandling } from '../hooks/useSearchWithNearMeHandling';
import { useSynchronizedRequest } from '../hooks/useSynchronizedRequest';
import { useDebouncedFunction } from '../hooks/useDebouncedFunction';
import { VerticalDividerIcon } from '../icons/VerticalDividerIcon';
import { HistoryIcon as RecentSearchIcon } from '../icons/HistoryIcon';
import { CloseIcon } from '../icons/CloseIcon';
import { MagnifyingGlassIcon } from '../icons/MagnifyingGlassIcon';
import { Dropdown } from './Dropdown/Dropdown';
import { useDropdownContext } from './Dropdown/DropdownContext';
import { DropdownInput } from './Dropdown/DropdownInput';
import { DropdownItem } from './Dropdown/DropdownItem';
import { DropdownMenu } from './Dropdown/DropdownMenu';
import { FocusedItemData } from './Dropdown/FocusContext';
import { useComposedCssClasses, twMerge } from '../hooks/useComposedCssClasses';
import { SearchButton } from './SearchButton';
import {
  renderAutocompleteResult,
  AutocompleteResultCssClasses,
  builtInCssClasses as AutocompleteResultBuiltInCssClasses
} from './utils/renderAutocompleteResult';
import { useSearchBarAnalytics } from '../hooks/useSearchBarAnalytics';
import { isVerticalLink, VerticalLink } from '../models/verticalLink';
import { executeAutocomplete as executeAutocompleteSearch } from '../utils/search-operations';
import { clearStaticRangeFilters } from '../utils/filterutils';
import { recursivelyMapChildren } from './utils/recursivelyMapChildren';

const builtInCssClasses: Readonly<SearchBarCssClasses> = {
  searchBarContainer: 'h-12 mb-6',
  inputDivider: 'border-t border-gray-200 mx-2.5',
  inputElement: 'outline-none flex-grow border-none h-11 pl-5 pr-2 text-neutral-dark text-base placeholder:text-neutral-light',
  searchButtonContainer: ' w-8 h-full mx-2 flex flex-col justify-center items-center',
  searchButton: 'h-7 w-7',
  focusedOption: 'bg-gray-100',
  clearButton: 'h-3 w-3 mr-3.5',
  verticalDivider: 'mr-0.5',
  recentSearchesIcon: 'w-5 mr-1 flex-shrink-0 h-full text-gray-400',
  recentSearchesOption: 'whitespace-no-wrap max-w-full px-3 text-neutral-dark truncate',
  recentSearchesNonHighlighted: 'font-normal', // Swap this to semibold once we apply highlighting to recent searches
  verticalLink: 'ml-12 pl-1 text-neutral italic',
  entityPreviewsDivider: 'h-px bg-gray-200 mt-1 mb-4 mx-3.5',
  ...AutocompleteResultBuiltInCssClasses
};

/**
 * The CSS class interface for the {@link SearchBar}.
 *
 * @public
 */
export interface SearchBarCssClasses extends AutocompleteResultCssClasses {
  searchBarContainer?: string,
  inputElement?: string,
  inputDivider?: string,
  clearButton?: string,
  searchButton?: string,
  searchButtonContainer?: string,
  focusedOption?: string,
  recentSearchesIcon?: string,
  recentSearchesOption?: string,
  recentSearchesNonHighlighted?: string,
  verticalLink?: string,
  verticalDivider?: string,
  entityPreviewsDivider?: string
}

/**
 * The type of a functional React component which renders entity previews using
 * a map of vertical key to the corresponding VerticalResults data.
 *
 * @remarks
 * The autocomplete loading state is passed in as an optional param.
 *
 * Default props for rendering corresponding DropdownItems are passed in:
 * an onClick function to allow an entity preview to be submitted, and
 * an ariaLabel function that returns text for the screenreader
 *
 * For the entity previews to be navigable in the search bar's dropdown section,
 * wrap each entity preview in a {@link DropdownItem} component.
 *
 * @public
 */
export type RenderEntityPreviews = (
  autocompleteLoading: boolean,
  verticalKeyToResults: Record<string, VerticalResultsData>,
  dropdownItemProps: {
    onClick: (value: string, _index: number, itemData?: FocusedItemData) => void,
    ariaLabel: (value: string) => string
  }
) => React.JSX.Element | null;

/**
 * The configuration options for Visual Autocomplete.
 *
 * @public
 */
export interface VisualAutocompleteConfig {
  /** The Search Headless instance used to perform visual autocomplete searches. */
  entityPreviewSearcher: SearchHeadless,
  /** Renders entity previews based on the autocomplete loading state and results. */
  renderEntityPreviews: RenderEntityPreviews,
  /** Specify which verticals to include for VisualAutocomplete. */
  includedVerticals: string[],
  /** Specify the number of entities to return per vertical. **/
  universalLimit?: UniversalLimit,
  /** The debouncing time, in milliseconds, for making API requests for entity previews. */
  entityPreviewsDebouncingTime?: number
}

/**
 * The interface of a function which is called on a search.
 *
 * @public
 */
export type onSearchFunc = (searchEventData: { verticalKey?: string, query?: string }) => void;

/**
 * The props for the {@link SearchBar} component.
 *
 * @public
 */
export interface SearchBarProps {
  /** The search bar's placeholder text. */
  placeholder?: string,
  /** {@inheritDoc LocationBiasProps.geolocationOptions} */
  geolocationOptions?: PositionOptions,
  /** CSS classes for customizing the component styling. */
  customCssClasses?: SearchBarCssClasses,
  /** {@inheritDoc VisualAutocompleteConfig} */
  visualAutocompleteConfig?: VisualAutocompleteConfig,
  /** Shows vertical links if true, set to false on default. */
  showVerticalLinks?: boolean,
  /** A function which is called when a vertical link is selected. */
  onSelectVerticalLink?: (data: { verticalLink: VerticalLink, querySource: QuerySource }) => void,
  /** A function which returns a display label for the given verticalKey. */
  verticalKeyToLabel?: (verticalKey: string) => string,
  /** Hides recent searches if true. */
  hideRecentSearches?: boolean,
  /** Limits the number of recent searches shown. */
  recentSearchesLimit?: number,
  /** Limits the number of universal query suggestions returned by autocomplete. */
  universalAutocompleteLimit?: number,
  /**
   * Limits the number of query suggestions returned by autocomplete for verticals.
   * The keys of the record correspond to the vertical keys, and the values correspond
   * to the maximum number of suggestions to return for that vertical.
   * If a limit for the current vertical is not specified, the default limit will be used.
   */
  verticalAutocompleteLimits?: Record<string, number>,
  /** A callback which is called when a search is ran. */
  onSearch?: onSearchFunc,
  /** Disable autocomplete if true, set to false on default. */
  autocompleteDisabled?: boolean
}

/**
 * Renders a SearchBar that is hooked up with an InputDropdown component.
 *
 * @public
 */
export function SearchBar({
  placeholder,
  geolocationOptions,
  hideRecentSearches,
  visualAutocompleteConfig,
  showVerticalLinks = false,
  onSelectVerticalLink,
  verticalKeyToLabel,
  recentSearchesLimit = 5,
  universalAutocompleteLimit,
  verticalAutocompleteLimits,
  customCssClasses,
  onSearch,
  autocompleteDisabled = false
}: SearchBarProps): React.JSX.Element {
  const { t } = useTranslation();
  const {
    entityPreviewSearcher,
    renderEntityPreviews,
    includedVerticals,
    universalLimit,
    entityPreviewsDebouncingTime = 500
  } = visualAutocompleteConfig ?? {};
  const searchActions = useSearchActions();
  const searchUtilities = useSearchUtilities();
  const reportAnalyticsEvent = useSearchBarAnalytics();

  const query = useSearchState(state => state.query.input) ?? '';
  const cssClasses = useComposedCssClasses(builtInCssClasses, customCssClasses);
  const isVertical = useSearchState(state => state.meta.searchType) === SearchTypeEnum.Vertical;
  const verticalKey = useSearchState(state => state.vertical.verticalKey);
  const debouncedExecuteAutocompleteSearch = useDebouncedFunction(
    () => executeAutocompleteSearch(searchActions),
    200
  );
  const [autocompleteResponse, executeAutocomplete, clearAutocompleteData] = useSynchronizedRequest(
    async () => {
      return !autocompleteDisabled && debouncedExecuteAutocompleteSearch ?
        debouncedExecuteAutocompleteSearch() :
        undefined;
    }
  );
  const [
    executeQueryWithNearMeHandling,
    autocompletePromiseRef,
  ] = useSearchWithNearMeHandling(geolocationOptions, onSearch);
  const [
    recentSearches,
    setRecentSearch,
    clearRecentSearches,
  ] = useRecentSearches(recentSearchesLimit, verticalKey);
  const filteredRecentSearches = recentSearches?.filter(search =>
    searchUtilities.isCloseMatch(search.query, query)
  );

  useEffect(() => {
    if (hideRecentSearches) {
      clearRecentSearches();
    }
  }, [clearRecentSearches, hideRecentSearches]);

  useEffect(() => {
    if (universalAutocompleteLimit) {
      searchActions.setUniversalAutocompleteLimit(universalAutocompleteLimit);
    } else {
      searchActions.setUniversalAutocompleteLimit(undefined);
    }
    if (verticalKey && verticalAutocompleteLimits?.[verticalKey]) {
      searchActions.setVerticalAutocompleteLimit(verticalAutocompleteLimits[verticalKey]);
    } else {
      searchActions.setVerticalAutocompleteLimit(undefined);
    }
  }, [searchActions, universalAutocompleteLimit, verticalAutocompleteLimits, verticalKey]);

  const clearAutocomplete = useCallback(() => {
    clearAutocompleteData();
    autocompletePromiseRef.current = undefined;
  }, [autocompletePromiseRef, clearAutocompleteData]);

  const executeQuery = useCallback(() => {
    if (!hideRecentSearches) {
      const input = searchActions.state.query.input;
      input && setRecentSearch(input);
    }
    executeQueryWithNearMeHandling();
  }, [
    searchActions.state.query.input,
    executeQueryWithNearMeHandling,
    hideRecentSearches,
    setRecentSearch
  ]);

  const handleSubmit = useCallback((value?: string, index?: number, itemData?: FocusedItemData) => {
    value !== undefined && searchActions.setQuery(value);
    searchActions.setOffset(0);
    searchActions.setFacets([]);
    clearStaticRangeFilters(searchActions);
    if (itemData && isVerticalLink(itemData.verticalLink) && onSelectVerticalLink) {
      onSelectVerticalLink({ verticalLink: itemData.verticalLink, querySource: QuerySource.Autocomplete });
    } else {
      executeQuery();
    }
    if (typeof index === 'number' && index >= 0 && !itemData?.isEntityPreview) {
      reportAnalyticsEvent('AUTO_COMPLETE_SELECTION');
    }
  }, [searchActions, executeQuery, onSelectVerticalLink, reportAnalyticsEvent]);

  const [
    entityPreviewsState,
    executeEntityPreviewsQuery
  ] = useEntityPreviews(entityPreviewSearcher, entityPreviewsDebouncingTime);
  const { verticalKeyToResults, isLoading: entityPreviewsLoading } = entityPreviewsState;
  const getAriaLabelText = useCallback((value: string) => {
    return t('resultPreview', { value });
  }, [t]);
  const entityPreviews = renderEntityPreviews?.(
    entityPreviewsLoading,
    verticalKeyToResults,
    { onClick: handleSubmit, ariaLabel: getAriaLabelText }
  );
  const updateEntityPreviews = useCallback((query: string) => {
    if (!renderEntityPreviews || !includedVerticals) {
      return;
    }
    executeEntityPreviewsQuery(query, universalLimit ?? {}, includedVerticals);
  }, [executeEntityPreviewsQuery, renderEntityPreviews, includedVerticals, universalLimit]);

  const handleInputFocus = useCallback((value = '') => {
    searchActions.setQuery(value);
    updateEntityPreviews(value);
    autocompletePromiseRef.current = executeAutocomplete();
  }, [searchActions, autocompletePromiseRef, executeAutocomplete, updateEntityPreviews]);

  const handleInputChange = useCallback((value = '') => {
    searchActions.setQuery(value);
    updateEntityPreviews(value);
    autocompletePromiseRef.current = executeAutocomplete();
  }, [searchActions, autocompletePromiseRef, executeAutocomplete, updateEntityPreviews]);

  const handleClickClearButton = useCallback(() => {
    updateEntityPreviews('');
    searchActions.setQuery('');
    reportAnalyticsEvent('SEARCH_CLEAR_BUTTON');
  }, [reportAnalyticsEvent, searchActions, updateEntityPreviews]);

  function renderInput() {
    return (
      <DropdownInput
        className={cssClasses.inputElement}
        placeholder={placeholder}
        onSubmit={handleSubmit}
        onFocus={handleInputFocus}
        onChange={handleInputChange}
        ariaLabel={t('conductASearch')}
      />
    );
  }

  function renderRecentSearches() {
    const recentSearchesCssClasses = {
      icon: cssClasses.recentSearchesIcon,
      option: cssClasses.recentSearchesOption,
      nonHighlighted: cssClasses.recentSearchesNonHighlighted
    };

    return filteredRecentSearches?.map((result, i) => (
      <DropdownItem
        className='flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100'
        focusedClassName={twMerge('flex items-center h-6.5 px-3.5 py-1.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}
        key={i}
        value={result.query}
        onClick={handleSubmit}
        ariaLabel={t('recentSearch', {
          query: result.query
        })}
      >
        {renderAutocompleteResult(
          { value: result.query, inputIntents: [] },
          recentSearchesCssClasses,
          RecentSearchIcon
        )}
      </DropdownItem>
    ));
  }

  const itemDataMatrix = useMemo(() => {
    return autocompleteResponse?.results.map(result => {
      return result.verticalKeys?.map(verticalKey => ({
        verticalLink: { verticalKey, query: result.value }
      })) ?? [];
    }) ?? [];
  }, [autocompleteResponse?.results]);

  function renderQuerySuggestions() {
    return autocompleteResponse?.results.map((result, i) => (
      <Fragment key={i}>
        <DropdownItem
          className='flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100'
          focusedClassName={twMerge('flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}
          value={result.value}
          onClick={handleSubmit}
          ariaLabel={t('autocompleteSuggestion', { suggestion: result.value })}
        >
          {renderAutocompleteResult(
            result,
            cssClasses,
            MagnifyingGlassIcon
          )}
        </DropdownItem>
        {showVerticalLinks && !isVertical && result.verticalKeys?.map((verticalKey, j) => (
          <DropdownItem
            key={j}
            className='flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100'
            focusedClassName={twMerge('flex items-stretch py-1.5 px-3.5 cursor-pointer hover:bg-gray-100', cssClasses.focusedOption)}
            value={result.value}
            itemData={itemDataMatrix[i][j]}
            onClick={handleSubmit}
          >
            {renderAutocompleteResult(
              {
                value: `in ${verticalKeyToLabel ? verticalKeyToLabel(verticalKey) : verticalKey}`,
                inputIntents: []
              },
              { ...cssClasses, option: cssClasses.verticalLink }
            )}
          </DropdownItem>
        ))}
      </Fragment>
    ));
  }

  function renderClearButton() {
    return (
      <>
        <button
          aria-label={t('clearTheSearchBar')}
          className={cssClasses.clearButton}
          onClick={handleClickClearButton}
        >
          <CloseIcon />
        </button>
        <VerticalDividerIcon className={cssClasses.verticalDivider} />
      </>
    );
  }

  const entityPreviewsCount = calculateEntityPreviewsCount(entityPreviews);
  const showEntityPreviewsDivider = entityPreviews
    && !!(autocompleteResponse?.results.length || filteredRecentSearches?.length);
  const hasItems = !!(autocompleteResponse?.results.length
    || filteredRecentSearches?.length || entityPreviews);
  const screenReaderText = getScreenReaderText(
    autocompleteResponse?.results.length,
    filteredRecentSearches?.length,
    entityPreviewsCount,
    t
  );
  const activeClassName = classNames('relative z-10 bg-white border rounded-3xl border-gray-200 w-full overflow-hidden', {
    'shadow-lg': hasItems
  });

  const handleToggleDropdown = useCallback((isActive: boolean) => {
    if (!isActive) {
      clearAutocomplete();
    }
  }, [clearAutocomplete]);

  return (
    <div className={cssClasses.searchBarContainer}>
      <Dropdown
        className='relative bg-white border rounded-3xl border-gray-200 w-full overflow-hidden'
        activeClassName={activeClassName}
        screenReaderText={screenReaderText}
        parentQuery={query}
        onToggle={handleToggleDropdown}
      >
        <div className='inline-flex items-center justify-between w-full'>
          {renderInput()}
          {query && renderClearButton()}
          <DropdownSearchButton
            handleSubmit={handleSubmit}
            cssClasses={cssClasses}
          />
        </div>
        {hasItems &&
          <StyledDropdownMenu cssClasses={cssClasses}>
            {renderRecentSearches()}
            {renderQuerySuggestions()}
            {entityPreviews && (
              <div role='group'>
                {showEntityPreviewsDivider && <div className={cssClasses.entityPreviewsDivider} aria-hidden='true' />}
                {entityPreviews}
              </div>
            )}
          </StyledDropdownMenu>
        }
      </Dropdown>
    </div>
  );
}

function StyledDropdownMenu({ cssClasses, children }: PropsWithChildren<{
  cssClasses: {
    inputDivider?: string
  }
}>) {
  return (
    <div>
      <div className={cssClasses.inputDivider} aria-hidden='true' />
      <DropdownMenu className='bg-white py-4'>
        {children}
      </DropdownMenu>
    </div>
  );
}

function getScreenReaderText(
  autocompleteOptions = 0,
  recentSearchesOptions = 0,
  entityPreviewsCount = 0,
  t: ReturnType<typeof useTranslation>['t']
): string {
  const texts: string[] = [];
  recentSearchesOptions > 0 && texts.push(t('recentSearchesFound', {
    count: recentSearchesOptions
  }));
  entityPreviewsCount > 0 && texts.push(t('resultPreviewsFound', {
    count: entityPreviewsCount
  }));
  autocompleteOptions > 0 && texts.push(t('autocompleteSuggestionsFound', {
    count: autocompleteOptions
  }));

  const text = texts.join(' ');
  if (text === '') {
    return t('noAutocompleteSuggestionsFound');
  }
  return text.trim();
}

function DropdownSearchButton({ handleSubmit, cssClasses }: {
  handleSubmit: () => void,
  cssClasses: {
    searchButtonContainer?: string,
    searchButton?: string
  }
}) {
  const { toggleDropdown } = useDropdownContext();
  const handleClick = useCallback(() => {
    handleSubmit();
    toggleDropdown(false);
  }, [handleSubmit, toggleDropdown]);
  return (
    <div className={cssClasses.searchButtonContainer}>
      <SearchButton
        className={cssClasses.searchButton}
        handleClick={handleClick}
      />
    </div>
  );
}

/**
 * Calculates the number of navigable entity previews from a ReactNode containing DropdownItems.
 */
export function calculateEntityPreviewsCount(children: ReactNode): number {
  let count = 0;
  recursivelyMapChildren(children, c => {
    if (isValidElement(c) && c.type === DropdownItem) {
      count++;
    }
    return c;
  });
  return count;
}
