import React, { useState, useEffect, useCallback, type ChangeEvent } from 'react'
import styled from 'styled-components'
import { animated, config as springConfig, Transition } from '@react-spring/web'
import de from './locales/de.yml'
import fr from './locales/fr.yml'

type MessageListContent = string | MessageList

/** Recursive type for storing translated strings in objects */
interface MessageList {
  [key: string]: MessageListContent
}

interface LocalisedMessagesList {
  [key: string]: MessageList
}

export interface Municipality {
  readonly GDENR: number
  readonly ORTNAME?: string
  readonly PLZ4?: number
  readonly PLZ6?: number
  readonly GDENAMK?: string
  readonly KTKZ: string
  readonly NORMORTSNAME?: string
  readonly NORMGEMEINDE: string
  readonly URL?: string
  [x: string]: any
}

export interface Props {
  /** Which year to use for the municipality data. For the 2019 data, this
   *  would be `2019`. */
  municipalityData?: string

  /** Deduplicate list of municipality names */
  dedupe?: boolean

  /** Reset the component when the user selects a municipality */
  resetOnSelect?: boolean

  /** Handler for when a municipality is selected by the user */
  onSelectionHandler: (municipality: Municipality) => void

  /** Locale to use */
  locale?: 'de' | 'fr'

  /** Placeholder text */
  placeholder?: string

  /** Show the search icon on the right side */
  iconOnRightSide?: boolean

  /** Max number of shown results */
  maxResults?: number

  /** Custom list of municipalities */
  customMunicipalities?: Municipality[]

  /** Currently selected municipality displayed inside the input field */
  selectedMunicipality?: Municipality

  /** The id of currently selected municipality displayed inside the input field */
  selectedMunicipalityId?: number

  /** Handler for when close button pressed */
  onCloseHandler?: () => void

  /** Input background color */
  inputBackgroundColor?: string

  /** Result background color */
  resultBackgroundColor?: string

  /** Property on your municipality you want to search for. The value of this key MUST be a string */
  propertyToSearch?: keyof Municipality
}

/** All localised strings for the component */
const MESSAGES: LocalisedMessagesList = { de, fr }

const STARTS_WITH_NUMBER = /^\d/
const CONTAINS_ONLY_LETTERS = /^\D+$/
const CONTAINS_FOUR_DIGITS = /\d{4}/

const API_URL =
  'https://interaktiv-mf.tagesanzeiger.ch/municipality-data/'
const LEGACY_API_URL =
  'https://interaktiv.tagesanzeiger.ch/static/gemeindesuche/'

class MunicipalityDownloadError extends Error {}

const translate = (
  messageList: LocalisedMessagesList,
  locale: string,
  key: string | null,
): string => {
  if (key == null) return ''
  return (
    key
      .split('.')
      .reduce(
        (messages: MessageListContent, prop: string) => messages[prop],
        messageList[locale],
      ) || 'TRANSLATED STRING NOT FOUND'
  ) as string
}

const muniToString = (muni: Municipality) =>
  `${muni.GDENR}${muni.ORTNAME ?? ''}${muni.PLZ4 ?? ''}`

export const MunicipalitySearch = ({
  municipalityData = '2021v3',
  locale = 'de',
  resultBackgroundColor = 'var(--site-background)',
  inputBackgroundColor = 'var(--site-background)',
  maxResults = 10,
  propertyToSearch = 'NORMGEMEINDE',
  dedupe = false,
  resetOnSelect = false,
  onSelectionHandler,
  placeholder,
  iconOnRightSide,
  customMunicipalities,
  selectedMunicipality,
  selectedMunicipalityId,
  onCloseHandler,
}: Props) => {
  const [results, setResults] = useState<Municipality[]>([])
  const [value, setValue] = useState('')
  const [error, setError] = useState<string | null>(null)
  const [municipalities, setMunicipalities] = useState<Municipality[]>([])

  const t = useCallback(
    (key: string | null) => translate(MESSAGES, locale, key),
    [locale],
  )

  useEffect(() => {
    setResults([])
    setValue('')
    setError(null)

    if (customMunicipalities) {
      setMunicipalities(customMunicipalities)
      return
    }

    let cancelled = false

    const fetchData = async (useLegacy = false) => {
      const baseUrl = useLegacy ? LEGACY_API_URL : API_URL
      try {
        const res = await fetch(`${baseUrl}${municipalityData}.json`)
        if (!res.ok) {
          throw new MunicipalityDownloadError(
            `Download error: ${res.status}: ${res.statusText}.`,
          )
        }
        const data = await res.json()
        if (!cancelled) setMunicipalities(data)
      } catch (err) {
        if (cancelled) return
        if (!useLegacy) return fetchData(true)
        if (err instanceof MunicipalityDownloadError) {
          console.error(err)
          console.info(
            'Make sure that you have the municipality and zip code data for the required year uploaded to the Interaktiv server.',
          )
          setError('error.municipalitiesNotDownloaded')
        } else {
          throw err
        }
      }
    }

    fetchData()
    return () => {
      cancelled = true
    }
  }, [municipalityData, customMunicipalities])

  const removeDuplicates = (arr: Municipality[], searchTerm: string) => {
    const map = new Map<string, Municipality>()
    for (const v of arr) {
      const key = dedupe
        ? `${v.ORTNAME ?? ''}${v.GDENR}`
        : `${v.PLZ4 ?? ''}${v.ORTNAME ?? ''}${v.GDENR}`
      map.set(key, v)
    }

    const exact: Municipality[] = []
    const rest: Municipality[] = []
    const lower = searchTerm.toLowerCase()

    for (const m of map.values()) {
      if (
        m.GDENAMK?.toLowerCase() === lower ||
        m.ORTNAME?.toLowerCase() === lower
      ) {
        exact.push(m)
      } else {
        rest.push(m)
      }
    }

    return [...exact, ...rest]
  }

  const filterList = (searchTerm: string) => {
    searchTerm = searchTerm.trim()

    if (searchTerm.length <= 1 || municipalities.length === 0) {
      return []
    }

    let filtered: Municipality[] = []

    const fourDigitMatch = CONTAINS_FOUR_DIGITS.exec(searchTerm)
    if (fourDigitMatch) {
      filtered = municipalities.filter(
        (m) => (m.PLZ4 ?? '').toString() === fourDigitMatch[0],
      )
    } else if (STARTS_WITH_NUMBER.test(searchTerm)) {
      filtered = municipalities.filter((m) =>
        RegExp(`^${searchTerm}`).test((m.PLZ4 ?? '').toString()),
      )
    } else if (CONTAINS_ONLY_LETTERS.test(searchTerm)) {
      const sanitized = searchTerm.replace('.', '\\.')
      const regex = RegExp(sanitized, 'i')
      const exactWordRegex = RegExp(`\\b${sanitized}\\b`, 'i')

      const scoreResult = (term: string, compare: string) => {
        if (compare === term) return 2
        if (exactWordRegex.test(compare)) return 1
        return 0
      }

      filtered = municipalities
        .filter((m) => regex.test(m[propertyToSearch]))
        .sort((a, b) => {
          const aScore = scoreResult(
            searchTerm.toLowerCase(),
            a[propertyToSearch].toLowerCase(),
          )
          const bScore = scoreResult(
            searchTerm.toLowerCase(),
            b[propertyToSearch].toLowerCase(),
          )
          return bScore - aScore
        })
        .slice(0, maxResults)
    }

    window.dataLayer = window.dataLayer || []
    window.dataLayer.push({
      event: 'Interactions',
      event_action: 'input',
      event_label: `search:for_${searchTerm}:results_${filtered.length}`,
    })

    return removeDuplicates(filtered, searchTerm)
  }

  const resetComponent = () => {
    setResults([])
    setValue('')
  }

  const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
    const inputValue = e.currentTarget.value
    setValue(inputValue)

    if (inputValue.length < 1) {
      return resetComponent()
    }

    setResults(filterList(inputValue))
  }

  const handleSelect = (municipality: Municipality) => {
    onSelectionHandler(municipality)
    if (resetOnSelect) resetComponent()
  }

  const preSelectedMunicipality =
    selectedMunicipality ??
    municipalities.find((m) => m.GDENR === selectedMunicipalityId)

  const hasResults = results.length > 0

  return (
    <MunicipalitySearchContainer className='municipality-search'>
      <InputRow className='inputRow'>
        <FlexInput
          type='text'
          $lessPaddingLeft={!!preSelectedMunicipality || iconOnRightSide}
          placeholder={placeholder || t('placeholder')}
          value={preSelectedMunicipality?.NORMGEMEINDE ?? value}
          disabled={
            error === 'error.municipalitiesNotDownloaded' ||
            !!preSelectedMunicipality
          }
          onChange={handleSearchChange}
          $backgroundColor={inputBackgroundColor}
        />
        {preSelectedMunicipality ? (
          <Icon
            onClick={() => onCloseHandler?.()}
            $isClickable
            id='search-closing-icon'
            $isRight
          >
            <svg
              width='20'
              height='21'
              viewBox='0 0 24 25'
              xmlns='http://www.w3.org/2000/svg'
              fill='currentColor'
              strokeWidth='2'
            >
              <path
                d='M3.9 22L2 20.1L9.6 12.5L2 4.9L3.9 3L11.5 10.6L19.1 3L21 4.9L13.4 12.5L21 20.1L19.1 22L11.5 14.4L3.9 22Z'
                fill='currentColor'
              />
            </svg>
          </Icon>
        ) : (
          <Icon $isRight={iconOnRightSide}>
            <svg
              width='18'
              height='18'
              viewBox='0 0 18 18'
              fill='none'
              xmlns='http://www.w3.org/2000/svg'
              stroke='currentColor'
              strokeWidth='2'
            >
              <circle cx='7.5' cy='7.5' r='6' />
              <line x1='11' y1='11' x2='17' y2='17' />
            </svg>
          </Icon>
        )}
      </InputRow>
      {hasResults && (
        <Results
          className='results'
          $backgroundColor={resultBackgroundColor}
        >
          <Transition
            items={results}
            keys={muniToString}
            from={{ transform: 'translate(0,-20px)', opacity: 0 }}
            enter={{ transform: 'translate(0,0px)', opacity: 1 }}
            config={springConfig.gentle}
          >
            {(style, result) => (
              <animated.div
                onClick={() => handleSelect(result)}
                onKeyUp={(e) => {
                  if (e.key === 'Enter' || e.key === 'Space') {
                    handleSelect(result)
                  }
                }}
                className='result'
                style={style}
                tabIndex={0}
              >
                <ResultHeader className='resultHeader'>
                  <ResultPlz className='resultPlz'>{result.PLZ4}</ResultPlz>{' '}
                  {result.ORTNAME}
                </ResultHeader>
                <ResultMeta className='resultMeta'>
                  {result.ORTNAME ? t('list.municipalityPrefix') : ''}{' '}
                  <b>
                    {result.GDENAMK ??
                      result.NORMGEMEINDE +
                        (results.find(
                          (m) =>
                            m !== result &&
                            m.NORMGEMEINDE === result.NORMGEMEINDE,
                        ) && result.KTKZ
                          ? ` (${result.KTKZ})`
                          : '')}
                  </b>
                </ResultMeta>
              </animated.div>
            )}
          </Transition>
        </Results>
      )}
    </MunicipalitySearchContainer>
  )
}

const Icon = styled.i<{ $isRight?: boolean; $isClickable?: boolean }>`
  cursor: ${(props) => (props.$isClickable ? 'pointer' : 'default')};
  position: absolute;
  top: 50%;
  ${(props) => (props.$isRight ? 'right: 0; padding-right: 0.8em;' : '')}
  padding-left: 0.8em;
  transform: translateY(-50%);
  display: flex;
  opacity: 0.5;
  transition: opacity 0.5s ease-in-out;
  animation: fromLeft 0.3s ease-in-out;
  @keyframes fromLeft {
    0% {
      transform: translateY(-50%) translateX(-10px);
      opacity: 0;
    }
    100% {
      transform: translateY(-50%) translateX(0);
    }
  }
  &#search-closing-icon {
    &:hover {
      opacity: 1;
    }
    animation: fromRight 0.3s ease-in-out;
    @keyframes fromRight {
      0% {
        transform: translateY(-50%) translateX(10px);
        opacity: 0;
      }
      100% {
        transform: translateY(-50%) translateX(0);
      }
    }
  }
`

const MunicipalitySearchContainer = styled.div`
  --border-radius: 0.3em;
  font-size: 18px;
  font-family: var(--font-plex);
  max-width: 300px;
  width: 100%;
  color: inherit;
  position: relative;

  @media screen and (max-width: 599px) {
    max-width: 100%;
  }
`

const InputRow = styled.div`
  position: relative;
  display: flex;
  flex-direction: row;
  align-items: center;

  input:focus ~ i {
    opacity: 1;
  }
`

const FlexInput = styled.input<{
  $lessPaddingLeft?: boolean
  $backgroundColor?: string
}>`
  color: inherit;
  background-color: ${({ $backgroundColor }) => $backgroundColor};
  border: solid 1px #7e7e7e7e;
  border-radius: var(--border-radius);
  box-shadow: none;
  font-size: 1em;
  outline: none;
  transition: all 0.3s ease-in-out;
  padding: 0.5em;
  padding-left: ${({ $lessPaddingLeft }) =>
    $lessPaddingLeft ? '1em' : '2.4em'};
  margin: 0;
  width: 100%;
  display: flex;

  &:disabled {
    opacity: 0.5;
  }

  &::placeholder {
    color: inherit;
    opacity: 0.5;
  }
`

const Results = styled.div<{ $backgroundColor?: string }>`
  margin-top: 0.25em;
  font-family: var(--font-plex);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  pointer-events: auto;
  position: absolute;
  background-color: ${({ $backgroundColor }) => $backgroundColor};
  width: 100%;
  border-radius: var(--border-radius);

  @media screen and (min-width: 599px) {
    max-width: 300px;
  }

  .result {
    max-width: 100%;
    color: inherit;
    padding: calc(11 / 16 * 1em) calc(14 / 16 * 1em);
    border-radius: var(--border-radius);
    box-shadow: 0 0 0 rgba(0, 0, 0, 0.3);
    line-height: 1.1em;
    cursor: pointer;
    transition: box-shadow 200ms ease-in-out;

    &:hover,
    &:focus {
      box-shadow:
        0 0 0.5ex rgba(0, 0, 0, 0.2),
        0 0 0 1px #007abf inset;
    }
  }
`

const ResultHeader = styled.div``

const ResultPlz = styled.span`
  font-weight: 400;
`

const ResultMeta = styled.div`
  font-size: 0.9em;
`
