import {
  Box,
  Button,
  Checkbox,
  Divider,
  Group,
  Input,
  Popover,
  PopoverProps,
  Text,
  Flex,
  BoxProps,
  TextProps
} from '@tidbcloud/uikit'
import { useDebouncedValue } from '@tidbcloud/uikit/hooks'
import { IconChevronDown, IconChevronRight, IconChevronSelectorVertical } from '@tidbcloud/uikit/icons'
import { useEffect, useMemo, useState } from 'react'

type SelectionProtectType = string | number

export interface TreeSelectOption<T extends SelectionProtectType = string> {
  label: string
  value: T
  disabled?: boolean
  isChecked?: boolean
  isLeaf?: boolean
  showChildren?: boolean
  children?: TreeSelectOption<T>[]
  renderSelectItem?: (options: TreeSelectOption<T>) => React.ReactNode
}

interface TreeSelectSearchProps {
  placeholder?: string
  emptyMessage?: string
  inputTestId?: string
  debounceMs?: number
}

export interface TreeSelectProps<T extends SelectionProtectType = string> {
  value?: T[]
  options: TreeSelectOption<T>[]
  onChange: (value: T[]) => void
  disabled?: boolean
  popoverProps?: Omit<PopoverProps, 'children'>
  invalid?: boolean
  placeholder?: string
  renderSelectItem?: (options: TreeSelectOption<T>) => React.ReactNode
  allWithEmpty?: boolean
  emptyMessage?: string
  selectItemProps?: SelectItemWrapperProps
  maxContentHeight?: number
  isShouldParentSelectable?: boolean
  searchable?: boolean
  searchProps?: TreeSelectSearchProps
}

const checkAll = <T extends SelectionProtectType = string>(
  options: TreeSelectOption<T>[],
  isChecked: boolean
): TreeSelectOption<T>[] => {
  return options.map((v) => ({
    ...v,
    // skip disabled
    isChecked: v.disabled ? false : isChecked,
    indeterminate: false,
    children: v.children ? checkAll(v.children, isChecked) : undefined
  }))
}
const checkOptionsByValue = <T extends SelectionProtectType = string>(
  options: TreeSelectOption<T>[],
  value: T[]
): TreeSelectOption<T>[] => {
  // [] means check all
  return options.map((op) => {
    if (op.children?.length) {
      const checkedChildren = checkOptionsByValue(op.children, value)

      const allChildrenChecked = checkedChildren.every((child) => child.isChecked)
      const someChildrenChecked = checkedChildren.some((child) => child.isChecked)

      return {
        ...op,
        children: checkedChildren,
        isChecked: allChildrenChecked,
        indeterminate: someChildrenChecked && !allChildrenChecked
      }
    } else {
      return {
        ...op,
        isChecked: value.includes(op.value),
        indeterminate: false
      }
    }
  })
}
const flatAllLeaf = <T extends SelectionProtectType = string>(
  value: TreeSelectOption<T>[],
  isShouldParentSelectable?: boolean
) =>
  value.reduce((prev, cur) => {
    if (cur.isLeaf && !cur.disabled) {
      prev.push(cur)
    }
    if (cur.children) {
      if (cur.children.every((c) => c.isChecked) && isShouldParentSelectable) {
        cur.isChecked = true
        prev.push(cur)
      }

      prev.push(...flatAllLeaf(cur.children, isShouldParentSelectable))
    }
    return prev
  }, [] as TreeSelectOption<T>[])

const flatAllNodes = <T extends SelectionProtectType = string>(value: TreeSelectOption<T>[]) =>
  value.reduce((prev, cur) => {
    if (!cur.disabled) {
      prev.push(cur)
    }

    if (cur.children) {
      prev.push(...flatAllNodes(cur.children))
    }

    return prev
  }, [] as TreeSelectOption<T>[])

const isOptionMatched = <T extends SelectionProtectType = string>(option: TreeSelectOption<T>, keyword: string) => {
  if (!keyword) {
    return true
  }

  if (option.label.toLowerCase().includes(keyword)) {
    return true
  }

  return option.children?.some((child) => isOptionMatched(child, keyword)) ?? false
}

export const TreeSelect = <T extends SelectionProtectType = string>({
  options,
  value,
  disabled,
  onChange,
  popoverProps,
  invalid,
  renderSelectItem,
  placeholder,
  allWithEmpty = true,
  emptyMessage = 'No data',
  selectItemProps,
  maxContentHeight,
  isShouldParentSelectable = false,
  searchable = false,
  searchProps
}: TreeSelectProps<T>) => {
  const [opened, setOpened] = useState(false)
  const [searchValue, setSearchValue] = useState('')
  const [_options, setOptions] = useState<TreeSelectOption<T>[] | undefined>(undefined)
  const internalOptions = useMemo(() => {
    if (!!_options) {
      return _options
    }
    return options
  }, [options, _options])
  const flatOptions = useMemo(() => flatAllLeaf(internalOptions), [internalOptions])
  const flatNodes = useMemo(() => flatAllNodes(internalOptions), [internalOptions])
  const itemCount = isShouldParentSelectable ? flatNodes.length : flatOptions.length
  const isCheckAll = value && (!value.length || value.length === itemCount)
  const selectedTips =
    value === undefined ? '' : isCheckAll ? `All (${itemCount})` : `Checked ${value.length} (Total ${itemCount})`
  const everyInternalChecked = flatOptions.every((o) => o.isChecked)
  const someInternalChecked = flatOptions.some((o) => o.isChecked)
  const everInternalNotChecked = flatOptions.every((o) => !o.isChecked)
  const searchPlaceholder = searchProps?.placeholder ?? 'Search'
  const searchEmptyMessage = searchProps?.emptyMessage ?? 'No matches found'
  const searchInputTestId = searchProps?.inputTestId
  const searchDebounceMs = searchProps?.debounceMs ?? 200
  const [debouncedSearchValue] = useDebouncedValue(searchValue, searchDebounceMs)
  const normalizedSearchValue = (searchValue ? debouncedSearchValue : '').trim().toLowerCase()
  const hasMatchedOptions = internalOptions.some((option) => isOptionMatched(option, normalizedSearchValue))

  const resetCheckedStatus = () => setOptions(checkOptionsByValue(internalOptions, value || []))

  useEffect(() => {
    // value could be undefined
    setOptions(checkOptionsByValue(options, value || []))
  }, [options, value])

  return (
    <Popover
      position="bottom-end"
      {...popoverProps}
      opened={opened}
      onChange={setOpened}
      shadow="sm"
      onClose={() => {
        resetCheckedStatus()
        setSearchValue('')
      }}
    >
      <Popover.Target>
        <Input
          styles={{
            input: {
              userSelect: 'none',
              height: '40px',
              lineHeight: '38px',
              paddingRight: '40px',
              ':hover': {
                cursor: disabled ? 'not-allowed' : 'pointer'
              },
              '::selection': {
                backgroundColor: 'transparent'
              }
            },
            section: {
              width: '40px',
              pointerEvents: 'none'
            }
          }}
          sx={{
            lineHeight: '1.55'
          }}
          value={selectedTips}
          disabled={disabled}
          placeholder={placeholder}
          readOnly
          rightSection={<IconChevronSelectorVertical size={16} color="var(--mantine-color-carbon-7)!important" />}
          onClick={() => setOpened((v) => !v)}
          data-invalid={invalid}
        />
      </Popover.Target>
      <Popover.Dropdown p={0}>
        {searchable && internalOptions.length ? (
          <>
            <Box p="sm">
              <Input
                value={searchValue}
                placeholder={searchPlaceholder}
                onChange={(e) => setSearchValue(e.currentTarget.value)}
                data-testid={searchInputTestId}
              />
            </Box>
            <Divider color="carbon.3" />
          </>
        ) : null}
        {internalOptions.length ? (
          <SelectItem
            label={selectedTips}
            value="all"
            isLeaf={true}
            isChecked={everyInternalChecked}
            indeterminate={someInternalChecked && !everyInternalChecked}
            onChange={(v) => {
              setOptions(checkAll(internalOptions, v))
            }}
            {...selectItemProps}
          />
        ) : (
          <Flex my={16} justify="center">
            {emptyMessage}
          </Flex>
        )}
        <Divider color="carbon.3" />
        <Box mah={maxContentHeight ?? 240} sx={{ overflow: 'auto' }}>
          {internalOptions.length ? (
            hasMatchedOptions ? (
              <SelectItems<T>
                value={internalOptions}
                renderSelectItem={renderSelectItem}
                onChange={setOptions}
                searchKeyword={normalizedSearchValue}
                {...selectItemProps}
              />
            ) : (
              <Flex my={16} justify="center">
                {searchable && normalizedSearchValue ? searchEmptyMessage : emptyMessage}
              </Flex>
            )
          ) : null}
        </Box>
        <Divider color="carbon.3" />
        <Group p="md" gap={8} justify="flex-end">
          <Button
            variant="default"
            size="xs"
            onClick={() => {
              resetCheckedStatus()
              setOpened(false)
            }}
          >
            Cancel
          </Button>
          <Button
            size="xs"
            onClick={() => {
              const flatOptions = flatAllLeaf(internalOptions, isShouldParentSelectable)
              const isCheckAll = flatOptions.every((n) => n.isChecked)
              onChange(isCheckAll && allWithEmpty ? [] : flatOptions.filter((n) => n.isChecked).map((n) => n.value))
              setOpened(false)
            }}
            disabled={everInternalNotChecked}
          >
            Apply
          </Button>
        </Group>
      </Popover.Dropdown>
    </Popover>
  )
}

interface SelectItemsProps<T extends SelectionProtectType = string> {
  value: TreeSelectOption<T>[]
  onChange: (opts: TreeSelectOption<T>[]) => void
  level?: number
  renderSelectItem?: (options: TreeSelectOption<T>) => React.ReactNode
  searchKeyword?: string
}

const SelectItems = <T extends SelectionProtectType = string>({
  value,
  level = 0,
  onChange,
  searchKeyword,
  ...rest
}: SelectItemsProps<T>) => {
  return (
    <>
      {value.map((v, i) => {
        if (searchKeyword && !isOptionMatched(v, searchKeyword)) {
          return null
        }

        return (
          <SelectItemWithChildren<T>
            key={v.value}
            value={v}
            level={level}
            onChange={(v) => onChange([...value.slice(0, i), v, ...value.slice(i + 1)])}
            onChildrenChange={(opts) => {
              const childChange = { ...v, children: opts }
              onChange([...value.slice(0, i), childChange, ...value.slice(i + 1)])
            }}
            searchKeyword={searchKeyword}
            {...rest}
          />
        )
      })}
    </>
  )
}

interface SelectItemWithChildrenProps<T extends SelectionProtectType = string> {
  value: TreeSelectOption<T>
  onChange: (v: TreeSelectOption<T>) => void
  onChildrenChange: (opts: TreeSelectOption<T>[]) => void
  level?: number
  renderSelectItem?: (options: SelectItemProps<T>) => React.ReactNode
  selectItemProps?: SelectItemWrapperProps
  searchKeyword?: string
}

const SelectItemWithChildren = <T extends SelectionProtectType = string>({
  value,
  level = 0,
  onChange,
  onChildrenChange,
  renderSelectItem,
  searchKeyword,
  ...rest
}: SelectItemWithChildrenProps<T>) => {
  const someChildrenChecked = flatAllLeaf(value.children || []).some((c) => c.isChecked)
  const everyChildrenChecked = flatAllLeaf(value.children || []).every((c) => c.isChecked)
  const shouldShowChildren = !!searchKeyword || value.showChildren
  return (
    <>
      <SelectItem<T>
        {...value}
        level={level}
        showChildren={shouldShowChildren}
        setShowChildren={(v) => onChange({ ...value, showChildren: v })}
        someChildrenChecked={someChildrenChecked}
        everyChildrenChecked={everyChildrenChecked}
        onChange={(v) => {
          onChange({
            ...value,
            isChecked: v,
            children: value.isLeaf ? value.children : checkAll(value.children!, v)
          })
        }}
        renderSelectItem={renderSelectItem}
        {...rest}
      />
      {value.children && shouldShowChildren && (
        <SelectItems<T>
          value={value.children}
          level={level + 1}
          onChange={onChildrenChange}
          renderSelectItem={renderSelectItem}
          searchKeyword={searchKeyword}
          {...rest}
        />
      )}
    </>
  )
}

interface SelectItemWrapperProps extends Omit<BoxProps, 'children'> {
  textProps?: TextProps
}

interface SelectItemProps<T extends SelectionProtectType = string> extends TreeSelectOption<T>, SelectItemWrapperProps {
  onChange: (v: boolean) => void
  indeterminate?: boolean
  level?: number
  showChildren?: boolean
  setShowChildren?: (v: boolean) => void
  someChildrenChecked?: boolean
  everyChildrenChecked?: boolean
}

const SelectItem = <T extends SelectionProtectType = string>({
  label,
  indeterminate,
  level = 0,
  isLeaf,
  someChildrenChecked,
  everyChildrenChecked,
  showChildren,
  setShowChildren,
  isChecked,
  onChange,
  disabled,
  renderSelectItem,
  value,
  ...rest
}: SelectItemProps<T>) => {
  const { textProps, ...wrapperProps } = rest
  return (
    <Box pl={8 + 24 * level} {...wrapperProps}>
      <Group
        p="sm"
        gap={8}
        onClick={() => {
          isLeaf ? onChange(!isChecked) : setShowChildren?.(!showChildren)
        }}
        sx={{ cursor: 'pointer', userSelect: 'none' }}
      >
        <Checkbox
          indeterminate={(!isLeaf && someChildrenChecked && !everyChildrenChecked) || indeterminate}
          checked={isLeaf ? isChecked : everyChildrenChecked}
          onClick={(e) => {
            e.stopPropagation()
            onChange(isLeaf ? !isChecked : !everyChildrenChecked!)
          }}
          disabled={disabled}
          readOnly
        />
        {!isLeaf && (showChildren ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />)}
        {!!renderSelectItem ? (
          renderSelectItem({ label, value, disabled })
        ) : (
          <Text truncate maw="80%" {...textProps}>
            {label}
          </Text>
        )}
      </Group>
    </Box>
  )
}
