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

// 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 ObjectSelect = ({
  type = 'unknown',
  showSearch = false,
  multiple = false,
  treeSelectProps = {},
  filter = {},
  masterFilter = {},
  value,
  disabled = false,
  ...rest
}) => {
  const { fetchObjectsByProperty, fetchObject, connected } =
    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 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') {
      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 === object._id)) 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}
                  style={{ top: '-0.5px' }}
                />
              </div>
            ),
            value: object._id,
            key: object._id,
            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
          })

          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: buildTreeData(
              children,
              pIdx + 1,
              parentKeys.concat(valueString),
              newFilterPath
            )
          }
        })
        .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 = (value) => {
    // Mark this as an internal change
    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) => obj._id === id))
          .filter(Boolean)
      }
      setTreeSelectValue(value)
      if (rest.onChange) rest.onChange(selectedObjects)
    } else {
      // Single selection
      const selectedObject = objectList.find((obj) => obj._id === value)
      setTreeSelectValue(value)
      if (rest.onChange) rest.onChange(selectedObject)
    }
  }

  // Update treeData when objectPropertiesTree changes
  useEffect(() => {
    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])

  useEffect(() => {
    const handleValue = async () => {
      if (
        value &&
        typeof value === 'object' &&
        value !== null &&
        valueRef.current !== value &&
        type != 'unknown'
      ) {
        console.log('fetching full object', value)
        valueRef.current = value
        // Check if value is a minimal object and fetch full object if needed
        const fullValue = await fetchFullObjectIfNeeded(value)
        console.log('fullValue', fullValue)
        // Build a new filter from value's properties that are in the properties list
        const valueFilter = { ...filter }
        properties.forEach((prop) => {
          if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
            const filterValue = fullValue[prop]
            if (
              filterValue &&
              typeof filterValue === 'object' &&
              filterValue._id
            ) {
              valueFilter[prop] = filterValue._id
            } else if (filterValue?.name) {
              valueFilter[prop] = filterValue.name
            } else if (Array.isArray(filterValue)) {
              valueFilter[prop] = filterValue.join(',')
            } else {
              valueFilter[prop] = filterValue
            }
          }
        })
        // Fetch with the new filter
        handleFetchObjectsProperties(valueFilter)
        console.log('setting treeSelectValue', valueRef.current._id)
        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
  ])

  const prevValuesRef = useRef({ type, masterFilter })

  useEffect(() => {
    console.log('treeSelectValue', treeSelectValue)
  }, [treeSelectValue])

  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) {
      setObjectPropertiesTree({})
      setObjectList([])
      setTreeData([])
      setInitialized(false)
      onTreeSelectChange(null)
      setTreeSelectValue(null)
      setInitialLoading(true)
      setError(false)
      prevValuesRef.current = { type, masterFilter }
    }
  }, [type, masterFilter])

  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])

  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>
    )
  }

  if (initialLoading) {
    return <TreeSelect disabled loading placeholder='Loading...' />
  }

  // --- Main TreeSelect UI ---
  return (
    <TreeSelect
      treeDataSimpleMode={false}
      treeDefaultExpandAll={true}
      treeData={treeData}
      showSearch={showSearch}
      multiple={multiple}
      loadData={loadData}
      showCheckedStrategy={SHOW_CHILD}
      placeholder={placeholder}
      {...treeSelectProps}
      {...rest}
      value={treeSelectValue}
      onChange={onTreeSelectChange}
      disabled={disabled || type == 'unknown' || type == undefined}
    />
  )
}

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
}

export default ObjectSelect
