import { ChangeEventHandler, ComponentType, FocusEvent, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { LoadingDots } from './loading-dots.js'
import { Icon } from './icon.js'
import { Err, Errors, Field, FieldWrapper, Helper, Input, Label } from './form-elements/shared.js'
import { Copy } from './copy.js'
import { focusWithoutScroll, capitalize, noop } from '@navinc/utils'
import { useDebouncedCallback } from 'use-debounce'

const id = <T,>(id: T): T => id

const SearchInputContainer = styled.div`
  width: 100%;
`

const ResultsContainer = styled.div`
  position: relative;
  z-index: 2;

  & > ${Helper} {
    margin-left: ${({ theme }) => theme.gu(2)};
  }
`

const ResultContainer = styled.div<{ isActive?: boolean }>`
  cursor: pointer;
  outline: none;
  padding: 16px;
  border-bottom: 1px solid ${({ theme }) => theme.navNeutral300};

  ${({ isActive, theme }) =>
    isActive && `background-color: ${theme.navNeutral100}; border: 4px solid ${theme.navStatusPositive500};`}
  &:hover {
    background-color: ${({ theme }) => theme.navNeutral100};
  }

  &:focus {
    outline: ${({ theme }) => theme.focusOutline};
  }
`

const Results = styled.div<{ shouldPositionResultsRelative?: boolean; resultsMaxHeight?: string }>`
  background-color: ${({ theme }) => theme.navNeutralLight};
  border: solid 1px ${({ theme }) => theme.navNeutral300};
  border-top: none;
  border-radius: 0 0 8px 8px;
  position: ${({ shouldPositionResultsRelative }) => (shouldPositionResultsRelative ? 'relative' : 'absolute')};
  top: 0;
  overflow-y: auto;
  max-height: ${({ resultsMaxHeight }) => resultsMaxHeight || '300px'};
  width: 100%;

  /* prettier-ignore */
  ${ResultContainer}:last-child {
    border-bottom: none;
  }
`

const StyledFieldWrapper = styled(FieldWrapper)<{ shouldShowDropDown?: boolean }>`
  & ${Icon}, & ${LoadingDots} {
    position: absolute;
    right: 16px;
    top: 50%;
    transform: translateY(-50%);
  }
  & ${Icon} {
    width: 24px;
  }
  & ${LoadingDots} {
    color: ${({ theme }) => theme.navPrimary};
    width: 56px;
  }
  & ${Input} {
    text-overflow: ellipsis;
    padding-right: 40px;
    ${({ shouldShowDropDown }) => shouldShowDropDown && 'border-radius: 4px 4px 0 0;'}
  }
`

const StyledNoResults = styled.div`
  align-items: center;
  display: flex;
  flex-direction: column;
  margin-bottom: 24px;
  margin-top: 24px;
`

const NoResultsIcon = styled(Icon)`
  color: ${({ theme }) => theme.navNeutral400};
  margin-bottom: 8px;
`

const DefaultNoResults = ({ query }: { query: string }) => (
  <StyledNoResults>
    <NoResultsIcon name="actions/circle-faq" />
    <Copy>No results for "{query}"</Copy>
  </StyledNoResults>
)

export type SearchInputProps<T> = {
  value?: T
  results?: T[]
  autoFocus?: boolean
  className?: string
  errors?: string[]
  helperText?: string
  isLoading?: boolean
  isInvalid?: boolean
  label?: string
  lede?: string
  name?: string
  NoResults?: ComponentType<{ query: string }>
  onBlur?: (event: FocusEvent<HTMLInputElement>) => void
  onChange?: (event: { target: { name?: string; value: T | null } }) => void
  onFocus?: (event: FocusEvent<HTMLInputElement>) => void
  Result: ComponentType<{ result: T }>
  required?: boolean
  resultsMaxHeight?: string
  resultToQuery?: (result: T) => string
  search: (query: string) => Promise<T[]>
  shouldPositionResultsRelative?: boolean
  touched?: boolean
  type?: string
}

const _SearchInput = <T,>({
  autoFocus,
  className,
  errors = [],
  helperText,
  isLoading,
  isInvalid,
  label,
  lede,
  name,
  NoResults,
  onBlur = noop,
  onChange = noop,
  onFocus = noop,
  Result,
  required,
  results = [],
  resultsMaxHeight,
  resultToQuery = id as (result: T) => string,
  search,
  shouldPositionResultsRelative,
  touched,
  type,
  value,
  ...inputProps
}: SearchInputProps<T>) => {
  const [focusedResultIndex, setFocusedResultIndex] = useState(-1)
  const [canBeOpen, setCanBeOpen] = useState(false)
  const [query, setQuery] = useState('')
  const [lastSearchedQuery, setLastSearchedQuery] = useState('')

  const inputRef = useRef<HTMLInputElement>(null)
  const resultsRef = useRef<HTMLDivElement>(null)

  const shouldShowDropDown = !!(query.length && canBeOpen && query === lastSearchedQuery && !isLoading)

  const shouldShowResults = !!(shouldShowDropDown && results.length)
  const shouldShowNoResults = !!(shouldShowDropDown && !results.length)
  const RenderNoResults = NoResults || DefaultNoResults
  const isVisited = touched || !!value

  useEffect(() => {
    if (!value) {
      return
    }

    const newQuery = resultToQuery(value)

    if (!newQuery) {
      return
    }

    setQuery(newQuery)
  }, [resultToQuery, value])

  useEffect(() => {
    if (autoFocus && inputRef.current) {
      focusWithoutScroll(inputRef.current)
    }
  }, [autoFocus])

  const searchDebounced = useDebouncedCallback((query) => {
    setLastSearchedQuery(query)
    search(query)

    setFocusedResultIndex(-1)
  }, 500)

  const handleChange: ChangeEventHandler<HTMLInputElement> = ({ target }) => {
    const newQuery = target.value ?? ''

    setQuery(newQuery)

    onChange({
      target: {
        name,
        value: null,
      },
    })

    if (newQuery.length > 0) {
      searchDebounced(newQuery)
      setCanBeOpen(true)
    }
  }

  const handleInputFocus = (event) => {
    setCanBeOpen(true)

    onFocus(event)
  }

  const focusResult = (index) => {
    const ref = resultsRef.current?.children?.[index] as HTMLElement | undefined

    if (ref) {
      setFocusedResultIndex(index)
      ref.focus()
      inputRef.current?.focus()
    }
  }

  const goToNextResult = () => {
    const newIndex = focusedResultIndex + 1

    if (newIndex < results.length) {
      focusResult(newIndex)
    }
  }

  const goToPreviousResult = () => {
    const newIndex = focusedResultIndex - 1

    if (newIndex > -2) {
      focusResult(newIndex)
    }
  }

  const closeResults = () => {
    setCanBeOpen(false)
    setFocusedResultIndex(-1)
  }

  const onSelectResult = (clickedResult?: T) => {
    const result = clickedResult || results[focusedResultIndex]

    closeResults()

    onChange({
      target: {
        name,
        value: result,
      },
    })
  }

  const handleBlurOnInput = (event) => {
    if (!resultsRef.current || (resultsRef.current && !resultsRef.current.contains(event.relatedTarget))) {
      closeResults()
      onBlur(event)
    }
  }

  const handleKeyDownOnInput = (event) => {
    switch (event.keyCode) {
      case 40: // Down Arrow
        if (shouldShowResults) {
          event.preventDefault()
          goToNextResult()
        } else {
          setCanBeOpen(true)
        }
        break

      case 9: // Tab
        if (shouldShowResults) {
          event.preventDefault()
          if (event.shiftKey) {
            goToPreviousResult()
          } else {
            goToNextResult()
          }
        }
        break

      case 38: // Up Arrow
        if (shouldShowResults) {
          event.preventDefault()
          goToPreviousResult()
        }
        break

      case 27: // Escape
        if (focusedResultIndex > -1) {
          setFocusedResultIndex(-1)
        } else {
          setQuery('')
        }
        break

      case 13: // Enter
        if (focusedResultIndex > -1) {
          event.preventDefault()
          onSelectResult()
        }
        break
    }
  }

  return (
    <SearchInputContainer className={className}>
      <StyledFieldWrapper shouldShowDropDown={shouldShowDropDown}>
        {lede && <Copy bold>{lede}</Copy>}
        <Field isVisited={isVisited}>
          <Input
            autoComplete="off"
            data-testid="search-input:input"
            name={name}
            onBlur={handleBlurOnInput}
            onChange={handleChange}
            onFocus={handleInputFocus}
            onKeyDown={handleKeyDownOnInput}
            type={type}
            ref={inputRef}
            required={required}
            value={query}
            isInvalid={isInvalid}
            {...inputProps}
          />
          {isLoading ? <LoadingDots /> : <Icon name="system/search" />}
          <Label required={required}>{capitalize(label)}</Label>
        </Field>
      </StyledFieldWrapper>
      <ResultsContainer>
        {shouldShowDropDown && (
          <Results
            resultsMaxHeight={resultsMaxHeight}
            ref={resultsRef}
            shouldPositionResultsRelative={shouldPositionResultsRelative}
          >
            {shouldShowNoResults && <RenderNoResults query={query} />}
            {shouldShowResults &&
              results.map((result, index) => (
                <ResultContainer
                  isActive={focusedResultIndex === index}
                  key={JSON.stringify(result)}
                  onMouseDown={() => onSelectResult(result)}
                  tabIndex={0}
                >
                  <Result result={result} />
                </ResultContainer>
              ))}
          </Results>
        )}
        {helperText && <Helper hasSpaceForHelper helperText={helperText} />}
        <Errors hasSpaceForErrors>
          {!!errors.length && errors.map((err, i) => <Err key={`err-${i}`}>{err}</Err>)}
        </Errors>
      </ResultsContainer>
    </SearchInputContainer>
  )
}

export const SearchInput = styled(_SearchInput)`` as typeof _SearchInput
