import { useState, useEffect, useContext, useMemo, useCallback } from 'react'
import { Spin, Tree, Flex, Checkbox, Typography } from 'antd'
import PropTypes from 'prop-types'
import dayjs from 'dayjs'
import { ApiServerContext } from '../context/ApiServerContext'
import { LoadingOutlined, CaretDownOutlined } from '@ant-design/icons'

const { Text } = Typography

const MONTH_NAMES = Array.from({ length: 12 }, (_, i) =>
  dayjs().month(i).format('MMMM')
)

const pad2 = (n) => String(n).padStart(2, '0')

const nodeKey = {
  year: (y) => `${y}`,
  month: (y, m) => `${y}-${pad2(m)}`,
  day: (y, m, d) => `${y}-${pad2(m)}-${pad2(d)}`,
  hour: (y, m, d, h) => `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}`,
  minute: (y, m, d, h, min) =>
    `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}:${pad2(min)}`,
  second: (y, m, d, h, min, s) =>
    `${y}-${pad2(m)}-${pad2(d)}T${pad2(h)}:${pad2(min)}:${pad2(s)}`
}

const lastDayOfMonth = (year, month) => new Date(year, month, 0).getDate()

const toYearExpression = (year) => `1 1 ${year}..31 12 ${year}`

const toMonthExpression = (year, month) => {
  const last = lastDayOfMonth(year, month)
  return `1 ${month} ${year}..${last} ${month} ${year}`
}

const toDayExpression = (year, month, day) => `${day} ${month} ${year}`

const toHourExpression = (year, month, day, hour) =>
  `${day} ${month} ${year} ${hour}`

const toMinuteExpression = (year, month, day, hour, minute) =>
  `${day} ${month} ${year} ${hour} ${minute}`

const toSecondExpression = (year, month, day, hour, minute, second) =>
  `${day} ${month} ${year} ${hour} ${minute} ${second}`

const parseDateValue = (value) => {
  if (value == null || value === '') return null
  const date = value instanceof Date ? value : new Date(value)
  return Number.isNaN(date.getTime()) ? null : date
}

const hasNonMidnightTime = (date) =>
  date.getHours() !== 0 ||
  date.getMinutes() !== 0 ||
  date.getSeconds() !== 0 ||
  date.getMilliseconds() !== 0

/** Nested map: year → month → day → hour → minute → Set(second) */
const buildDateHierarchy = (dates) => {
  const root = new Map()

  for (const date of dates) {
    const y = date.getFullYear()
    const m = date.getMonth() + 1
    const d = date.getDate()
    const h = date.getHours()
    const min = date.getMinutes()
    const s = date.getSeconds()

    if (!root.has(y)) root.set(y, new Map())
    const months = root.get(y)
    if (!months.has(m)) months.set(m, new Map())
    const days = months.get(m)
    if (!days.has(d)) days.set(d, { times: new Map(), dates: [] })
    const dayNode = days.get(d)
    dayNode.dates.push(date)

    if (!dayNode.times.has(h)) dayNode.times.set(h, new Map())
    const minutes = dayNode.times.get(h)
    if (!minutes.has(min)) minutes.set(min, new Set())
    minutes.get(min).add(s)
  }

  return root
}

const dayNeedsTimeLevels = (dayNode) => {
  const { dates, times } = dayNode
  if (dates.some(hasNonMidnightTime)) return true
  let distinct = 0
  for (const minutes of times.values()) {
    for (const seconds of minutes.values()) {
      distinct += seconds.size
      if (distinct > 1) return true
    }
  }
  return false
}

const sortNumericKeys = (map) => [...map.keys()].sort((a, b) => a - b)

const buildTreeData = (hierarchy) => {
  const years = sortNumericKeys(hierarchy)

  return years.map((year) => {
    const monthsMap = hierarchy.get(year)
    const monthNodes = sortNumericKeys(monthsMap).map((month) => {
      const daysMap = monthsMap.get(month)
      const dayNodes = sortNumericKeys(daysMap).map((day) => {
        const dayNode = daysMap.get(day)
        const includeTime = dayNeedsTimeLevels(dayNode)
        let children

        if (includeTime) {
          children = sortNumericKeys(dayNode.times).map((hour) => {
            const minutesMap = dayNode.times.get(hour)
            const minuteNodes = sortNumericKeys(minutesMap).map((minute) => {
              const seconds = [...minutesMap.get(minute)].sort((a, b) => a - b)
              const secondNodes = seconds.map((second) => ({
                key: nodeKey.second(year, month, day, hour, minute, second),
                title: pad2(second),
                level: 'second',
                year,
                month,
                day,
                hour,
                minute,
                second,
                expression: toSecondExpression(
                  year,
                  month,
                  day,
                  hour,
                  minute,
                  second
                ),
                isLeaf: true
              }))

              return {
                key: nodeKey.minute(year, month, day, hour, minute),
                title: pad2(minute),
                level: 'minute',
                year,
                month,
                day,
                hour,
                minute,
                expression: toMinuteExpression(year, month, day, hour, minute),
                children: secondNodes
              }
            })

            return {
              key: nodeKey.hour(year, month, day, hour),
              title: `${pad2(hour)}:00`,
              level: 'hour',
              year,
              month,
              day,
              hour,
              expression: toHourExpression(year, month, day, hour),
              children: minuteNodes
            }
          })
        }

        return {
          key: nodeKey.day(year, month, day),
          title: String(day),
          level: 'day',
          year,
          month,
          day,
          expression: toDayExpression(year, month, day),
          ...(children ? { children } : { isLeaf: true })
        }
      })

      return {
        key: nodeKey.month(year, month),
        title: MONTH_NAMES[month - 1],
        level: 'month',
        year,
        month,
        expression: toMonthExpression(year, month),
        children: dayNodes
      }
    })

    return {
      key: nodeKey.year(year),
      title: String(year),
      level: 'year',
      year,
      expression: toYearExpression(year),
      children: monthNodes
    }
  })
}

const collectAllKeys = (nodes, keys = []) => {
  for (const node of nodes) {
    keys.push(node.key)
    if (node.children?.length) collectAllKeys(node.children, keys)
  }
  return keys
}

const collectLeafKeys = (nodes, keys = []) => {
  for (const node of nodes) {
    if (!node.children?.length) keys.push(node.key)
    else collectLeafKeys(node.children, keys)
  }
  return keys
}

const indexNodesByKey = (nodes, map = new Map()) => {
  for (const node of nodes) {
    map.set(node.key, node)
    if (node.children?.length) indexNodesByKey(node.children, map)
  }
  return map
}

/** Prefer fully-selected ancestors over listing every leaf (Excel-style). */
const collectCollapsedExpressions = (nodes, checkedSet) => {
  const expressions = []

  const walk = (nodeList) => {
    for (const node of nodeList) {
      const leafKeys = collectLeafKeys([node])
      const allChecked =
        leafKeys.length > 0 && leafKeys.every((key) => checkedSet.has(key))

      if (allChecked) {
        expressions.push(node.expression)
        continue
      }

      if (node.children?.length) walk(node.children)
    }
  }

  walk(nodes)
  return expressions
}

const filterTreeBySearch = (nodes, query) => {
  if (!query) return nodes

  const filterNodes = (list) => {
    const result = []
    for (const node of list) {
      const titleMatch = String(node.title).toLowerCase().includes(query)
      if (titleMatch) {
        result.push(node)
        continue
      }
      const filteredChildren = node.children?.length
        ? filterNodes(node.children)
        : []
      if (filteredChildren.length > 0) {
        result.push({ ...node, children: filteredChildren })
      }
    }
    return result
  }

  return filterNodes(nodes)
}

const parseExpressionToKeys = (expression, nodeByKey) => {
  const text = String(expression).trim()
  if (!text) return []

  // Year: 1 1 Y..31 12 Y
  let match = text.match(/^1 1 (\d+)\.\.31 12 \1$/)
  if (match) {
    const key = nodeKey.year(Number(match[1]))
    return nodeByKey.has(key) ? [key] : []
  }

  // Month: 1 M Y..last M Y
  match = text.match(/^1 (\d+) (\d+)\.\.(\d+) \1 \2$/)
  if (match) {
    const month = Number(match[1])
    const year = Number(match[2])
    const last = Number(match[3])
    if (last === lastDayOfMonth(year, month)) {
      const key = nodeKey.month(year, month)
      return nodeByKey.has(key) ? [key] : []
    }
  }

  // Day-first numeric: D M Y [H [Min [S]]]
  match = text.match(
    /^(\d+)\s+(\d+)\s+(\d+)(?:\s+(\d+)(?:\s+(\d+)(?:\s+(\d+))?)?)?$/
  )
  if (match) {
    const day = Number(match[1])
    const month = Number(match[2])
    const year = Number(match[3])
    const hour = match[4] != null ? Number(match[4]) : null
    const minute = match[5] != null ? Number(match[5]) : null
    const second = match[6] != null ? Number(match[6]) : null

    let key
    if (second != null) {
      key = nodeKey.second(year, month, day, hour, minute, second)
    } else if (minute != null) {
      key = nodeKey.minute(year, month, day, hour, minute)
    } else if (hour != null) {
      key = nodeKey.hour(year, month, day, hour)
    } else {
      key = nodeKey.day(year, month, day)
    }
    return nodeByKey.has(key) ? [key] : []
  }

  // Interval spanning a single calendar day already handled; try ISO / Date parse
  // for a specific instant → check the deepest matching node.
  if (/[-/T]/.test(text) || /\d:\d/.test(text)) {
    const date = parseDateValue(text)
    if (!date) return []
    const y = date.getFullYear()
    const m = date.getMonth() + 1
    const d = date.getDate()
    const h = date.getHours()
    const min = date.getMinutes()
    const s = date.getSeconds()

    const candidates = [
      nodeKey.second(y, m, d, h, min, s),
      nodeKey.minute(y, m, d, h, min),
      nodeKey.hour(y, m, d, h),
      nodeKey.day(y, m, d)
    ]
    for (const key of candidates) {
      if (nodeByKey.has(key)) return [key]
    }
  }

  return []
}

const expandKeysForChecked = (keys, nodeByKey) => {
  const checked = new Set()

  for (const key of keys) {
    const node = nodeByKey.get(key)
    if (!node) {
      checked.add(key)
      continue
    }
    for (const leaf of collectLeafKeys([node])) {
      checked.add(leaf)
    }
    // Also mark intermediate keys so Tree shows parent check state correctly
    checked.add(key)
    if (node.children?.length) {
      for (const childKey of collectAllKeys(node.children)) {
        checked.add(childKey)
      }
    }
  }

  return [...checked]
}

const SimpleDateTimePropertyFilter = ({
  modelType,
  propertyName,
  value = [],
  onChange,
  search = ''
}) => {
  const { getModelPropertyValues } = useContext(ApiServerContext)
  const [dates, setDates] = useState([])
  const [loading, setLoading] = useState(false)
  const [localChecked, setLocalChecked] = useState(null)

  useEffect(() => {
    let cancelled = false

    const loadOptions = async () => {
      if (!modelType || !propertyName) return
      setLoading(true)
      try {
        const values = await getModelPropertyValues(modelType, propertyName)
        if (cancelled) return
        const parsed = (values || [])
          .map(parseDateValue)
          .filter(Boolean)
          .sort((a, b) => a.getTime() - b.getTime())
        setDates(parsed)
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    loadOptions()
    return () => {
      cancelled = true
    }
  }, [modelType, propertyName, getModelPropertyValues])

  const treeData = useMemo(
    () => buildTreeData(buildDateHierarchy(dates)),
    [dates]
  )

  const nodeByKey = useMemo(() => indexNodesByKey(treeData), [treeData])
  const allKeys = useMemo(() => collectAllKeys(treeData), [treeData])
  const allLeafKeys = useMemo(() => collectLeafKeys(treeData), [treeData])

  const valueKey = useMemo(() => JSON.stringify(value || []), [value])

  useEffect(() => {
    if (treeData.length === 0) return

    if (value?.length > 0) {
      const matched = []
      for (const expr of value) {
        matched.push(...parseExpressionToKeys(expr, nodeByKey))
      }
      if (matched.length > 0) {
        setLocalChecked(expandKeysForChecked(matched, nodeByKey))
      } else {
        // Unrecognized expression — treat as no tree selection highlight
        setLocalChecked([])
      }
    } else {
      setLocalChecked(allKeys)
    }
    // valueKey captures value contents
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [treeData, valueKey, allKeys, nodeByKey])

  const filteredTreeData = useMemo(() => {
    const query = search.trim().toLowerCase()
    return filterTreeBySearch(treeData, query)
  }, [treeData, search])

  const checkedKeys = localChecked ?? allKeys

  const emitChange = useCallback(
    (nextKeys) => {
      setLocalChecked(nextKeys)
      const checkedSet = new Set(nextKeys)

      if (
        allLeafKeys.length > 0 &&
        allLeafKeys.every((key) => checkedSet.has(key))
      ) {
        onChange?.([])
        return
      }

      const expressions = collectCollapsedExpressions(treeData, checkedSet)
      onChange?.(expressions)
    },
    [allLeafKeys, onChange, treeData]
  )

  const checkedSet = useMemo(() => new Set(checkedKeys), [checkedKeys])

  const rebuildCheckedKeys = useCallback(
    (selectedLeaves) => {
      const leafSet = new Set(selectedLeaves)
      const next = []

      const walk = (nodes) => {
        for (const node of nodes) {
          const leaves = collectLeafKeys([node])
          if (leaves.length > 0 && leaves.every((key) => leafSet.has(key))) {
            next.push(...collectAllKeys([node]))
          } else if (node.children?.length) {
            walk(node.children)
          } else if (leafSet.has(node.key)) {
            next.push(node.key)
          }
        }
      }

      walk(treeData)
      return [...new Set(next)]
    },
    [treeData]
  )

  const handleNodeCheck = (node, wantChecked) => {
    const leavesUnderNode = new Set(collectLeafKeys([node]))
    const currentLeaves = checkedKeys.filter((key) => {
      const n = nodeByKey.get(key)
      return n && !n.children?.length
    })

    let nextLeaves
    if (wantChecked) {
      nextLeaves = [...new Set([...currentLeaves, ...leavesUnderNode])]
    } else {
      nextLeaves = currentLeaves.filter((key) => !leavesUnderNode.has(key))
    }

    const query = search.trim().toLowerCase()
    if (!query) {
      emitChange(rebuildCheckedKeys(nextLeaves))
      return
    }

    // Preserve checked leaves that are hidden by the search filter
    const visibleLeafKeys = new Set(collectLeafKeys(filteredTreeData))
    const hiddenSelectedLeaves = currentLeaves.filter(
      (key) => !visibleLeafKeys.has(key)
    )
    const visibleSelectedLeaves = nextLeaves.filter((key) =>
      visibleLeafKeys.has(key)
    )

    emitChange(
      rebuildCheckedKeys([...hiddenSelectedLeaves, ...visibleSelectedLeaves])
    )
  }

  const titleRender = (node) => {
    const leafKeys = collectLeafKeys([node])
    const checkedCount = leafKeys.filter((key) => checkedSet.has(key)).length
    const checked = leafKeys.length > 0 && checkedCount === leafKeys.length
    const indeterminate = checkedCount > 0 && checkedCount < leafKeys.length

    return (
      <Flex align='center' style={{ marginLeft: '-3.5px', marginTop: '1px' }}>
        <Checkbox
          checked={checked}
          indeterminate={indeterminate}
          onChange={(e) => handleNodeCheck(node, e.target.checked)}
          onClick={(e) => e.stopPropagation()}
          style={{ minWidth: 0 }}
        />
        <Text style={{ minWidth: 0, paddingInlineStart: '8px' }}>
          {node.title}
        </Text>
      </Flex>
    )
  }

  return (
    <Spin spinning={loading} indicator={<LoadingOutlined spin />}>
      {treeData.length > 0 ? (
        <Tree
          selectable={false}
          treeData={filteredTreeData}
          titleRender={titleRender}
          showLine={true}
          switcherIcon={<CaretDownOutlined />}
          className='simple-date-time-property-filter'
          defaultExpandAll={false}
          style={{ minWidth: 0 }}
        />
      ) : null}
    </Spin>
  )
}

SimpleDateTimePropertyFilter.propTypes = {
  modelType: PropTypes.string,
  propertyName: PropTypes.string,
  value: PropTypes.array,
  onChange: PropTypes.func,
  search: PropTypes.string
}

export default SimpleDateTimePropertyFilter
