import React, { useState, useEffect, useRef, ReactNode, RefObject } from 'react'
import { useRouter } from 'next/router'
import useSWR from 'swr'
import cx from 'classnames'
import { Flash, Label, ActionList, ActionMenu } from '@primer/react'
import { ItemInput } from '@primer/react/lib/deprecated/ActionList/List'
import { InfoIcon } from '@primer/octicons-react'

import { useLanguages } from 'components/context/LanguagesContext'
import { useTranslation } from 'components/hooks/useTranslation'
import { sendEvent, EventType } from 'components/lib/events'
import { useMainContext } from './context/MainContext'
import { DEFAULT_VERSION, useVersion } from 'components/hooks/useVersion'
import { useQuery } from 'components/hooks/useQuery'
import { Link } from 'components/Link'

import styles from './Search.module.scss'

const SEARCH_API_ENDPOINT = '/api/search/v1'

type Hit = {
  id: string
  url: string
  title: string
  breadcrumbs: string
  highlights: {
    title: Array<string>
    content: Array<string>
  }
  score?: number
  popularity?: number
  es_url?: string
}

// Note, the JSON will contain other keys too, but since we don't use
// them in this component, we don't need to specify them.
type Meta = {
  found: {
    value: number
  }
}

type Data = {
  hits: Hit[]
  meta: Meta
}

type Props = {
  isHeaderSearch?: boolean
  isMobileSearch?: boolean
  variant?: 'compact' | 'expanded'
  iconSize: number
  children?: (props: { SearchInput: ReactNode; SearchResults: ReactNode }) => ReactNode
}

export function Search({
  isHeaderSearch = false,
  isMobileSearch = false,
  variant = 'compact',
  iconSize = 24,
  children,
}: Props) {
  const router = useRouter()
  const { query, debug } = useQuery()
  const [localQuery, setLocalQuery] = useState(query)
  const [debouncedQuery, setDebouncedQuery] = useDebounce<string>(localQuery, 300)
  const inputRef = useRef<HTMLInputElement>(null)
  const { t } = useTranslation('search')
  const { currentVersion } = useVersion()
  const { languages } = useLanguages()

  // Figure out language and version for index
  const { searchVersions, nonEnterpriseDefaultVersion } = useMainContext()
  // fall back to the non-enterprise default version (FPT currently) on the homepage, 404 page, etc.
  const version = searchVersions[currentVersion] || searchVersions[nonEnterpriseDefaultVersion]
  const language = languages
    ? (Object.keys(languages).includes(router.locale || '') && router.locale) || 'en'
    : 'en'

  const fetchURL = query
    ? `${SEARCH_API_ENDPOINT}?${new URLSearchParams({
        language,
        version,
        query,
        // In its current state, can't with confidence know if the user
        // initiated a search because of the input debounce or if the
        // user has finished typing.
        autocomplete: 'true',
      })}`
    : null

  const { data, error: searchError } = useSWR<Data, Error>(
    fetchURL,
    async (url: string) => {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`${response.status} on ${url}`)
      }
      return await response.json()
    },
    {
      onSuccess: () => {
        sendEvent({
          type: EventType.search,
          search_query: query,
          // search_context
        })
      },
      // Because the backend never changes between fetches, we can treat
      // it as an immutable resource and disable these revalidation
      // checks.
      revalidateIfStale: false,
      revalidateOnFocus: false,
      revalidateOnReconnect: false,
      // By default, when the URL changes to the hook, the `data` and `error`
      // are reset. Just as if it was the very first mount.
      // Previously we used to keep a copy of the "last" data output
      // so that we don't immediately wipe away any search results
      // when waiting for the new ones.
      // With the `keepPreviousData` option, it simply doesn't reset the
      // `data` output just because the URL is changing from what it
      // was previously.
      keepPreviousData: true,
    }
  )

  // The `isLoading` boolean will become false every time the useSWR hook
  // fires off a new XHR. So it toggles from false/true often.
  // But we don't want to display "Loading..." every time a new XHR query
  // begins, immediately, because the XHR requests are usually very fast
  // so that you just see it flicker by. That's why we introduce a
  // debounced version of that same boolean value.
  // The problem is that the debounce is *trailing*. Meaning, it will
  // always yield the last thing you sent to it, but with a delay.
  // The problem is that, by the time the debounce finally fires,
  // it might say 'true' when in fact the XHR has finished! That would
  // mean saying "Loading..." is a lie!
  // That's why we combine them into a final one. We're basically doing
  // this to favor *NOT* saying "Loading...".
  const isLoadingRaw = Boolean(query && !data && !searchError)
  const [isLoadingDebounced] = useDebounce<boolean>(isLoadingRaw, 500)
  const isLoading = isLoadingRaw && isLoadingDebounced

  useEffect(() => {
    // Because we don't want to have to type .trim() everywhere we
    // use this variable and we also don't want to change the origin.
    // This variable is used to decide if and what we should change
    // the URL to.
    // Trim whitespace to make sure there's anything left and when
    // do put this debounced query into the query string, we use it
    // with the whitespace trimmed.
    const query = debouncedQuery.trim()

    if ((router.query.query || '') !== query) {
      const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
      const params = new URLSearchParams(asPathQuery)
      if (query) {
        params.set('query', query)
      } else {
        params.delete('query')
      }
      let asPath = `/${router.locale}${asPathRoot}`
      if (params.toString()) {
        asPath += `?${params.toString()}`
      }
      // Workaround a next.js routing behavior that
      // will cause the default locale path of the index page
      // "/en" to change to just "/".
      if (router.pathname === '/') {
        // Don't include router.locale so next doesn't attempt a
        // request to `/_next/static/chunks/pages/en.js`
        router.replace(`/?${params.toString()}`, asPath, { shallow: true })
      } else {
        router.replace(asPath, undefined, { shallow: true })
      }
    }
  }, [debouncedQuery])

  // When the user finishes typing, update the results
  function onSearch(e: React.ChangeEvent<HTMLInputElement>) {
    setLocalQuery(e.target.value)
  }
  useEffect(() => {
    if (localQuery.trim()) {
      if (localQuery.endsWith(' ')) {
        setDebouncedQuery(localQuery.trim())
      }
    } else {
      setDebouncedQuery('')
    }
  }, [localQuery])

  // Close panel if overlay is clicked
  function closeSearch() {
    setLocalQuery('')
  }

  // Prevent the page from refreshing when you "submit" the form
  function onFormSubmit(evt: React.FormEvent) {
    evt.preventDefault()
    if (localQuery.trim()) {
      setDebouncedQuery(localQuery.trim())
    }
  }

  const SearchResults = (
    <>
      <div
        id="search-results-container"
        className={cx(
          'z-1 pb-5 px-3',
          isHeaderSearch &&
            'pt-9 color-bg-default color-shadow-medium position-absolute top-0 right-0',
          styles.resultsContainer,
          isHeaderSearch && styles.resultsContainerHeader,
          query || searchError ? 'd-block' : 'd-none',
          (query || searchError) && styles.resultsContainerOpen
        )}
      >
        {searchError ? (
          <ShowSearchError
            error={searchError}
            isHeaderSearch={isHeaderSearch}
            isMobileSearch={isMobileSearch}
          />
        ) : (
          <ShowSearchResults
            anchorRef={inputRef}
            isHeaderSearch={isHeaderSearch}
            isMobileSearch={isMobileSearch}
            isLoading={isLoading}
            results={data}
            closeSearch={closeSearch}
            debug={debug}
            query={query}
          />
        )}
      </div>
      {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
      <div
        className={cx(
          '-z-1',
          isHeaderSearch && query
            ? 'position-fixed top-0 right-0 bottom-0 left-0 d-block'
            : 'd-none',
          isHeaderSearch && query && styles.headerSearchOpen
        )}
        onClick={closeSearch}
      />
    </>
  )

  const SearchInput = (
    <div data-testid="search">
      <div className="position-relative z-2">
        <form role="search" className="width-full d-flex" noValidate onSubmit={onFormSubmit}>
          <label className="text-normal width-full">
            <span
              className="visually-hidden"
              aria-label={t`label`}
              aria-describedby={t`description`}
            >{t`placeholder`}</span>
            <input
              data-testid="site-search-input"
              ref={inputRef}
              className={cx(
                styles.searchInput,
                iconSize === 24 && styles.searchIconBackground24,
                iconSize === 24 && 'form-control px-6 f4',
                iconSize === 16 && styles.searchIconBackground16,
                iconSize === 16 && 'form-control px-5 f4',
                variant === 'compact' && 'py-2',
                variant === 'expanded' && 'py-3',
                isHeaderSearch && styles.searchInputHeader,
                !isHeaderSearch && 'width-full',
                isHeaderSearch && query && styles.searchInputExpanded,
                isHeaderSearch && query && 'position-absolute top-0 right-0'
              )}
              type="search"
              placeholder={t`placeholder`}
              autoComplete={localQuery ? 'on' : 'off'}
              autoCorrect="off"
              autoCapitalize="off"
              spellCheck="false"
              maxLength={512}
              onChange={onSearch}
              value={localQuery}
              aria-label={t`label`}
              aria-describedby={t`description`}
            />
          </label>
          <button className="d-none" type="submit" title="Submit the search query." hidden />
        </form>
      </div>
    </div>
  )

  return (
    <>
      {typeof children === 'function' ? (
        children({ SearchInput, SearchResults })
      ) : (
        <>
          {SearchInput}
          {SearchResults}
        </>
      )}
    </>
  )
}

function useDebounce<T>(value: T, delay?: number): [T, (value: T) => void] {
  const [debouncedValue, setDebouncedValue] = useState<T>(value)

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay || 500)
    return () => {
      clearTimeout(timer)
    }
  }, [value, delay])

  return [debouncedValue, setDebouncedValue]
}

function ShowSearchError({
  error,
}: {
  error: Error
  isHeaderSearch: boolean
  isMobileSearch: boolean
}) {
  const { t } = useTranslation('search')
  return (
    <Flash variant="danger" sx={{ margin: '2rem 2rem 0 2em' }}>
      <p>{t('search_error')}</p>
      {process.env.NODE_ENV === 'development' && (
        <p>
          <small>
            <code>{error.toString()}</code>
          </small>
        </p>
      )}
    </Flash>
  )
}

function ShowSearchResults({
  isHeaderSearch,
  isLoading,
  results,
  debug,
  query,
}: {
  anchorRef: RefObject<HTMLElement>
  isHeaderSearch: boolean
  isMobileSearch: boolean
  isLoading: boolean
  results: Data | undefined
  closeSearch: () => void
  debug: boolean
  query: string
}) {
  const { t } = useTranslation(['pages', 'search'])
  const router = useRouter()
  const { currentVersion } = useVersion()
  const { allVersions } = useMainContext()
  const searchVersion = allVersions[currentVersion].versionTitle
  const [selectedVersion, setSelectedVersion] = useState<ItemInput | undefined>()
  const currentVersionPathSegment = currentVersion === DEFAULT_VERSION ? '' : `/${currentVersion}`

  const latestVersions = new Set(
    Object.keys(allVersions)
      .map((version) => allVersions[version].latestVersion)
      .filter((version) => version !== currentVersion)
  )

  const versions = Array.from(latestVersions).map((version) => {
    return {
      title: allVersions[version].versionTitle,
      version,
    }
  })

  const searchVersions: ItemInput[] = versions.map(({ title, version }) => {
    return {
      text: title,
      key: version,
    }
  })

  const redirectParams: {
    query: string
    debug?: string
  } = { query }

  if (debug) redirectParams.debug = JSON.stringify(debug)

  const redirectQuery = `?${new URLSearchParams(redirectParams).toString()}`

  useEffect(() => {
    if (selectedVersion) {
      const params = new URLSearchParams(redirectParams)
      let asPath = `/${router.locale}`

      if (params.toString()) {
        asPath += `?${params.toString()}`
      }

      if (selectedVersion.key === DEFAULT_VERSION) {
        router.push(`/?${params.toString()}`, asPath)
      } else {
        router.push(`/${router.locale}/${selectedVersion.key}${redirectQuery}`)
      }
    }
  }, [selectedVersion])

  if (results) {
    const ActionListResults = (
      <div
        data-testid="search-results"
        className={cx(
          'mt-3',
          isHeaderSearch && styles.headerSearchResults,
          isHeaderSearch && 'overflow-auto'
        )}
      >
        <div className={cx(styles.versionSearchContainer, 'mt-4 pb-4 width-full border-bottom')}>
          <p className={cx(styles.searchWording, 'f6 ml-4 d-inline-block')}>
            You're searching the <strong>{searchVersion}</strong> version.
          </p>
          <div className="float-right mr-4">
            <p
              aria-describedby={`You're searching the ${searchVersion} version`}
              className={cx(styles.selectWording, 'f6 d-inline-block')}
            >
              Select version:
            </p>
            <ActionMenu>
              <ActionMenu.Button sx={{ display: 'inline-block' }}>
                {selectedVersion ? selectedVersion.text : searchVersion}
              </ActionMenu.Button>
              <ActionMenu.Overlay>
                <ActionList selectionVariant="single">
                  {searchVersions.map((searchVersion) => {
                    return (
                      <ActionList.Item
                        onSelect={() => setSelectedVersion(searchVersion)}
                        key={searchVersion.key}
                      >
                        {searchVersion.text}
                      </ActionList.Item>
                    )
                  })}

                  <ActionList.LinkItem
                    className="f6"
                    href={`/${router.locale}${currentVersionPathSegment}/get-started/learning-about-github/about-versions-of-github-docs`}
                  >
                    {t('about_versions')} <InfoIcon />
                  </ActionList.LinkItem>
                </ActionList>
              </ActionMenu.Overlay>
            </ActionMenu>
          </div>
        </div>
        {/* We might have results AND isLoading. For example, the user typed
        a first word, and is now typing more. */}
        {isLoading && (
          <p className="d-block ml-4 mt-4">
            <span>{t('loading')}...</span>
          </p>
        )}
        <h1 className="ml-4 f2 mt-4">
          {t('search_results_for')}: {query}
        </h1>
        <p className="ml-4 mb-4 text-normal f5">
          {results.meta.found.value === 0 ? (
            t('no_results')
          ) : (
            <span>
              {t('matches_found')}: {results.meta.found.value.toLocaleString()}
            </span>
          )}
          .{' '}
          {results.meta.found.value > results.hits.length && (
            <span>
              {t('matches_displayed')}:{' '}
              {results.meta.found.value === 0 ? t('no_results') : results.hits.length}
            </span>
          )}
        </p>

        <ActionList variant="full">
          {results.hits.map((hit, index) => {
            const { url, breadcrumbs, title, highlights, score, popularity } = hit

            const contentHTML =
              highlights.content && highlights.content.length > 0
                ? highlights.content.join('<br>')
                : ''
            const titleHTML =
              highlights.title && highlights.title.length > 0 ? highlights.title[0] : title

            return (
              <ActionList.Item className="width-full" key={url}>
                <Link
                  href={url}
                  className="no-underline color-fg-default"
                  onClick={() => {
                    sendEvent({
                      type: EventType.searchResult,
                      search_result_query: Array.isArray(query) ? query[0] : query,
                      search_result_index: index,
                      search_result_total: results.hits.length,
                      search_result_rank: (results.hits.length - index) / results.hits.length,
                      search_result_url: url,
                    })
                  }}
                >
                  <div
                    data-testid="search-result"
                    className={cx('list-style-none', styles.resultsContainer)}
                  >
                    <div className={cx('py-2 px-3')}>
                      <Label size="small" variant="accent">
                        {breadcrumbs ? breadcrumbs.split(' / ')[0] : title}
                      </Label>
                      {debug && score !== undefined && popularity !== undefined && (
                        <small className="float-right">
                          score: {score.toFixed(4)} popularity: {popularity.toFixed(4)}
                        </small>
                      )}
                      <h2
                        className={cx('mt-2 text-normal f3 d-block')}
                        dangerouslySetInnerHTML={{
                          __html: titleHTML,
                        }}
                      />
                      <div
                        className={cx(styles.searchResultContent, 'mt-1 d-block overflow-hidden')}
                        style={{ maxHeight: '2.5rem' }}
                        dangerouslySetInnerHTML={{ __html: contentHTML }}
                      />
                      <div
                        className={'d-block mt-2 opacity-70 text-small'}
                        dangerouslySetInnerHTML={
                          breadcrumbs.length === 0
                            ? { __html: `${title}`.replace(/<\/?[^>]+(>|$)|(\/)/g, '') }
                            : {
                                __html: breadcrumbs
                                  .split(' / ')
                                  .slice(0, breadcrumbs.length - 1)
                                  .join(' / ')
                                  .replace(/<\/?[^>]+(>|$)/g, ''),
                              }
                        }
                      />
                    </div>
                  </div>
                </Link>
              </ActionList.Item>
            )
          })}
        </ActionList>
      </div>
    )
    return <div>{ActionListResults}</div>
  }

  // We have no results at all, but perhaps we're waiting.
  if (isHeaderSearch) {
    return (
      <div className="mt-2 px-6 pt-3">
        {isLoading ? <span>{t('loading')}...</span> : <span>&nbsp;</span>}
      </div>
    )
  }
  return (
    <p data-testid="results-spacer" className="d-block mt-4">
      {/*
        This exists so that there's always *something* displayed in the
        DOM with or without a search result.
        That way, the vertical space is predetermined as a minimum.
        Note: Perhaps it would be better to use CSS but by using a
        real, but empty, DOM element, the height is always minimal and
        always perfectly accurate.
      */}
      {isLoading ? <span>{t('loading')}...</span> : <span>&nbsp;</span>}
    </p>
  )
}
