import {
  useEffect,
  useState,
  useContext,
  useCallback,
  useMemo,
  useRef
} from 'react'
import PropTypes from 'prop-types'
import { TreeSelect, Space, Button, Input } from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels'
import merge from 'lodash/merge'
import { getModelProperty } from '../../../database/ObjectModels'
const { SHOW_CHILD } = TreeSelect

const EMPTY_OBJECT = {}

// Helper to check if two values are equal (handling objects/ids)
const areValuesEqual = (v1, v2) => {
  const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
  const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
  return String(id1) === String(id2)
}

const getFirstSelectableLeaf = (nodes) => {
  if (!Array.isArray(nodes)) return null
  for (const node of nodes) {
    if (node.isLeaf && node.selectable !== false) return node
    const found = getFirstSelectableLeaf(node.children)
    if (found) return found
  }
  return null
}

const ObjectSelect = ({
  type = 'unknown',
  showSearch = true,
  multiple = false,
  treeSelectProps = EMPTY_OBJECT,
  filter = EMPTY_OBJECT,
  masterFilter = EMPTY_OBJECT,
  value,
  onChange,
  disabled = false,
  style = {},
  ...rest
}) => {
  const { fetchObjectsByProperty, fetchObject, connected, searchObjects } =
    useContext(ApiServerContext)
  const { token } = useContext(AuthContext)
  // --- State ---
  const [treeData, setTreeData] = useState([])
  const [objectPropertiesTree, setObjectPropertiesTree] = useState([])
  const [initialized, setInitialized] = useState(false)
  const [error, setError] = useState(false)
  const properties = useMemo(() => getModelByName(type).group || [], [type])
  const [objectList, setObjectList] = useState([])
  const [treeSelectValue, setTreeSelectValue] = useState(null)
  const [initialLoading, setInitialLoading] = useState(true)
  const [delayedInitialLoading, setDelayedInitalLoading] = useState(true)
  const [expandedKeys, setExpandedKeys] = useState([])
  const [treeVersion, setTreeVersion] = useState(0)
  const [isSearching, setIsSearching] = useState(false)
  const [searchValue, setSearchValue] = useState('')
  const searchRequestIdRef = useRef(0)
  const valueRef = useRef(null)

  // Refs to track value changes
  const prevValueRef = useRef(value)
  const isInternalChangeRef = useRef(false)

  // Normalize a value to an identity string so we can detect in-place _id updates
  const getValueIdentity = useCallback((val) => {
    if (val && typeof val === 'object') {
      // Handle arrays
      if (Array.isArray(val)) {
        const ids = val
          .map((item) => {
            if (item && typeof item === 'object') {
              if (item._id) return String(item._id)
              if (
                item.value &&
                typeof item.value === 'object' &&
                item.value._id
              )
                return String(item.value._id)
            }
            return null
          })
          .filter(Boolean)
          .sort()
        return ids.length > 0 ? ids.join(',') : JSON.stringify(val)
      }
      // Handle single objects
      if (val._id) return String(val._id)
      if (val.value && typeof val.value === 'object' && val.value._id)
        return String(val.value._id)
    }
    return JSON.stringify(val)
  }, [])
  const prevValueIdentityRef = useRef(getValueIdentity(value))

  // Utility function to check if object only contains _id
  const isMinimalObject = useCallback((obj) => {
    if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
      return false
    }
    const keys = Object.keys(obj)
    return keys.length === 1 && keys[0] === '_id' && obj._id
  }, [])

  // Function to fetch full object if only _id is present
  const fetchFullObjectIfNeeded = useCallback(
    async (obj) => {
      if (isMinimalObject(obj)) {
        try {
          const fullObject = await fetchObject(obj._id, type)
          return fullObject
        } catch (err) {
          console.error('Failed to fetch full object:', err)
          return obj // Return original object if fetch fails
        }
      }
      return obj
    },
    [isMinimalObject, fetchObject, type]
  )

  const mergeGroups = useCallback((current, incoming) => {
    if (!current) return incoming
    if (!incoming) return current
    if (!Array.isArray(current) || !Array.isArray(incoming)) return incoming

    const merged = [...current]

    // Helper to generate a unique key for a group node
    const getGroupKey = (item) => {
      const val = item.value
      const valPart =
        val && typeof val === 'object' && val._id
          ? val._id
          : JSON.stringify(val)
      return `${item.property}:${valPart}`
    }

    for (const item of incoming) {
      if (item.property && item.value !== undefined) {
        // It's a group node
        const itemKey = getGroupKey(item)
        const existingIdx = merged.findIndex(
          (x) =>
            x.property && x.value !== undefined && getGroupKey(x) === itemKey
        )

        if (existingIdx > -1) {
          merged[existingIdx] = {
            ...merged[existingIdx],
            children: mergeGroups(merged[existingIdx].children, item.children)
          }
        } else {
          merged.push(item)
        }
      } else {
        // It's a leaf object
        if (!merged.some((x) => String(x._id) === String(item._id))) {
          merged.push(item)
        }
      }
    }
    return merged
  }, [])

  // Fetch the object properties tree from the API
  const handleFetchObjectsProperties = useCallback(
    async (customFilter = filter) => {
      try {
        const data = await fetchObjectsByProperty(type, {
          properties: properties,
          filter: customFilter,
          masterFilter
        })

        if (Array.isArray(data)) {
          setObjectPropertiesTree((prev) => mergeGroups(prev, data))
        } else {
          // Fallback if API returns something unexpected
          setObjectPropertiesTree((prev) => merge([], prev, data))
        }

        setInitialLoading(false)
        setError(false)
        return data
      } catch (err) {
        console.error(err)
        setError(true)
        return null
      }
    },
    [
      type,
      fetchObjectsByProperty,
      properties,
      filter,
      masterFilter,
      mergeGroups
    ]
  )

  // Convert the API response to AntD TreeSelect treeData
  const buildTreeData = useCallback(
    (data, pIdx = 0, parentKeys = [], filterPath = []) => {
      if (!data || !Array.isArray(data)) return []
      // If we are past the grouping properties, these are leaf objects
      if (pIdx >= properties.length) {
        return data.map((object) => {
          setObjectList((prev) => {
            if (
              prev.some(
                (p) =>
                  p._id.toString().toLowerCase() ===
                  object._id.toString().toLowerCase()
              )
            )
              return prev
            return [...prev, object]
          })
          return {
            title: (
              <div style={{ paddingTop: 0 }}>
                <ObjectProperty
                  key={object._id}
                  type='object'
                  value={object}
                  objectType={type}
                  objectData={object}
                  isEditing={false}
                  showHyperlink={false}
                  style={{ top: '-0.5px' }}
                />
              </div>
            ),
            value: object._id.toString().toLowerCase(),
            key: object._id.toString().toLowerCase(),
            isLeaf: true,
            parentKeys,
            filterPath
          }
        })
      }

      // Group Nodes
      return data
        .map((group) => {
          // Only process if it looks like a group
          if (!group.property) return null

          const { property, value, children } = group
          var valueString = value
          if (value && typeof value === 'object' && value._id) {
            valueString = value._id
          }
          if (Array.isArray(valueString)) {
            valueString = valueString.join(',')
          }
          const nodeKey = parentKeys
            .concat(property + ':' + valueString)
            .join('-')
          const newFilterPath = filterPath.concat({
            property,
            value: valueString
          })

          var nodeChildren = buildTreeData(
            children,
            pIdx + 1,
            parentKeys.concat(valueString),
            newFilterPath
          )
          if (nodeChildren.length == 0) {
            nodeChildren = undefined
          }
          const modelProperty = getModelProperty(type, property)
          return {
            title: <ObjectProperty {...modelProperty} value={value} />,
            value: nodeKey,
            key: nodeKey,
            property,
            filterValue: valueString,
            parentKeys: parentKeys.concat(valueString),
            filterPath: newFilterPath,
            selectable: false,
            isLeaf: false,
            children: nodeChildren
          }
        })
        .filter(Boolean)
    },
    [properties, type]
  )

  // --- loadData for async loading on expand ---
  const loadData = async (node) => {
    // node.property is the property name, node.value is the value key
    if (!node.property) return
    if (type == 'unknown') return
    // Build filter for this node by merging all parent property-value pairs
    const customFilter = { ...filter }
    if (Array.isArray(node.filterPath)) {
      node.filterPath.forEach(({ property, value }) => {
        customFilter[property] = value
      })
    }
    // Ensure current node is in filter (should be covered by filterPath, but redundancy is safe)
    customFilter[node.property] = node.filterValue
    // Fetch children for this node
    const data = await handleFetchObjectsProperties(customFilter)
    if (!data) return

    // Navigate to the specific node's children in the response
    let nodeSpecificChildren = data

    if (node.filterPath && Array.isArray(node.filterPath)) {
      for (const pathItem of node.filterPath) {
        if (!Array.isArray(nodeSpecificChildren)) break
        const match = nodeSpecificChildren.find(
          (g) =>
            g.property === pathItem.property &&
            areValuesEqual(g.value, pathItem.value)
        )
        if (match) {
          nodeSpecificChildren = match.children
        } else {
          nodeSpecificChildren = []
          break
        }
      }
    }

    // Build new tree children only for this specific node
    const children = buildTreeData(
      nodeSpecificChildren,
      properties.indexOf(node.property) + 1,
      node.parentKeys || [],
      node.filterPath
    )

    // Update treeData with new children for this node only
    setTreeData((prevTreeData) => {
      // Helper to recursively update the correct node
      const updateNode = (nodes) =>
        nodes.map((n) => {
          if (n.key === node.key) {
            return { ...n, children, isLeaf: children.length === 0 }
          } else if (n.children) {
            return { ...n, children: updateNode(n.children) }
          }
          return n
        })
      return updateNode(prevTreeData)
    })
  }

  const onTreeSelectChange = useCallback(
    (value) => {
      // Mark this as an internal change
      if (!multiple) setIsSearching(false)
      isInternalChangeRef.current = true

      // value can be a string (single) or array (multiple)
      if (multiple) {
        // Multiple selection
        let selectedObjects = []
        if (Array.isArray(value)) {
          selectedObjects = value
            .map((id) => objectList.find((obj) => areValuesEqual(obj._id, id)))
            .filter(Boolean)
        }
        setTreeSelectValue(value)
        onChange?.(selectedObjects)
      } else {
        // Single selection
        const selectedObject = objectList.find((obj) => obj._id === value)
        setTreeSelectValue(value)
        onChange?.(selectedObject)
      }
    },
    [multiple, objectList, onChange]
  )

  const onSearch = useCallback(
    async (search) => {
      setSearchValue(search)
      const trimmed = search?.trim() ?? ''
      if (!trimmed) {
        searchRequestIdRef.current += 1
        setIsSearching(false)
        setTreeData(buildTreeData(objectPropertiesTree))
        return
      }

      const requestId = ++searchRequestIdRef.current
      setIsSearching(true)
      const data = await searchObjects(type, trimmed)
      if (requestId !== searchRequestIdRef.current) return
      if (!Array.isArray(data)) return

      const searchData =
        multiple && Array.isArray(treeSelectValue)
          ? [
              ...data,
              ...objectList.filter((object) =>
                treeSelectValue.some((id) => areValuesEqual(object._id, id))
              )
            ].filter(
              (object, index, objects) =>
                objects.findIndex((item) =>
                  areValuesEqual(item._id, object._id)
                ) === index
            )
          : data

      setTreeData(buildTreeData(searchData, properties.length))
    },
    [
      searchObjects,
      type,
      buildTreeData,
      objectPropertiesTree,
      properties.length,
      multiple,
      treeSelectValue,
      objectList
    ]
  )

  const onInputKeyDown = useCallback(
    (e) => {
      treeSelectProps.onInputKeyDown?.(e)
      if (e.defaultPrevented || e.key !== 'Enter' || !isSearching) return

      const firstLeaf = getFirstSelectableLeaf(treeData)
      if (!firstLeaf?.value) return

      e.preventDefault()
      e.stopPropagation()
      onTreeSelectChange(
        multiple
          ? [
              ...(Array.isArray(treeSelectValue) ? treeSelectValue : []),
              firstLeaf.value
            ].filter(
              (item, index, values) =>
                values.findIndex((value) => areValuesEqual(value, item)) ===
                index
            )
          : firstLeaf.value
      )
      setSearchValue('')
      onSearch('')
    },
    [
      treeSelectProps,
      isSearching,
      treeData,
      treeSelectValue,
      multiple,
      onTreeSelectChange,
      onSearch
    ]
  )
  // Update treeData when objectPropertiesTree changes
  useEffect(() => {
    if (isSearching) return
    if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
      const newTreeData = buildTreeData(objectPropertiesTree)
      setTreeData((prev) => {
        if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
          return newTreeData
        }
        return prev
      })
    }
  }, [objectPropertiesTree, properties, buildTreeData, isSearching])

  useEffect(() => {
    const handleValue = async () => {
      if (
        multiple &&
        Array.isArray(value) &&
        getValueIdentity(valueRef.current) !== getValueIdentity(value) &&
        type != 'unknown'
      ) {
        valueRef.current = value
        const fullValues = await Promise.all(value.map(fetchFullObjectIfNeeded))
        const pathKeys = []

        if (fullValues.length === 0) {
          handleFetchObjectsProperties()
        } else {
          fullValues.forEach((fullValue) => {
            const valueFilter = { ...filter }
            const parentKeys = []

            properties.forEach((prop) => {
              if (!Object.prototype.hasOwnProperty.call(fullValue, prop)) return

              const filterValue = fullValue[prop]
              let valueString = filterValue
              if (
                filterValue &&
                typeof filterValue === 'object' &&
                filterValue._id
              ) {
                valueString = filterValue._id
              } else if (filterValue?.name) {
                valueString = filterValue.name
              } else if (Array.isArray(filterValue)) {
                valueString = filterValue.join(',')
              }

              valueFilter[prop] = valueString
              pathKeys.push(
                parentKeys.concat(prop + ':' + valueString).join('-')
              )
              parentKeys.push(valueString)
            })

            handleFetchObjectsProperties(valueFilter)
          })
        }

        setExpandedKeys([...new Set(pathKeys)])
        setTreeSelectValue(
          value
            .map((item) => (item && typeof item === 'object' ? item._id : item))
            .filter((id) => id != null)
        )
        setInitialized(true)
        return
      }
      if (
        value &&
        typeof value === 'object' &&
        value !== null &&
        getValueIdentity(valueRef.current) !== getValueIdentity(value) &&
        type != 'unknown'
      ) {
        valueRef.current = value
        // Check if value is a minimal object and fetch full object if needed
        const fullValue = await fetchFullObjectIfNeeded(value)
        // Build a new filter from value's properties that are in the properties list
        const valueFilter = { ...filter }
        const pathKeys = []
        const parentKeys = []
        properties.forEach((prop) => {
          if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
            const filterValue = fullValue[prop]
            let valueString = filterValue
            if (
              filterValue &&
              typeof filterValue === 'object' &&
              filterValue._id
            ) {
              valueFilter[prop] = filterValue._id
              valueString = filterValue._id
            } else if (filterValue?.name) {
              valueFilter[prop] = filterValue.name
              valueString = filterValue.name
            } else if (Array.isArray(filterValue)) {
              valueFilter[prop] = filterValue.join(',')
              valueString = filterValue.join(',')
            } else {
              valueFilter[prop] = filterValue
              valueString = filterValue
            }
            // Build the path key for this property level
            const nodeKey = parentKeys
              .concat(prop + ':' + valueString)
              .join('-')
            pathKeys.push(nodeKey)
            parentKeys.push(valueString)
          }
        })
        // Expand the path to the object
        setExpandedKeys(pathKeys)
        // Fetch with the new filter
        handleFetchObjectsProperties(valueFilter)
        setTreeSelectValue(valueRef.current._id)
        setInitialized(true)
        return
      }
      if (
        !initialized &&
        token != null &&
        type != 'unknown' &&
        type != undefined &&
        connected == true
      ) {
        handleFetchObjectsProperties()
        setInitialized(true)
      }
      if (
        value == null ||
        type == 'unknown' ||
        type == undefined ||
        connected == false
      ) {
        setTreeSelectValue(null)
        setInitialLoading(false)
        setInitialized(true)
      }
    }
    handleValue()
  }, [
    value,
    filter,
    properties,
    handleFetchObjectsProperties,
    initialized,
    token,
    fetchFullObjectIfNeeded,
    type,
    connected,
    getValueIdentity,
    multiple
  ])

  const prevValuesRef = useRef({ type, masterFilter })

  useEffect(() => {
    const prevValues = prevValuesRef.current

    // Deep comparison for objects, simple comparison for primitives
    const hasChanged =
      prevValues.type !== type ||
      JSON.stringify(prevValues.masterFilter) !== JSON.stringify(masterFilter)

    if (hasChanged) {
      searchRequestIdRef.current += 1
      setIsSearching(false)
      setSearchValue('')
      setObjectPropertiesTree({})
      setObjectList([])
      setTreeData([])
      setTreeVersion((v) => v + 1)
      setExpandedKeys([])
      setInitialized(false)
      onTreeSelectChange(null)
      setTreeSelectValue(null)
      setInitialLoading(true)
      setError(false)
      prevValuesRef.current = { type, masterFilter }
    }
  }, [type, masterFilter, onTreeSelectChange])

  useEffect(() => {
    // Check if value has actually changed
    const currentValueIdentity = getValueIdentity(value)
    const hasValueChanged =
      prevValueIdentityRef.current !== currentValueIdentity

    if (hasValueChanged) {
      const changeSource = isInternalChangeRef.current ? 'internal' : 'external'

      if (changeSource == 'external') {
        setObjectPropertiesTree({})
        setTreeData([])
        setInitialized(false)
        prevValuesRef.current = { type, masterFilter }
      }

      // Reset the internal change flag
      isInternalChangeRef.current = false

      // Update the previous value reference
      prevValueRef.current = value
      prevValueIdentityRef.current = currentValueIdentity
    }
  }, [value, getValueIdentity, type, masterFilter])

  useEffect(() => {
    if (initialLoading == false) {
      setTimeout(() => {
        setDelayedInitalLoading(false)
      }, 100)
    } else {
      setDelayedInitalLoading(true)
    }
  }, [initialLoading])

  const placeholder = useMemo(
    () =>
      type == 'unknown' || type == undefined
        ? 'n/a'
        : `Select a ${getModelByName(type).label.toLowerCase()}...`,
    [type]
  )

  // --- Error UI ---
  if (error) {
    return (
      <Space.Compact style={{ width: '100%' }}>
        <Input value='Failed to load data.' status='error' disabled />
        <Button
          icon={<ReloadIcon />}
          onClick={() => {
            setError(false)
            setTreeData([])
            setInitialized(false)
          }}
          danger
        />
      </Space.Compact>
    )
  }

  // --- Main TreeSelect UI ---
  return (
    <div style={{ ...style, position: 'relative' }}>
      <TreeSelect
        key={treeVersion}
        treeDataSimpleMode={false}
        treeDefaultExpandAll={true}
        treeExpandedKeys={expandedKeys}
        onTreeExpand={setExpandedKeys}
        treeData={treeData}
        showSearch={showSearch}
        filterTreeNode={() => true}
        multiple={multiple}
        loadData={isSearching ? undefined : loadData}
        showCheckedStrategy={SHOW_CHILD}
        placeholder={placeholder}
        {...treeSelectProps}
        {...rest}
        value={treeSelectValue}
        onChange={onTreeSelectChange}
        onSearch={onSearch}
        onInputKeyDown={onInputKeyDown}
        searchValue={searchValue}
        disabled={disabled || type == 'unknown' || type == undefined}
        style={{ opacity: delayedInitialLoading ? 0 : 1 }}
        className={multiple ? 'object-select-multiple' : 'object-select'}
      />
      {delayedInitialLoading && (
        <TreeSelect
          disabled
          loading
          placeholder='Loading...'
          style={{
            position: 'absolute',
            top: 0,
            left: 0,
            right: 0,
            bottom: 0
          }}
        />
      )}
    </div>
  )
}

ObjectSelect.propTypes = {
  properties: PropTypes.arrayOf(PropTypes.string).isRequired,
  filter: PropTypes.object,
  masterFilter: PropTypes.object,
  useFilter: PropTypes.bool,
  value: PropTypes.any,
  onChange: PropTypes.func,
  showSearch: PropTypes.bool,
  multiple: PropTypes.bool,
  treeSelectProps: PropTypes.object,
  type: PropTypes.string.isRequired,
  disabled: PropTypes.bool,
  style: PropTypes.object
}

export default ObjectSelect
