import { Button } from '#amidala/main/atoms/button.js'
import { Input } from '#amidala/main/atoms/input.js'
import { Link } from '#amidala/main/atoms/link.js'
import getId from '#amidala/main/shared/get-id.js'
import h from '#amidala/main/shared/h.js'
import t from '#amidala/main/shared/t.js'
import { useLayoutEffect, useMemo, useRef, useState } from 'preact/hooks'
import { Section } from './section.js'

const LOAD_MORE_LIMIT = 12
const SEARCH_DEBOUNCE = 300
const SEARCH_PARAM = 'q'

export function FilteredSection (props) {
  return h(SearchableSection, { ...props, _hideSearch: true })
}

export function SearchableSection ({ _model, _hideSearch, _wrap, children = [], ...props }) {
  const { tags } = _model
  const [items, allLabels] = useMemo(() => getItemsAndLabels(children, _wrap), [children, _wrap])

  // Each filter is an object: { name, values: [] }
  const allFilters = useMemo(() => tags
    ?.filter?.(tag => allLabels[tag.name])
    ?.map?.(({ name }) => ({
      name,
      values: [...allLabels[name]].toSorted((a, b) => {
        if (isNumber(a) && isNumber(b)) {
          return Number.parseFloat(a) - Number.parseFloat(b)
        }
        return a.localeCompare(b)
      })
    })),
  [tags])

  const [limit, setLimit] = useState(LOAD_MORE_LIMIT)
  const [selection, setSelection] = useState({})
  const [search, setSearch] = useState(null)
  const [id] = useState(() => getId())

  const updateFiltersAndSearchFromUrl = () => {
    const params = new URLSearchParams(location.hash.substring(1))
    const selected = {}

    for (const [name, value] of params) {
      if (allLabels[name] && value) {
        selected[name] = value
      }
    }

    setSelection(selected)
    setSearch(params.get(SEARCH_PARAM))
    setLimit(LOAD_MORE_LIMIT)
  }

  useLayoutEffect(() => {
    updateFiltersAndSearchFromUrl()
    globalThis.addEventListener('hashchange', updateFiltersAndSearchFromUrl)
    return () => globalThis.removeEventListener('hashchange', updateFiltersAndSearchFromUrl)
  }, [])

  // Get filtered and limited children.
  const [filteredItems, limitedItems] = useMemo(() => {
    const fc = filterItems(selection, search, items)
    const lc = fc.slice(0, limit)
    return [fc, lc]
  }, [items, selection, search, limit])

  // Get filter instance.
  const filterInstance = useMemo(() => h(Filters, { _hideSearch, allFilters, selection, search, items, id }),
    [allFilters, selection, search, items])

  //
  // Resolver section combined children.
  //

  const combinedWrap = []
  const combinedChildren = []

  // Add filters.
  if (filterInstance) {
    combinedWrap.push({ colspan: 12, key: 'filters' })
    combinedChildren.push(filterInstance)
  }

  // Add items.
  for (const item of limitedItems) {
    combinedWrap.push(item.wrap)
    combinedChildren.push(h('div', { className: 'animate-fade h-full' }, item.child))
  }

  // Load more button.
  if (filteredItems.length > limit) {
    combinedWrap.push({ colspan: 12, key: 'load-more' })
    combinedChildren.push(h('div', { className: 'flex justify-center animate-fade' },
      h(Button, {
        'aria-controls': id,
        onClick: () => {
          setLimit(prev => prev + LOAD_MORE_LIMIT)
        },
        _model: { name: t('Load more') }
      })
    ))
  }

  // Add no results message.
  if (filteredItems.length === 0) {
    combinedWrap.push({ colspan: 12, key: 'no-results' })
    combinedChildren.push(h('p', { className: 'animate-fade' }, t('No results found.')))
  }

  return h(Section, {
    ...props,
    id,
    ariaLive: 'polite',
    _model: { ..._model, tags: null },
    _wrap: combinedWrap,
    children: combinedChildren
  })
}

function setFilterRoute (params) {
  const paramHash = params ? `#${params.toString()}` : ''
  globalThis.history.replaceState({}, '', `${globalThis.location.href.split('#')[0]}${paramHash}`)
  globalThis.dispatchEvent(new globalThis.HashChangeEvent('hashchange'))
}

function Filters ({ _hideSearch, allFilters, selection, search, items, id }) {
  const searchTimeoutRef = useRef(null)

  if (Object.keys(allFilters).length === 0 && _hideSearch) {
    return null
  }

  return h('div', { className: 'space-y-5' },
    h('div', { className: 'flex gap-5 sm:flex-col' },
      _hideSearch || h('label', { className: 'flex-1 block' },
        h('span', { className: 'font-bold' }, t('Search')),
        h(Input, {
          'aria-controls': id,
          type: 'search',
          name: t('Search'),
          value: search || '',
          onInput: event => {
            // Avoid flooding updates when user is typing.
            clearTimeout(searchTimeoutRef.current)
            searchTimeoutRef.current = setTimeout(() => {
              const query = event.target.value
              const params = new URLSearchParams(location.hash.substring(1))
              query ? params.set(SEARCH_PARAM, query) : params.delete(SEARCH_PARAM)
              setFilterRoute(params)
            }, SEARCH_DEBOUNCE)
          }
        })
      ),
      allFilters.map(({ name, values }) => h(Filter, {
        id,
        name,
        values,
        selectedValue: selection[name],
        selection,
        search,
        items,
        onChange: value => {
          const params = new URLSearchParams(location.hash.substring(1))
          value ? params.set(name, value) : params.delete(name)
          setFilterRoute(params)
        }
      }))
    ),
    // Reset filters.
    h(Link, {
      'aria-controls': id,
      _variant: 'underline',
      _model: {
        name: t('Reset filters'),
        value: '#'
      },
      role: 'button',
      onClick: e => {
        e.preventDefault()
        // Route to avoid scroll to top.
        setFilterRoute()
      }
    })
  )
}

function Filter ({ name, values, selectedValue, selection, search, items, onChange, id }) {
  // Remove current selection from the list to calculate counts.
  const adjustedSelection = { ...selection }
  delete adjustedSelection[name]

  // Get counts for the current filter based on partially filtered children
  const filteredItems = filterItems(adjustedSelection, search, items)
  const count = getCountForFilter(name, filteredItems)

  return h('label', { className: 'block flex-1' },
    h('span', { className: 'font-bold' }, name),
    h(Input, {
      'aria-controls': id,
      name,
      type: 'select',
      onChange: event => onChange(event.target.value),
      children: [
        h('option'),
        values.map(value => h('option', { value, selected: value === selectedValue },
          `${value} (${count[value] ?? 0})`
        ))
      ]
    })
  )
}

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

function getItemsAndLabels (children, _wrap) {
  const items = []
  const labels = {}

  for (const [ii, child] of children.entries()) {
    const model = child.props._model
    const wrap = { colspan: _wrap[ii]?.colspan }
    items.push({ child, model, wrap })

    for (const tag of model.tags) {
      labels[tag.name] ||= new Set()
      labels[tag.name].add(tag.value)
    }
  }

  return [items, labels]
}

function filterItems (selection, search, items) {
  const itemMatchesSelection = item => Object.entries(selection).every(([name, value]) => {
    const tags = item.model.tags
    return tags.some(tag => tag.name === name && tag.value === value)
  })

  // Skip filtering if no criteria.
  if (Object.keys(selection).length === 0 && !search) {
    return items
  }

  const filteredItems = []

  for (const item of items) {
    // Match search.
    if (search) {
      const searchIndex = item.model.tags.map(tag => tag.value.toLowerCase())

      const title = item.model.title?.toLowerCase()
      title && searchIndex.push(title)
      const text = item.model.text?.toLowerCase()
      text && searchIndex.push(text)

      if (!searchIndex.some(s => s.includes(search.trim().toLowerCase()))) {
        continue
      }
    }

    // Match selection.
    if (!itemMatchesSelection(item)) {
      continue
    }

    filteredItems.push(item)
  }

  return filteredItems
}

function getCountForFilter (name, items) {
  const count = {}

  for (const item of items) {
    for (const tag of item.model.tags) {
      if (name === tag.name) {
        count[tag.value] ||= 0
        count[tag.value] += 1
      }
    }
  }

  return count
}

function isNumber (value) {
  return !Number.isNaN(Number.parseFloat(value))
}
