import { CalendarIcon, TrashIcon } from '@radix-ui/react-icons'
import { useQuery } from '@tanstack/react-query'
import {
  DateFilter,
  FilterConfig,
  FilterFieldName,
  FilterFieldType,
  FilterState,
  FilterValue,
  NumberFilter,
} from '@types'

import { FilterX } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'

import { cn } from '@lib/utils'
import { Button, Calendar, Checkbox, Input, Popover, PopoverContent, PopoverTrigger } from '@postxl/ui-components'

export const TableFilter = <TField extends FilterFieldName, TFilters extends FilterState>({
  open = false,
  field,
  filters,
  onChange,
  config,
  getFilterOptions,
}: {
  open?: boolean
  field: TField
  filters: TFilters
  onChange: (val: FilterValue) => void
  config: FilterConfig<TField, TFilters>
  getFilterOptions: (args: { field: TField; filters: TFilters }) => any
}) => {
  const fieldConfig = config[field]
  const { filterKey, valueType: fieldType } = fieldConfig

  // We exclude the current field from the filters passed to the query.
  // This prevents the options from refetching (and resetting scroll position) when the user selects an item within the same filter.
  const queryFilters = useMemo(() => {
    const key = filterKey
    if (!key) {
      return filters
    }

    // For number filters, we want to include the min/max values in the query so that the backend can gray out options outside the range.
    // However, we still want to exclude the selected values (checkboxes) to avoid filtering the options list by itself.
    if (fieldType === 'number') {
      const val = filters[key] as any
      if (val && (val.min !== undefined || val.max !== undefined)) {
        return {
          ...filters,
          [key]: { min: val.min, max: val.max }, // Pass only min/max, exclude values
        }
      }
    }
    if (fieldType === 'date') {
      const val = filters[key] as DateFilter | undefined
      if (val && (val.start !== undefined || val.end !== undefined)) {
        return {
          ...filters,
          [key]: { start: val.start, end: val.end }, // Pass only start/end, exclude values
        }
      }
    }

    const { [key]: _ignored, ...rest } = filters
    return rest as TFilters
  }, [filterKey, filters, fieldType])

  const { data: optionsData } = useQuery({
    ...getFilterOptions({ field, filters: queryFilters }),
    enabled: open,
  })

  const options = useMemo(() => (optionsData && Array.isArray(optionsData) ? optionsData : []), [optionsData])

  const [searchQuery, setSearchQuery] = useState('')

  const filteredOptions = useMemo(() => {
    if (!searchQuery) {
      return options
    }
    const lowerQuery = searchQuery.toLowerCase()
    return options.filter((option: any) => option.label.toLowerCase().includes(lowerQuery))
  }, [options, searchQuery])

  const value = useMemo(() => {
    const key = fieldConfig.filterKey
    const val = filters[key]
    if (!val) {
      return []
    }
    if (fieldType === 'number') {
      return ((val as NumberFilter).values || []).map(String)
    }
    if (fieldType === 'date') {
      return (val as DateFilter).values || []
    }
    if (fieldType === 'boolean') {
      return (val as boolean[]).map(String)
    }
    return val as string[]
  }, [fieldConfig.filterKey, fieldType, filters])

  const selectedValues = new Set(value)
  const areAllSelected = options.length > 0 && options.every((option) => selectedValues.has(option.value))

  const areAllFilteredSelected =
    filteredOptions.length > 0 && filteredOptions.every((option) => selectedValues.has(option.value))
  const isAnyFilteredSelected = filteredOptions.some((option) => selectedValues.has(option.value))

  // Min/Max state for number filters
  const [minVal, setMinVal] = useState('')
  const [maxVal, setMaxVal] = useState('')

  useEffect(() => {
    if (open && fieldType === 'number') {
      const key = fieldConfig.filterKey
      const val = filters[key] as NumberFilter | undefined
      setMinVal(val?.min?.toString() ?? '')
      setMaxVal(val?.max?.toString() ?? '')
    }
    if (open && fieldType === 'date') {
      const key = fieldConfig.filterKey
      const val = filters[key] as DateFilter | undefined
      setMinVal(val?.start ?? '')
      setMaxVal(val?.end ?? '')
    }
  }, [open, filters, fieldType, fieldConfig.filterKey])

  const handleNumberChange = (newValues: string[], newMin: string, newMax: string) => {
    const min = newMin === '' ? null : Number(newMin)
    const max = newMax === '' ? null : Number(newMax)
    const values = newValues.length > 0 ? newValues.map((v) => (v === '' ? '' : Number(v))) : undefined

    if (!values && min === null && max === null) {
      onChange(undefined)
    } else {
      onChange({ values, min, max })
    }
  }
  const handleDateChange = (newValues: string[], newStart: string, newEnd: string) => {
    const start = newStart === '' ? null : newStart
    const end = newEnd === '' ? null : newEnd
    const values = newValues.length > 0 ? newValues.map((v) => (v === '' ? '' : v)) : undefined

    if (!values && start === null && end === null) {
      onChange(undefined)
    } else {
      onChange({ values, start, end })
    }
  }

  const commitMinMax = () => {
    let nextMin = minVal
    let nextMax = maxVal
    if (
      minVal !== '' &&
      maxVal !== '' &&
      ((fieldType === 'number' && Number(minVal) > Number(maxVal)) || (fieldType === 'date' && minVal > maxVal))
    ) {
      nextMin = maxVal
      nextMax = minVal
      setMinVal(nextMin)
      setMaxVal(nextMax)
    }
    if (fieldType === 'number') {
      handleNumberChange(Array.from(selectedValues).map(String), nextMin, nextMax)
    } else if (fieldType === 'date') {
      handleDateChange(Array.from(selectedValues).map(String), nextMin, nextMax)
    }
  }

  return (
    <div className="w-full flex flex-col">
      {/* Clear filter button */}
      <Button
        variant="ghost"
        size="xs"
        disabled={selectedValues.size === 0 && minVal === '' && maxVal === ''}
        onClick={() => {
          if (fieldType === 'number' || fieldType === 'date') {
            setMinVal('')
            setMaxVal('')
            if (fieldType === 'number') {
              handleNumberChange([], '', '')
            } else if (fieldType === 'date') {
              handleDateChange([], '', '')
            }
          } else {
            onChange([])
          }
        }}
        className="w-full justify-start px-2 rounded-sm text-sm h-7 font-normal"
      >
        <FilterX className="text-muted-foreground" />
        Clear Filter
      </Button>

      {/* Min/Max inputs for number fields */}
      {fieldType === 'number' && (
        <>
          <NumberMinMaxInput
            value={minVal}
            setValue={setMinVal}
            label="At least"
            placeholder="min"
            onCommit={commitMinMax}
          />
          <NumberMinMaxInput
            value={maxVal}
            setValue={setMaxVal}
            label="At most"
            placeholder="max"
            onCommit={commitMinMax}
          />
        </>
      )}

      {/* From/to inputs for date fields */}
      {fieldType === 'date' && (
        <>
          <DateMinMaxInput
            value={minVal}
            setValue={setMinVal}
            label="From"
            onDelete={() => {
              setMinVal('')
              handleDateChange(Array.from(selectedValues).map(String), '', maxVal)
            }}
            onCommit={commitMinMax}
          />
          <DateMinMaxInput
            value={maxVal}
            setValue={setMaxVal}
            label="To"
            onDelete={() => {
              setMaxVal('')
              handleDateChange(Array.from(selectedValues).map(String), minVal, '')
            }}
            onCommit={commitMinMax}
            setTime={(date: Date) => {
              date.setHours(23, 59, 59, 999)
            }}
          />
        </>
      )}

      {/* Search input */}
      <Input
        autoFocus
        className="border-none shadow-none rounded-sm text-sm py-0 h-6.5"
        variant="simple"
        placeholder="Search..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
      />

      <div className="ps-1.5 pe-3 h-1">
        <div className="w-full border-b border-b-border/70" />
      </div>

      {/* Select all / all search results button */}
      <Button
        variant="ghost"
        size="xs"
        className="w-full justify-start px-2 py-1 rounded-sm text-sm font-normal"
        onClick={() => {
          let newVals: string[] = []
          if (searchQuery.length > 0) {
            const newSelected = new Set(selectedValues)
            if (areAllFilteredSelected) {
              filteredOptions.forEach((o) => newSelected.delete(o.value))
            } else {
              filteredOptions.forEach((o) => newSelected.add(o.value))
            }
            newVals = Array.from(newSelected)
          } else if (areAllSelected) {
            newVals = []
          } else {
            newVals = options.map((o) => o.value)
          }

          if (fieldType === 'number') {
            handleNumberChange(newVals, minVal, maxVal)
          } else if (fieldType === 'date') {
            handleDateChange(newVals, minVal, maxVal)
          } else {
            onChange(newVals)
          }
        }}
      >
        {searchQuery.length > 0 ? (
          <Checkbox
            readOnly
            checked={isAnyFilteredSelected}
            disabled={filteredOptions.length === 0}
            label="Select Search Results"
            className="pointer-events-none"
            checkboxSize="sm"
            variant={areAllFilteredSelected ? 'simple' : 'default'}
            iconStyle={areAllFilteredSelected ? 'simple' : 'solo'}
            checkIcon={areAllFilteredSelected ? 'check' : 'square'}
          />
        ) : (
          <Checkbox
            readOnly
            checked={selectedValues.size > 0}
            disabled={options.length === 0}
            label="Select All"
            className="pointer-events-none"
            checkboxSize="sm"
            variant={areAllSelected ? 'simple' : 'default'}
            iconStyle={areAllSelected ? 'simple' : 'solo'}
            checkIcon={areAllSelected ? 'check' : 'square'}
          />
        )}
      </Button>

      {/* Options list */}
      {filteredOptions.length === 0 ? (
        <div className="py-6 text-center text-sm">No results found.</div>
      ) : (
        <div className="px-2 py-1 max-h-[200px] overflow-auto">
          {filteredOptions.map((option) => (
            <FilterItem
              key={option.value}
              option={option}
              isSelected={selectedValues.has(option.value)}
              fieldType={fieldType}
              selectedValues={selectedValues}
              minVal={minVal}
              maxVal={maxVal}
              handleNumberChange={handleNumberChange}
              handleDateChange={handleDateChange}
              onChange={onChange}
            />
          ))}
        </div>
      )}
    </div>
  )
}

const NumberMinMaxInput = ({
  value,
  setValue,
  label,
  placeholder,
  onCommit,
}: {
  value: string
  setValue: (val: string) => void
  label: string
  placeholder: string
  onCommit: () => void
}) => {
  return (
    <div className="flex items-center gap-2 px-2 pt-0.5">
      <span className="whitespace-nowrap w-16">{label}</span>
      <Input
        type="number"
        variant="simple"
        className="py-0 h-5.5 w-full text-center text-sm focus:text-start placeholder:text-center rounded italic not-hover:not-focus:border-transparent not-hover:not-focus:shadow-none hover:bg-accent/30"
        placeholder={placeholder}
        value={value}
        onChange={(e) => setValue(e.target.value)}
        onBlur={onCommit}
        onEnter={onCommit}
      />
    </div>
  )
}

// Helper to convert ISO string to only YYYY-MM-DD for min/max date fields
const isoToDateInput = (iso: string | null | undefined) => {
  if (!iso) {
    return ''
  }
  if (iso.length === 10) {
    return iso
  }
  const date = new Date(iso)
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}

const DateMinMaxInput = ({
  value,
  setValue,
  label,
  onCommit,
  onDelete,
  setTime, // to use last ms of the day for 'To' field
}: {
  value: string
  setValue: (val: string) => void
  label: string
  onCommit: () => void
  onDelete: () => void
  setTime?: (date: Date) => void
}) => {
  return (
    <div className="flex items-center gap-2 px-2 pt-0.5">
      <span className="whitespace-nowrap w-12">{label}</span>
      <div className="relative w-full group">
        <Input
          type="date"
          variant="simple"
          className={cn(
            'no-date-icon py-0 h-5.5 ps-8 w-full text-primary rounded italic text-sm not-group-hover:not-focus:border-transparent not-group-hover:not-focus:shadow-none group-hover:bg-accent/30',
            value ? 'text-foreground' : 'text-muted-foreground',
          )}
          value={isoToDateInput(value)}
          onChange={(e) => {
            const date = new Date(e.target.value)
            setTime?.(date)
            setValue(date.toISOString())
          }}
          onBlur={onCommit}
          onEnter={onCommit}
        />
        <Popover
          onOpenChange={(open) => {
            if (!open) {
              onCommit()
            }
          }}
        >
          <PopoverTrigger asChild>
            <Button
              variant="extraGhost2"
              size="iconSm"
              className="absolute right-1 top-1/2 transform -translate-y-1/2 size-6 rounded"
            >
              <CalendarIcon />
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-auto p-0" align="start">
            <Calendar
              showYearNavigation={true}
              autoFocus
              captionLayout="dropdown"
              mode="single"
              selected={new Date(value)}
              onSelect={(date) => {
                if (!date) {
                  setValue('')
                  return
                }
                setTime?.(date)
                setValue(date?.toISOString() ?? '')
              }}
              defaultMonth={Number.isNaN(new Date(value).getTime()) ? undefined : new Date(value)}
            />
          </PopoverContent>
        </Popover>
        {value && (
          <Button
            variant="extraGhost2"
            size="iconSm"
            className="absolute right-7 top-1/2 transform -translate-y-1/2 size-6 rounded opacity-40"
            onClick={onDelete}
          >
            <TrashIcon />
          </Button>
        )}
      </div>
    </div>
  )
}

const FilterItem = ({
  option,
  isSelected,
  fieldType,
  selectedValues,
  minVal,
  maxVal,
  handleNumberChange,
  handleDateChange,
  onChange,
}: {
  option: { label: string; value: string; hasMatches: boolean }
  isSelected: boolean
  fieldType: FilterFieldType
  selectedValues: Set<string>
  minVal?: string
  maxVal?: string
  handleNumberChange: (values: string[], min: string, max: string) => void
  handleDateChange: (values: string[], from: string, to: string) => void
  onChange: (values: string[]) => void
}) => {
  let displayLabel = option.label

  if (option.value !== '') {
    if (fieldType === 'date') {
      displayLabel = new Date(option.label).toLocaleDateString()
    } else if (fieldType === 'number') {
      displayLabel = Number(option.label).toLocaleString()
    }
  }

  return (
    <Checkbox
      key={option.value}
      checked={isSelected}
      onChange={(e) => {
        const newSelected = new Set(selectedValues)
        if (e.target.checked) {
          newSelected.add(option.value)
        } else {
          newSelected.delete(option.value)
        }

        if (fieldType === 'number' && minVal !== undefined && maxVal !== undefined) {
          handleNumberChange(Array.from(newSelected).map(String), minVal, maxVal)
        } else if (fieldType === 'date' && minVal !== undefined && maxVal !== undefined) {
          handleDateChange(Array.from(newSelected).map(String), minVal, maxVal)
        } else {
          onChange(Array.from(newSelected))
        }
      }}
      checkIcon="check"
      checkboxSize="sm"
      className={cn(
        'whitespace-nowrap py-px text-sm',
        !option.hasMatches && 'opacity-50',
        option.value === '' && 'italic',
      )}
      label={displayLabel}
    />
  )
}
