// src/contexts/ApiServerContext.js
import {
  createContext,
  useEffect,
  useState,
  useContext,
  useRef,
  useCallback
} from 'react'
import io from 'socket.io-client'
import { message, Modal, Space, Button } from 'antd'
import PropTypes from 'prop-types'
import { AuthContext } from './AuthContext'
import { useLocation, useNavigate } from 'react-router-dom'

import axios from 'axios'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import config from '../../../config'
import loglevel from 'loglevel'
import { getModelByName } from '../../../database/ObjectModels'
const logger = loglevel.getLogger('ApiServerContext')
logger.setLevel(config.logLevel)

const SPOTLIGHT_CACHE_TTL_MS = 10_000

const spotlightCache = new Map()
const runningSpotlightFetches = new Map()

const createEmptyUserSettings = () => ({
  viewMode: {},
  filterSidebarVisibility: {},
  sortSidebarVisibility: {},
  columnVisibility: {},
  collapseState: {}
})

const normalizeUserSettingsCategory = (category) =>
  category && typeof category === 'object' && !Array.isArray(category)
    ? category
    : {}

const normalizeUserSettings = (settings = {}) => ({
  viewMode: normalizeUserSettingsCategory(settings?.viewMode),
  filterSidebarVisibility: normalizeUserSettingsCategory(
    settings?.filterSidebarVisibility
  ),
  sortSidebarVisibility: normalizeUserSettingsCategory(
    settings?.sortSidebarVisibility
  ),
  columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility),
  collapseState: normalizeUserSettingsCategory(settings?.collapseState)
})

const emitWithAcknowledgement = (socket, eventName, data) =>
  new Promise((resolve) => socket.emit(eventName, data, resolve))

const stableStringify = (value) => {
  if (Array.isArray(value)) {
    return `[${value.map(stableStringify).join(',')}]`
  }

  if (value && typeof value === 'object') {
    return `{${Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
      .join(',')}}`
  }

  return JSON.stringify(value)
}

const getObjectTypeSubscriptionKey = (objectType, filter = {}) =>
  `${objectType}:${stableStringify(filter || {})}`

const getObjectTypeSubscriptionArgs = (filterOrCallback, callback) => {
  if (typeof filterOrCallback === 'function') {
    return { filter: {}, callback: filterOrCallback }
  }

  return { filter: filterOrCallback || {}, callback }
}

const getObjectEndpoint = (type) =>
  getModelByName(type)?.endpoint || `${type.toLowerCase()}s`

const ApiServerContext = createContext()

const ApiServerProvider = ({ children }) => {
  const location = useLocation()
  const navigate = useNavigate()
  const {
    token,
    userProfile,
    setUserProfile,
    authenticated,
    setUnauthenticated
  } = useContext(AuthContext)
  const socketRef = useRef(null)
  const [connected, setConnected] = useState(false)
  const [connecting, setConnecting] = useState(false)
  const [error, setError] = useState(null)
  const [messageApi, contextHolder] = message.useMessage()
  const [fetchLoading, setFetchLoading] = useState(false)
  const [showErrorModal, setShowErrorModal] = useState(false)
  const [errorModalContent, setErrorModalContent] = useState('')
  const [retryCallback, setRetryCallback] = useState(null)
  const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
  const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
  const subscribedCallbacksRef = useRef(new Map())
  const subscribedActivityCallbacksRef = useRef(new Map())
  const subscribedActivityServerSubscriptionsRef = useRef(new Set())
  const notificationListenersRef = useRef(new Set())
  const completedLaunchSessionsRef = useRef(new Set())

  const handleActivityUpdate = useCallback(async (data) => {
    logger.debug('Notifying activity update:', data)
    const objectId = data._id || data.id
    const objectType = data.objectType
    const activity = data.activity
    const activities = data.activities
    const callbacksRefKey = `${objectType}:${objectId}`

    if (
      objectId &&
      objectType &&
      (activity || Array.isArray(activities)) &&
      subscribedActivityCallbacksRef.current.has(callbacksRefKey)
    ) {
      const callbacks =
        subscribedActivityCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} activity callbacks for object:`,
        objectId
      )
      callbacks.forEach((callback) => {
        try {
          callback(activity, activities)
        } catch (error) {
          logger.error('Error in activity update callback:', error)
        }
      })
    }
  }, [])

  const clearSubscriptions = useCallback(() => {
    subscribedCallbacksRef.current.clear()
    subscribedActivityServerSubscriptionsRef.current.clear()
    notificationListenersRef.current.clear()
  }, [])

  const resubscribeActivityListeners = useCallback(
    (socket = socketRef.current) => {
      if (!socket?.connected) {
        return
      }

      subscribedActivityCallbacksRef.current.forEach(
        (callbacks, callbacksRefKey) => {
          if (!callbacks?.length) {
            return
          }

          if (
            subscribedActivityServerSubscriptionsRef.current.has(
              callbacksRefKey
            )
          ) {
            return
          }

          const colonIndex = callbacksRefKey.indexOf(':')
          if (colonIndex === -1) {
            return
          }

          const objectType = callbacksRefKey.slice(0, colonIndex)
          const objectId = callbacksRefKey.slice(colonIndex + 1)

          socket.emit(
            'subscribeToObjectActivity',
            { _id: objectId, objectType },
            (result) => {
              if (result?.success) {
                subscribedActivityServerSubscriptionsRef.current.add(
                  callbacksRefKey
                )
                if (Array.isArray(result.activities)) {
                  callbacks.forEach((callback) => {
                    try {
                      callback(null, result.activities)
                    } catch (error) {
                      logger.error(
                        'Error in activity resubscribe callback:',
                        error
                      )
                    }
                  })
                }
              }
            }
          )
        }
      )
    },
    []
  )

  const registerNotificationListener = useCallback((callback) => {
    notificationListenersRef.current.add(callback)
    return () => notificationListenersRef.current.delete(callback)
  }, [])

  const unregisterNotificationListener = useCallback((callback) => {
    notificationListenersRef.current.delete(callback)
  }, [])

  const getUserSettings = useCallback(async (socket = socketRef.current) => {
    if (!socket?.connected) {
      return null
    }

    try {
      const result = await emitWithAcknowledgement(
        socket,
        'getUserSettings',
        {}
      )
      if (!result?.success) {
        throw new Error(result?.error || 'Unable to get user settings')
      }

      const settings = normalizeUserSettings(result.settings)
      setUserSettings(settings)
      return settings
    } catch (err) {
      logger.error('Failed to get user settings:', err)
      setUserSettings(createEmptyUserSettings())
      return null
    } finally {
      setUserSettingsLoaded(true)
    }
  }, [])

  const updateUserSettings = useCallback(
    async (category, key, value) => {
      setUserSettings((currentSettings) => ({
        ...currentSettings,
        [category]: {
          ...currentSettings[category],
          [key]: value
        }
      }))

      const socket = socketRef.current
      if (!socket?.connected) {
        return null
      }

      try {
        const result = await emitWithAcknowledgement(
          socket,
          'updateUserSettings',
          { category, key, value }
        )
        if (!result?.success) {
          throw new Error(result?.error || 'Unable to update user settings')
        }
        return result.settings
      } catch (err) {
        logger.error('Failed to update user settings:', err)
        messageApi.error('Failed to save user settings')
        await getUserSettings(socket)
        return null
      }
    },
    [getUserSettings, messageApi]
  )

  const connectToServer = useCallback(() => {
    if (token && authenticated == true) {
      logger.debug('Token is available, connecting to api server...')

      const newSocket = io(config.apiServerUrl, {
        reconnectionAttempts: 3,
        timeout: 3000,
        auth: { type: 'user' }
      })

      setConnecting(true)

      newSocket.on('connect', () => {
        logger.debug('Api Server connected')
        newSocket.emit('authenticate', { token: token }, async (result) => {
          if (result?.valid !== true) {
            setConnecting(false)
            setError('Api Server authentication failed')
            newSocket.disconnect()
            return
          }

          setUserSettings(createEmptyUserSettings())
          setUserSettingsLoaded(false)
          await getUserSettings(newSocket)
          if (!newSocket.connected) {
            return
          }
          setConnecting(false)
          setConnected(true)
          setError(null)
          resubscribeActivityListeners(newSocket)
        })
      })

      newSocket.on('objectUpdate', handleObjectUpdate)
      newSocket.on('objectEvent', handleObjectEvent)
      newSocket.on('objectNew', handleObjectNew)
      newSocket.on('objectDelete', handleObjectDelete)
      newSocket.on('activityUpdate', handleActivityUpdate)
      newSocket.on('modelStats', handleModelStats)
      newSocket.on('notification', (data) => {
        let notification
        try {
          notification = typeof data === 'string' ? JSON.parse(data) : data
        } catch (err) {
          logger.error('Failed to parse notification:', err)
          return
        }
        if (!notification) return
        logger.debug('Notification received:', notification)
        notificationListenersRef.current.forEach((cb) => {
          try {
            cb(notification)
          } catch (err) {
            logger.error('Error in notification callback:', err)
          }
        })
      })

      newSocket.on('disconnect', () => {
        logger.debug('Api Server disconnected')
        setError('Api Server disconnected')
        clearSubscriptions()
        setConnected(false)
        setUserSettings(createEmptyUserSettings())
        setUserSettingsLoaded(false)
      })

      newSocket.on('connect_error', (err) => {
        logger.error('Api Server connection error:', err)
        messageApi.error('Api Server connection error: ' + err.message)
        setError('Api Server connection error')
        clearSubscriptions()
        setConnected(false)
        setUserSettings(createEmptyUserSettings())
        setUserSettingsLoaded(false)
      })

      newSocket.on('error', (err) => {
        logger.error('Api Server error:', err)
        setError('Api Server error')
      })

      socketRef.current = newSocket
    }
  }, [
    token,
    authenticated,
    messageApi,
    handleActivityUpdate,
    clearSubscriptions,
    getUserSettings,
    resubscribeActivityListeners
  ])

  useEffect(() => {
    if (token && authenticated == true) {
      connectToServer()
    } else if (!token && socketRef.current) {
      logger.debug('Token not available, disconnecting api server...')
      socketRef.current.disconnect()
      socketRef.current = null
      setUserSettings(createEmptyUserSettings())
      setUserSettingsLoaded(false)
    }

    // Clean up function
    return () => {
      if (socketRef.current) {
        logger.debug('Cleaning up api server connection...')
        socketRef.current.disconnect()
        socketRef.current = null
      }
    }
  }, [token, authenticated, connectToServer])

  useEffect(() => {
    const handlePageVisible = () => {
      if (document.visibilityState !== 'visible') {
        return
      }

      const socket = socketRef.current
      if (!socket || !token || authenticated !== true) {
        return
      }

      if (!socket.connected) {
        logger.debug('Page visible with disconnected socket, reconnecting...')
        socket.connect()
        return
      }

      if (subscribedActivityCallbacksRef.current.size > 0) {
        logger.debug('Page visible, resubscribing activity listeners...')
        subscribedActivityServerSubscriptionsRef.current.clear()
        resubscribeActivityListeners(socket)
      }
    }

    const handlePageShow = (event) => {
      if (!event.persisted) {
        return
      }

      logger.debug('Page restored from bfcache, reconnecting socket...')
      subscribedActivityServerSubscriptionsRef.current.clear()
      setConnected(false)
      if (socketRef.current) {
        socketRef.current.disconnect()
        socketRef.current = null
      }
      connectToServer()
    }

    document.addEventListener('visibilitychange', handlePageVisible)
    window.addEventListener('pageshow', handlePageShow)

    return () => {
      document.removeEventListener('visibilitychange', handlePageVisible)
      window.removeEventListener('pageshow', handlePageShow)
    }
  }, [token, authenticated, connectToServer, resubscribeActivityListeners])

  const setObjectActivity = (id, type, mode) => {
    logger.debug('Setting activity for', id, mode)
    if (socketRef.current && socketRef.current.connected) {
      return new Promise((resolve) => {
        socketRef.current.emit(
          'setActivity',
          { _id: id, type, mode },
          (result) => {
            logger.debug('Received setActivity result for object:', id, result)
            resolve(result || { success: true })
          }
        )
      })
    }
    return Promise.resolve({ success: false, error: 'not_connected' })
  }

  const clearObjectActivity = (id, type) => {
    logger.debug('Clearing activity for', id)
    if (socketRef.current && socketRef.current.connected == true) {
      socketRef.current.emit('clearActivity', { _id: id, type })
      logger.debug('Sent clearActivity command for object:', id)
    }
  }

  const fetchObjectActivities = async (id, type) => {
    if (socketRef.current && socketRef.current.connected == true) {
      logger.debug('Fetching activities for ' + id)
      return new Promise((resolve) => {
        socketRef.current.emit(
          'getActivities',
          {
            _id: id,
            type: type
          },
          (result) => {
            logger.debug('Received activities for object:', id, result)
            resolve(result?.activities || [])
          }
        )
      })
    }
    return []
  }

  const handleObjectUpdate = async (data) => {
    logger.debug('Notifying object update:', data)
    const id = data._id
    const objectType = data.objectType

    const callbacksRefKey = `${objectType}:${id}`

    if (
      id &&
      objectType &&
      subscribedCallbacksRef.current.has(callbacksRefKey)
    ) {
      const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} callbacks for object:`,
        callbacksRefKey
      )
      callbacks.forEach((callback) => {
        try {
          callback(data.object)
        } catch (error) {
          logger.error('Error in object update callback:', error)
        }
      })
    } else {
      logger.debug(
        `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

  const handleObjectEvent = async (data) => {
    const id = data._id
    const objectType = data.objectType

    const callbacksRefKey = `${objectType}:${id}:events:${data.event.type}`
    logger.debug('Notifying object event:', data)
    if (
      id &&
      objectType &&
      subscribedCallbacksRef.current.has(callbacksRefKey)
    ) {
      const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} callbacks for object:`,
        callbacksRefKey
      )
      callbacks.forEach((callback) => {
        try {
          callback(data.event)
        } catch (error) {
          logger.error('Error in object event callback:', error)
        }
      })
    } else {
      logger.debug(
        `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

  const handleModelStats = async (data) => {
    const objectType = data.objectType || 'unknown'

    const callbacksRefKey = `modelStats:${objectType}`
    logger.debug('Notifying model stats update:', data)
    if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
      const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} callbacks for model stats:`,
        callbacksRefKey
      )
      callbacks.forEach((callback) => {
        try {
          callback(data.stats)
        } catch (error) {
          logger.error('Error in model stats callback:', error)
        }
      })
    } else {
      logger.debug(
        `No callbacks found for model stats: ${callbacksRefKey}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

  const handleObjectNew = async (data) => {
    logger.debug('Notifying object new:', data)
    const objectType = data.objectType || 'unknown'
    const callbacksRefKey = getObjectTypeSubscriptionKey(
      objectType,
      data.filter || {}
    )

    if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
      const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} callbacks for type:`,
        callbacksRefKey
      )
      callbacks.forEach((callback) => {
        try {
          callback(data.object)
        } catch (error) {
          logger.error('Error in object new callback:', error)
        }
      })
    } else {
      logger.debug(
        `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

  const handleObjectDelete = async (data) => {
    logger.debug('Notifying object delete:', data)
    const objectType = data.objectType || 'unknown'
    const callbacksRefKey = getObjectTypeSubscriptionKey(
      objectType,
      data.filter || {}
    )

    if (objectType && subscribedCallbacksRef.current.has(callbacksRefKey)) {
      const callbacks = subscribedCallbacksRef.current.get(callbacksRefKey)
      logger.debug(
        `Calling ${callbacks.length} callbacks for type:`,
        callbacksRefKey
      )
      callbacks.forEach((callback) => {
        try {
          callback(data.object)
        } catch (error) {
          logger.error('Error in object new callback:', error)
        }
      })
    } else {
      logger.debug(
        `No callbacks found for object: ${callbacksRefKey}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

  const offObjectUpdatesEvent = useCallback((id, objectType, callback) => {
    if (socketRef.current && socketRef.current.connected == true) {
      const callbacksRefKey = `${objectType}:${id}`
      // Remove callback from the subscribed callbacks map
      if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
        const callbacks = subscribedCallbacksRef.current
          .get(callbacksRefKey)
          .filter((cb) => cb !== callback)
        if (callbacks.length === 0) {
          logger.debug(
            'No callbacks found for object:',
            callbacksRefKey,
            'unsubscribing from object update...'
          )
          subscribedCallbacksRef.current.delete(callbacksRefKey)
          socketRef.current.emit('unsubscribeObjectUpdate', {
            _id: id,
            objectType: objectType
          })
        } else {
          subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
        }
      }
    }
  }, [])

  const offObjectTypeUpdatesEvent = useCallback(
    (objectType, filter, callback) => {
      if (socketRef.current && socketRef.current.connected == true) {
        const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)
        // Remove callback from the subscribed callbacks map
        if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
          const callbacks = subscribedCallbacksRef.current
            .get(callbacksRefKey)
            .filter((cb) => cb !== callback)
          if (callbacks.length === 0) {
            subscribedCallbacksRef.current.delete(callbacksRefKey)
            socketRef.current.emit('unsubscribeObjectTypeUpdate', {
              objectType: objectType,
              filter: filter
            })
          } else {
            subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
          }
        }
      }
    },
    []
  )

  const subscribeToObjectUpdates = useCallback(
    (id, objectType, callback) => {
      if (socketRef.current && socketRef.current.connected == true) {
        const callbacksRefKey = `${objectType}:${id}`
        // Add callback to the subscribed callbacks map immediately
        if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
          subscribedCallbacksRef.current.set(callbacksRefKey, [])
        }

        const callbacksLength =
          subscribedCallbacksRef.current.get(callbacksRefKey).length

        if (callbacksLength <= 0) {
          socketRef.current.emit(
            'subscribeToObjectUpdate',
            {
              _id: id,
              objectType: objectType
            },
            (result) => {
              if (result.success) {
                logger.info('Subscribed to id:', id, 'objectType:', objectType)
              }
            }
          )
        }
        logger.info(
          'Adding callback id:',
          id,
          'objectType:',
          objectType,
          'callbacks length:',
          callbacksLength + 1
        )
        subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)

        // Return cleanup function
        return () => offObjectUpdatesEvent(id, objectType, callback)
      }
    },
    [offObjectUpdatesEvent]
  )

  // Subscribe to user profile updates when WebSocket is connected and userProfile._id exists
  useEffect(() => {
    if (connected && userProfile?._id) {
      const unsubscribe = subscribeToObjectUpdates(
        userProfile._id,
        'user',
        (updatedUser) => {
          logger.debug('Notifying user profile update:', updatedUser)
          setUserProfile((prev) =>
            prev && updatedUser ? { ...prev, ...updatedUser } : prev
          )
        }
      )
      return () => {
        if (unsubscribe) unsubscribe()
      }
    }
  }, [connected, userProfile?._id, subscribeToObjectUpdates, setUserProfile])

  const subscribeToObjectTypeUpdates = useCallback(
    (objectType, filterOrCallback = {}, maybeCallback) => {
      const { filter, callback } = getObjectTypeSubscriptionArgs(
        filterOrCallback,
        maybeCallback
      )
      const callbacksRefKey = getObjectTypeSubscriptionKey(objectType, filter)

      logger.debug('Subscribing to type updates:', objectType, filter)
      if (socketRef.current && socketRef.current.connected == true) {
        // Add callback to the subscribed callbacks map immediately
        if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
          subscribedCallbacksRef.current.set(callbacksRefKey, [])
        }

        const callbacksLength =
          subscribedCallbacksRef.current.get(callbacksRefKey).length

        if (callbacksLength <= 0) {
          socketRef.current.emit(
            'subscribeToObjectTypeUpdate',
            { objectType: objectType, filter: filter },
            (result) => {
              if (result.success) {
                logger.info('Subscribed to objectType:', objectType, filter)
              }
            }
          )
        }

        subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)
        logger.debug(
          `Added callback for type ${callbacksRefKey}, total callbacks: ${callbacksLength + 1}`
        )
        logger.debug(
          'Registered type event listener for object:',
          callbacksRefKey
        )

        // Return cleanup function
        return () => offObjectTypeUpdatesEvent(objectType, filter, callback)
      }
    },
    [offObjectTypeUpdatesEvent]
  )

  const offActivityEvent = useCallback((id, objectType, callback) => {
    const callbacksRefKey = `${objectType}:${id}`

    if (!subscribedActivityCallbacksRef.current.has(callbacksRefKey)) {
      return
    }

    const callbacks = subscribedActivityCallbacksRef.current
      .get(callbacksRefKey)
      .filter((cb) => cb !== callback)

    if (callbacks.length === 0) {
      subscribedActivityCallbacksRef.current.delete(callbacksRefKey)
      subscribedActivityServerSubscriptionsRef.current.delete(callbacksRefKey)

      if (socketRef.current?.connected) {
        socketRef.current.emit('unsubscribeObjectActivity', {
          _id: id,
          objectType
        })
      }
    } else {
      subscribedActivityCallbacksRef.current.set(callbacksRefKey, callbacks)
    }

    logger.debug('Removed activity event listener for object:', id)
  }, [])

  const offObjectEventEvent = useCallback(
    (id, objectType, eventType, callback) => {
      if (socketRef.current && socketRef.current.connected == true) {
        const callbacksRefKey = `${objectType}:${id}:events:${eventType}`

        // Remove callback from the subscribed callbacks map
        if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
          const callbacks = subscribedCallbacksRef.current
            .get(callbacksRefKey)
            .filter((cb) => cb !== callback)
          if (callbacks.length === 0) {
            subscribedCallbacksRef.current.delete(callbacksRefKey)
            socketRef.current.emit('unsubscribeObjectEvent', {
              _id: id,
              objectType,
              eventType
            })
          } else {
            subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
          }
        }
      }
    },
    []
  )

  const subscribeToObjectEvent = useCallback(
    (id, objectType, eventType, callback) => {
      if (socketRef.current && socketRef.current.connected == true) {
        const callbacksRefKey = `${objectType}:${id}:events:${eventType}`
        // Add callback to the subscribed callbacks map immediately
        if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
          subscribedCallbacksRef.current.set(callbacksRefKey, [])
        }

        const callbacksLength =
          subscribedCallbacksRef.current.get(callbacksRefKey).length

        if (callbacksLength <= 0) {
          socketRef.current.emit(
            'subscribeToObjectEvent',
            {
              _id: id,
              objectType: objectType,
              eventType: eventType
            },
            (result) => {
              if (result.success) {
                logger.info(
                  'Subscribed to event id:',
                  id,
                  'objectType:',
                  objectType,
                  'eventType:',
                  eventType
                )
              }
            }
          )
        }
        logger.info(
          'Adding event callback id:',
          id,
          'objectType:',
          objectType,
          'eventType:',
          eventType,
          'callbacks length:',
          callbacksLength + 1
        )
        subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)

        // Return cleanup function
        return () => offObjectEventEvent(id, objectType, eventType, callback)
      }
    },
    [offObjectEventEvent]
  )

  const offModelStats = useCallback((objectType, callback) => {
    if (socketRef.current && socketRef.current.connected == true) {
      const callbacksRefKey = `modelStats:${objectType}`

      // Remove callback from the subscribed callbacks map
      if (subscribedCallbacksRef.current.has(callbacksRefKey)) {
        const callbacks = subscribedCallbacksRef.current
          .get(callbacksRefKey)
          .filter((cb) => cb !== callback)
        if (callbacks.length === 0) {
          subscribedCallbacksRef.current.delete(callbacksRefKey)
          socketRef.current.emit('unsubscribeModelStats', {
            objectType
          })
        } else {
          subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
        }
      }
    }
  }, [])

  const subscribeToModelStats = useCallback(
    (objectType, callback) => {
      if (socketRef.current && socketRef.current.connected == true) {
        const callbacksRefKey = `modelStats:${objectType}`
        // Add callback to the subscribed callbacks map immediately
        if (!subscribedCallbacksRef.current.has(callbacksRefKey)) {
          subscribedCallbacksRef.current.set(callbacksRefKey, [])
        }

        const callbacksLength =
          subscribedCallbacksRef.current.get(callbacksRefKey).length

        if (callbacksLength <= 0) {
          socketRef.current.emit(
            'subscribeToModelStats',
            {
              objectType: objectType
            },
            (result) => {
              if (result.success) {
                logger.info('Subscribed to model stats:', objectType)
              }
            }
          )
        }
        logger.info(
          'Adding model stats callback:',
          objectType,
          'callbacks length:',
          callbacksLength + 1
        )
        subscribedCallbacksRef.current.get(callbacksRefKey).push(callback)

        // Return cleanup function
        return () => offModelStats(objectType, callback)
      }
    },
    [offModelStats]
  )

  const subscribeToObjectActivity = useCallback(
    (id, type, callback) => {
      logger.debug('Subscribing to activity for object:', id, 'type:', type)
      if (!id || !type) {
        return undefined
      }

      const callbacksRefKey = `${type}:${id}`
      if (!subscribedActivityCallbacksRef.current.has(callbacksRefKey)) {
        subscribedActivityCallbacksRef.current.set(callbacksRefKey, [])
      }

      subscribedActivityCallbacksRef.current.get(callbacksRefKey).push(callback)

      const socket = socketRef.current
      if (
        socket?.connected &&
        !subscribedActivityServerSubscriptionsRef.current.has(callbacksRefKey)
      ) {
        socket.emit(
          'subscribeToObjectActivity',
          { _id: id, objectType: type },
          (result) => {
            if (result?.success) {
              subscribedActivityServerSubscriptionsRef.current.add(
                callbacksRefKey
              )
              if (Array.isArray(result.activities)) {
                subscribedActivityCallbacksRef.current
                  .get(callbacksRefKey)
                  ?.forEach((activityCallback) => {
                    try {
                      activityCallback(null, result.activities)
                    } catch (error) {
                      logger.error(
                        'Error in activity subscribe callback:',
                        error
                      )
                    }
                  })
              }
            }
          }
        )
      }

      logger.debug('Registered activity event listener for object:', id)

      return () => offActivityEvent(id, type, callback)
    },
    [offActivityEvent]
  )

  const showError = useCallback(
    (error, callback = null) => {
      const code = error.response.data.code || 'UNKNOWN'
      if (code == 'UNAUTHORIZED') {
        setUnauthenticated()
        return
      }
      var content = `Error ${error.code} (${error.status}): ${error.message}`
      if (error.response?.data?.error) {
        content = `${error.response?.data?.error} (${error.status})`
      }
      setErrorModalContent(content)
      setRetryCallback(() => callback)
      setShowErrorModal(true)
    },
    [setUnauthenticated]
  )

  const handleRetry = () => {
    setShowErrorModal(false)
    setErrorModalContent('')
    if (retryCallback) {
      retryCallback()
    }
    setRetryCallback(null)
  }

  // Generalized fetchObject function
  const fetchObject = async (id, type) => {
    const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
    setFetchLoading(true)
    logger.debug('Fetching from ' + fetchUrl)
    try {
      const response = await axios.get(fetchUrl, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      setFetchLoading(false)
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchObject(id, type)
      })
      return {}
    }
  }

  // Fetch table data with pagination, filtering, and sorting
  const fetchObjects = async (type, params = {}) => {
    const {
      page = 1,
      limit = 25,
      filter = {},
      sorter = {},
      onDataChange
    } = params

    let newFilter = { ...filter }

    if (filter != null && Object.keys(filter).length > 0) {
      const model = getModelByName(type)

      if (model.name !== 'unknown') {
        for (const key of Object.keys(filter)) {
          const property = model?.properties?.find((p) => p.name === key)
          if (property && property.type === 'object') {
            const value = filter[key]
            newFilter[`${key}._id`] = value?._id ?? value
            delete newFilter[key]
          }
        }
      }
    }

    logger.debug('Fetching table data from:', type, {
      page,
      limit,
      newFilter,
      sorter
    })

    try {
      const response = await axios.get(
        `${config.backendUrl}/${getObjectEndpoint(type)}`,
        {
          params: {
            page,
            limit,
            ...newFilter,
            sort: sorter.field,
            order: sorter.order
          },
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )

      const newData = response.data
      const totalCount = parseInt(response.headers['x-total-count'] || '0', 10)
      const totalPages = Math.ceil(totalCount / limit)
      const hasMore = newData.length >= limit

      if (onDataChange) {
        onDataChange(newData)
      }

      return {
        data: newData,
        totalCount,
        totalPages,
        hasMore,
        page
      }
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchObjects(type, params)
      })
      return []
    }
  }

  const getObjectNeighbors = async (type, params = {}) => {
    const { id, filter = {}, sorter = {} } = params

    let newFilter = { ...filter }

    if (filter != null && Object.keys(filter).length > 0) {
      const model = getModelByName(type)

      if (model.name !== 'unknown') {
        for (const key of Object.keys(filter)) {
          const property = model?.properties?.find((p) => p.name === key)
          if (property && property.type === 'object') {
            const value = filter[key]
            newFilter[`${key}._id`] = value?._id ?? value
            delete newFilter[key]
          }
        }
      }
    }

    logger.debug('Fetching object neighbors from:', type, {
      id,
      newFilter,
      sorter
    })

    try {
      const response = await axios.get(
        `${config.backendUrl}/${getObjectEndpoint(type)}/neighbors`,
        {
          params: {
            id,
            ...newFilter,
            sort: sorter.field,
            order: sorter.order
          },
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )

      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        getObjectNeighbors(type, params)
      })
      return { next: null, previous: null }
    }
  }

  // Fetch table data with pagination, filtering, and sorting
  const fetchObjectsByProperty = async (type, params = {}) => {
    const { filter = {}, properties = [], masterFilter = {} } = params

    logger.debug('Fetching property object data from:', type, {
      properties,
      filter
    })

    try {
      const response = await axios.get(
        `${config.backendUrl}/${getObjectEndpoint(type)}/properties`,
        {
          params: {
            ...Object.keys(filter).reduce((acc, key) => {
              acc[key] = Array.isArray(filter[key])
                ? filter[key].join(',')
                : filter[key]
              return acc
            }, {}),
            properties: properties.join(','), // Convert array to comma-separated string
            masterFilter: JSON.stringify(masterFilter)
          },
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )

      const newData = response.data

      return newData
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchObjects(type, params)
      })

      return []
    }
  }

  const searchObjects = async (type, search) => {
    const searchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/search`
    logger.debug('Searching objects for ' + type, { search })
    try {
      const response = await axios.get(searchUrl, {
        params: { search },
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        searchObjects(type, search)
      })
      return []
    }
  }

  // Update filament information
  const updateObject = async (id, type, value) => {
    const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
    logger.debug('Updating info for ' + id)
    try {
      const response = await axios.put(updateUrl, value, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      logger.debug('Object updated successfully')
      return response.data
    } catch (err) {
      console.error(err)
      setError(err, () => {
        updateObject(id, type, value)
      })
      return {}
    }
  }

  // Update multiple objects
  const updateMultipleObjects = async (type, objects) => {
    const updateUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
    logger.debug('Updating multiple objects for ' + type)
    try {
      const response = await axios.put(updateUrl, objects, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      logger.debug('Objects updated successfully')
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        updateMultipleObjects(type, objects)
      })
      return []
    }
  }

  // Update filament information
  const deleteObject = async (id, type) => {
    const deleteUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
    logger.debug('Deleting object ID: ' + id)
    try {
      const response = await axios.delete(deleteUrl, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      logger.debug('Object deleted successfully')
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        deleteObject(id, type)
      })
      return {}
    }
  }

  // Update filament information
  const createObject = async (type, value) => {
    const createUrl = `${config.backendUrl}/${getObjectEndpoint(type)}`
    logger.debug('Creating object...')
    try {
      const response = await axios.post(createUrl, value, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        createObject(type, value)
      })
      return {}
    }
  }

  // Call a function on an object
  const sendObjectFunction = async (id, type, functionName, value = {}) => {
    const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
    logger.debug(`Calling object function ${functionName} for ${id} at ${url}`)
    try {
      const response = await axios.post(url, value, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        sendObjectFunction(id, type, functionName, value)
      })
      return {}
    }
  }

  const getObjectFunction = async (id, type, functionName, params = {}) => {
    const url = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}/${functionName}`
    logger.debug(`Fetching object function ${functionName} for ${id} at ${url}`)
    try {
      const response = await axios.get(url, {
        params,
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        getObjectFunction(id, type, functionName, params)
      })
      return {}
    }
  }

  const getMarketplaceAuthUrl = async (marketplaceId, state) => {
    return getObjectFunction(marketplaceId, 'Marketplace', 'auth/url', {
      state
    })
  }

  const refreshMarketplaceAuth = async (marketplaceId) => {
    return sendObjectFunction(marketplaceId, 'Marketplace', 'auth/refresh')
  }

  // Export list data to CSV and download
  const exportToCsv = async (objectType) => {
    try {
      const response = await axios.get(
        `${config.backendUrl}/csv/${objectType}`,
        {
          responseType: 'blob',
          headers: {
            Accept: 'text/csv',
            Authorization: `Bearer ${token}`
          }
        }
      )
      const blob = new Blob([response.data], { type: 'text/csv' })
      const url = URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = url
      link.download = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.csv`
      document.body.appendChild(link)
      link.click()
      document.body.removeChild(link)
      URL.revokeObjectURL(url)
      message.success('CSV file downloaded')
    } catch (err) {
      console.error(err)
      showError(err, () => exportToCsv(objectType))
    }
  }

  // Export list data to Excel and download or open in Excel
  const exportToExcel = async (objectType, mode = 'download') => {
    try {
      if (mode === 'open') {
        // Open in Excel: get a temporary https URL (Excel fetches it when opening)
        const response = await axios.post(
          `${config.backendUrl}/excel/${objectType}/open`,
          {},
          {
            headers: {
              Accept: 'application/json',
              Authorization: `Bearer ${token}`
            }
          }
        )
        const url = response.data?.url
        if (!url || typeof url !== 'string') {
          throw new Error('No URL received from server')
        }
        const excelUri = `ms-excel:ofe|u|${url}`
        window.location.href = excelUri
        message.success('Opening in Excel')
      } else if (mode === 'download') {
        const response = await axios.get(
          `${config.backendUrl}/excel/${objectType}`,
          {
            responseType: 'blob',
            headers: {
              Accept:
                'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
              Authorization: `Bearer ${token}`
            }
          }
        )
        const blob = new Blob([response.data], {
          type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        })
        const url = URL.createObjectURL(blob)
        const link = document.createElement('a')
        link.href = url
        link.download = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.xlsx`
        document.body.appendChild(link)
        link.click()
        document.body.removeChild(link)
        message.success('Excel file downloaded')
      } else {
        throw new Error('Invalid mode')
      }
    } catch (err) {
      console.error(err)
      showError(err, () => exportToExcel(objectType))
    }
  }

  // Download GCode file content
  const fetchFileContent = async (file, download = false) => {
    try {
      const response = await axios.get(
        `${config.backendUrl}/files/${file._id}/content`,
        {
          headers: {
            Accept: '*/*',
            Authorization: `Bearer ${token}`
          },
          responseType: 'blob'
        }
      )
      const blob = new Blob([response.data], {
        type: response.headers['content-type']
      })
      const fileURL = window.URL.createObjectURL(blob)
      if (download == true) {
        const fileLink = document.createElement('a')
        fileLink.href = fileURL
        fileLink.setAttribute('download', `${file.name}${file.extension}`)
        document.body.appendChild(fileLink)
        fileLink.click()
        fileLink.parentNode.removeChild(fileLink)
        return
      }
      return fileURL
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchFileContent(file, download)
      })
    }
  }

  const fetchFileThumbnail = async (file, size, show404Error = false) => {
    try {
      const response = await axios.get(
        `${config.backendUrl}/files/${file._id}/thumbnail`,
        {
          params: { size },
          headers: {
            Accept: 'image/jpeg',
            Authorization: `Bearer ${token}`
          },
          responseType: 'blob'
        }
      )
      const blob = new Blob([response.data], {
        type: response.headers['content-type'] || 'image/jpeg'
      })
      return window.URL.createObjectURL(blob)
    } catch (err) {
      if (err.response.status === 404 && show404Error == false) {
        return null
      }
      console.error(err)
      showError(err, () => {
        fetchFileThumbnail(file, size)
      })
    }
  }

  // Fetch notes for a specific parent
  const fetchNotes = async (parentId) => {
    logger.debug('Fetching notes for parent:', parentId)
    try {
      const response = await axios.get(`${config.backendUrl}/notes`, {
        params: {
          'parent._id': parentId,
          sort: 'createdAt',
          order: 'ascend'
        },
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })

      const notesData = response.data || []
      logger.debug('Fetched notes:', notesData.length)
      return notesData
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchNotes(parentId)
      })
    }
  }

  const fetchSpotlightData = async (query) => {
    const cached = spotlightCache.get(query)
    if (cached && Date.now() - cached.timestamp < SPOTLIGHT_CACHE_TTL_MS) {
      logger.debug('Returning cached spotlight data for query:', query)
      return cached.data
    }

    const existing = runningSpotlightFetches.get(query)
    if (existing) {
      return existing
    }

    const fetchPromise = (async () => {
      try {
        logger.debug('Fetching spotlight data with query:', query)
        const response = await axios.get(
          `${config.backendUrl}/spotlight/${query}`,
          {
            headers: {
              Accept: 'application/json',
              Authorization: `Bearer ${token}`
            }
          }
        )
        spotlightCache.set(query, {
          data: response.data,
          timestamp: Date.now()
        })
        return response.data
      } catch (err) {
        console.error(err)
        showError(err, () => {
          fetchSpotlightData(query)
        })
      } finally {
        runningSpotlightFetches.delete(query)
      }
    })()

    runningSpotlightFetches.set(query, fetchPromise)
    return fetchPromise
  }

  const getModelStats = async (objectType) => {
    logger.debug('Fetching stats for model type:', objectType)
    try {
      let statsUrl
      if (objectType === 'history') {
        statsUrl = `${config.backendUrl}/stats/history`
      } else {
        statsUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/stats`
      }

      const response = await axios.get(statsUrl, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      logger.debug('Fetched stats for model type:', objectType)
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        getModelStats(objectType)
      })
      return null
    }
  }

  const getModelPropertyValues = async (objectType, property) => {
    logger.debug(
      'Fetching property values for model type:',
      objectType,
      property
    )
    try {
      const response = await axios.get(
        `${config.backendUrl}/${getObjectEndpoint(objectType)}/values`,
        {
          params: { property },
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
      logger.debug(
        'Fetched property values for model type:',
        objectType,
        property
      )
      return Array.isArray(response.data) ? response.data : []
    } catch (err) {
      console.error(err)
      showError(err, () => {
        getModelPropertyValues(objectType, property)
      })
      return []
    }
  }

  const getModelHistory = async (objectType, startDate, endDate) => {
    logger.debug('Fetching history for model type:', objectType)
    const encodedStartDate = encodeURIComponent(startDate.toISOString())
    const encodedEndDate = encodeURIComponent(endDate.toISOString())
    try {
      const historyUrl = `${config.backendUrl}/${getObjectEndpoint(objectType)}/history?from=${encodedStartDate}&to=${encodedEndDate}`

      const response = await axios.get(historyUrl, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      logger.debug('Fetched history for model type:', objectType)

      // Calculate time range in milliseconds
      const timeRangeMs = endDate.getTime() - startDate.getTime()
      const oneHourMs = 60 * 60 * 1000
      const twelveHoursMs = 12 * 60 * 60 * 1000
      const oneDayMs = 24 * 60 * 60 * 1000
      const threeDaysMs = 3 * 24 * 60 * 60 * 1000

      // Determine interval based on time range
      let intervalMinutes = 1 // Default: 1 minute
      if (timeRangeMs > threeDaysMs) {
        intervalMinutes = 60 // Over 1 day: 60 minutes
      } else if (timeRangeMs > oneDayMs) {
        intervalMinutes = 30 // Over 2 days: 30 minutes
      } else if (timeRangeMs > twelveHoursMs) {
        intervalMinutes = 10 // Over 12 hours: 10 minutes
      } else if (timeRangeMs > oneHourMs) {
        intervalMinutes = 5 // Over 1 hour: 5 minutes
      }

      const intervalMs = intervalMinutes * 60 * 1000

      // Filter data to only include points at the specified intervals
      if (response.data && Array.isArray(response.data)) {
        const filteredData = []
        const seenIntervals = new Set()

        response.data.forEach((point) => {
          const pointDate = new Date(point.date)
          // Round down to the nearest interval
          const roundedTime =
            Math.floor(pointDate.getTime() / intervalMs) * intervalMs
          const roundedDate = new Date(roundedTime)
          const intervalKey = roundedDate.toISOString()

          // Only include if we haven't seen this interval yet
          if (!seenIntervals.has(intervalKey)) {
            seenIntervals.add(intervalKey)
            // Update the point's date to the rounded interval
            filteredData.push({
              ...point,
              date: roundedDate.toISOString()
            })
          }
        })

        // Sort by date to ensure chronological order
        filteredData.sort((a, b) => new Date(a.date) - new Date(b.date))

        logger.debug(
          `Filtered history data: ${response.data.length} -> ${filteredData.length} points (${intervalMinutes} min intervals)`
        )
        return filteredData
      }

      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        getModelHistory(objectType)
      })
      return null
    }
  }

  const fetchTemplatePreview = async (
    id,
    content,
    testObject,
    scale,
    callback
  ) => {
    logger.debug('Fetching preview...')
    if (socketRef.current && socketRef.current.connected) {
      return socketRef.current.emit(
        'previewTemplate',
        {
          _id: id,
          content: content,
          testObject: testObject,
          scale: scale
        },
        callback
      )
    }
    if (typeof callback === 'function') {
      callback({ error: 'Api Server disconnected' })
    }
  }

  const fetchTemplatePDF = async (id, content, testObject, callback) => {
    logger.debug('Fetching pdf template...')
    if (socketRef.current && socketRef.current.connected) {
      return socketRef.current.emit(
        'renderTemplatePDF',
        {
          _id: id,
          content: content,
          object: testObject
        },
        callback
      )
    }
    if (typeof callback === 'function') {
      callback({ error: 'Api Server disconnected' })
    }
  }

  const downloadTemplatePDF = async (
    id,
    content,
    object,
    filename,
    callback
  ) => {
    logger.debug('Downloading template PDF...')

    fetchTemplatePDF(id, content, object, (result) => {
      logger.debug('Downloading template PDF result:', result)
      if (result?.error) {
        console.error(result.error)
        if (callback) {
          callback(result.error)
        }
      } else {
        const pdfBlob = new Blob([result.pdf], { type: 'application/pdf' })
        const pdfUrl = URL.createObjectURL(pdfBlob)
        const fileLink = document.createElement('a')
        fileLink.href = pdfUrl
        fileLink.setAttribute('download', `${filename}.pdf`)
        document.body.appendChild(fileLink)
        fileLink.click()
        fileLink.parentNode.removeChild(fileLink)
        if (callback) {
          callback()
        }
      }
    })
  }

  const fetchHostOTP = async (id, callback) => {
    logger.debug('Fetching host OTP...')
    if (socketRef.current && socketRef.current.connected) {
      return socketRef.current.emit(
        'generateHostOtp',
        {
          _id: id
        },
        callback
      )
    }
  }

  const sendObjectAction = async (id, objectType, action, callback) => {
    logger.debug('Sending object action...')
    if (socketRef.current && socketRef.current.connected) {
      return socketRef.current.emit(
        'objectAction',
        {
          _id: id,
          objectType: objectType,
          action: action
        },
        callback
      )
    }
  }

  // Upload file to the API
  const uploadFile = async (
    file,
    additionalData = {},
    progressCallback = null
  ) => {
    const uploadUrl = `${config.backendUrl}/files`
    logger.debug('Uploading file:', file.name, 'to:', uploadUrl)

    try {
      const formData = new FormData()
      formData.append('file', file)

      // Add any additional data to the form
      Object.keys(additionalData).forEach((key) => {
        formData.append(key, additionalData[key])
      })

      const response = await axios.post(uploadUrl, formData, {
        headers: {
          'Content-Type': 'multipart/form-data',
          Authorization: `Bearer ${token}`
        },
        onUploadProgress: (progressEvent) => {
          const percentCompleted = Math.round(
            (progressEvent.loaded * 100) / progressEvent.total
          )
          logger.debug(`Upload progress: ${percentCompleted}%`)
          if (progressCallback) {
            progressCallback(percentCompleted)
          }
        }
      })

      logger.debug('File uploaded successfully:', response.data)
      return response.data
    } catch (err) {
      console.error('File upload error:', err)
      showError(err, () => {
        uploadFile(file, additionalData, progressCallback)
      })
      return null
    }
  }

  const createUserNotifier = async (objectId, objectType) => {
    if (!userProfile?._id) return null
    return createObject('userNotifier', {
      user: userProfile._id,
      object: objectId,
      objectType
    })
  }

  const deleteUserNotifier = async (userNotifierId) => {
    return deleteObject(userNotifierId, 'userNotifier')
  }

  const fetchUserNotifiersForObject = async (objectId, objectType) => {
    if (!userProfile?._id) return { data: [] }
    const result = await fetchObjects('userNotifier', {
      filter: {
        user: userProfile._id,
        object: objectId,
        objectType
      },
      limit: 1
    })
    return result || { data: [] }
  }

  const fetchAllUserNotifiersForObject = async (objectId, objectType) => {
    const result = await fetchObjects('userNotifier', {
      filter: {
        object: objectId,
        objectType
      },
      limit: 100
    })
    return result || { data: [] }
  }

  const toggleUserNotifier = async (objectId, objectType) => {
    const { data } = await fetchUserNotifiersForObject(objectId, objectType)
    const existing = data?.[0]
    if (existing) {
      await deleteUserNotifier(existing._id)
      return false
    } else {
      await createUserNotifier(objectId, objectType)
      return true
    }
  }

  const editUserNotifier = async (userNotifierId, updates) => {
    try {
      const result = await updateObject(userNotifierId, 'userNotifier', updates)
      return result
    } catch (err) {
      console.error(err)
      showError(err, () => editUserNotifier(userNotifierId, updates))
      return null
    }
  }

  const fetchNotificationsApi = useCallback(async () => {
    const response = await axios.get(`${config.backendUrl}/notifications`, {
      params: { limit: 100, sort: 'createdAt', order: 'descend' },
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${token}`
      }
    })
    return Array.isArray(response.data) ? response.data : []
  }, [token])

  const markNotificationAsReadApi = useCallback(
    async (notificationId) => {
      await axios.put(
        `${config.backendUrl}/notifications/${notificationId}/read`,
        {},
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
    },
    [token]
  )

  const markAllNotificationsAsReadApi = useCallback(async () => {
    await axios.put(
      `${config.backendUrl}/notifications/read-all`,
      {},
      {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      }
    )
  }, [token])

  const deleteNotificationApi = useCallback(
    async (notificationId) => {
      await axios.delete(
        `${config.backendUrl}/notifications/${notificationId}`,
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
    },
    [token]
  )

  const deleteAllNotificationsApi = useCallback(async () => {
    await axios.delete(`${config.backendUrl}/notifications`, {
      headers: {
        Accept: 'application/json',
        Authorization: `Bearer ${token}`
      }
    })
  }, [token])

  const completeAppLaunchSession = useCallback(
    async (launchSession) => {
      const response = await axios.post(
        `${config.backendUrl}/applaunch/${launchSession}`,
        {},
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
      return response.data
    },
    [token]
  )

  useEffect(() => {
    const launchSession = new URLSearchParams(location.search).get(
      'launchSession'
    )

    if (
      authenticated !== true ||
      !token ||
      !launchSession ||
      completedLaunchSessionsRef.current.has(launchSession)
    ) {
      return
    }

    completedLaunchSessionsRef.current.add(launchSession)

    completeAppLaunchSession(launchSession)
      .then(() => {
        const searchParams = new URLSearchParams(location.search)
        searchParams.delete('launchSession')
        const newSearch = searchParams.toString()
        const newPath = location.pathname + (newSearch ? `?${newSearch}` : '')
        navigate(newPath, { replace: true })
      })
      .catch((err) => {
        logger.error('Failed to complete app launch session:', err)
        completedLaunchSessionsRef.current.delete(launchSession)
      })
  }, [
    token,
    authenticated,
    completeAppLaunchSession,
    location.search,
    location.pathname,
    navigate
  ])

  const getAppLaunchSession = useCallback(async (launchSession) => {
    const response = await axios.get(
      `${config.backendUrl}/applaunch/${launchSession}`,
      {
        headers: {
          Accept: 'application/json'
        }
      }
    )
    return response.data
  }, [])

  const fetchApiServerVersion = useCallback(async () => {
    try {
      const response = await axios.get(`${config.backendUrl}/server/version`, {
        headers: {
          Accept: 'application/json',
          Authorization: `Bearer ${token}`
        }
      })
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchApiServerVersion()
      })
      return null
    }
  }, [token, showError])

  const fetchWsServerVersion = useCallback(async () => {
    try {
      if (socketRef.current && socketRef.current.connected) {
        return await new Promise((resolve) => {
          socketRef.current.emit('getServerVersion', {}, resolve)
        })
      }
      return null
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchWsServerVersion()
      })
      return null
    }
  }, [showError])

  const fetchAppUpdateBranches = useCallback(async () => {
    try {
      const response = await axios.get(
        `${config.backendUrl}/appupdate/branches`,
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
      return Array.isArray(response.data?.branches)
        ? response.data.branches
        : []
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchAppUpdateBranches()
      })
      return []
    }
  }, [token, showError])

  const fetchAppUpdateCurrent = useCallback(
    async (branch) => {
      try {
        const response = await axios.get(
          `${config.backendUrl}/appupdate/current`,
          {
            params: { branch },
            headers: {
              Accept: 'application/json',
              Authorization: `Bearer ${token}`
            }
          }
        )
        return response.data
      } catch (err) {
        console.error(err)
        showError(err, () => {
          fetchAppUpdateCurrent(branch)
        })
        return null
      }
    },
    [token, showError]
  )

  const flushFile = async (id) => {
    logger.debug('Flushing file...')
    try {
      const response = await axios.delete(
        `${config.backendUrl}/files/${id}/flush`,
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )

      logger.debug('Flushed file:', response.data)
      return true
    } catch (err) {
      console.error(err)
      showError(err, () => {
        flushFile(id)
      })
    }
  }

  // Sanitize a string so it is safe to use as a filename on most file systems
  const formatFileName = (name) => {
    if (!name || typeof name !== 'string') {
      return ''
    }

    // Remove characters that are problematic on most common file systems
    const cleaned = name.replace(/[^a-zA-Z0-9.\-_\s]/g, '')

    // Normalize whitespace to single underscores
    const normalized = cleaned.trim().replace(/\s+/g, '_')

    // Most file systems limit filenames to 255 characters
    return normalized.slice(0, 255)
  }

  return (
    <ApiServerContext.Provider
      value={{
        apiServer: socketRef.current,
        error,
        connecting,
        connected,
        userSettings,
        userSettingsLoaded,
        getUserSettings,
        updateUserSettings,
        setObjectActivity,
        clearObjectActivity,
        fetchObjectActivities,
        updateObject,
        updateMultipleObjects,
        createObject,
        getObjectFunction,
        sendObjectFunction,
        deleteObject,
        subscribeToObjectUpdates,
        subscribeToObjectEvent,
        subscribeToObjectTypeUpdates,
        subscribeToObjectActivity,
        subscribeToModelStats,
        fetchObject,
        fetchObjects,
        getObjectNeighbors,
        fetchObjectsByProperty,
        searchObjects,
        fetchSpotlightData,
        getModelStats,
        getModelPropertyValues,
        getModelHistory,
        fetchLoading,
        showError,
        fetchFileContent,
        fetchFileThumbnail,
        exportToExcel,
        exportToCsv,
        fetchTemplatePreview,
        fetchTemplatePDF,
        fetchNotes,
        downloadTemplatePDF,
        fetchHostOTP,
        sendObjectAction,
        uploadFile,
        flushFile,
        formatFileName,
        createUserNotifier,
        deleteUserNotifier,
        fetchUserNotifiersForObject,
        fetchAllUserNotifiersForObject,
        toggleUserNotifier,
        editUserNotifier,
        fetchNotificationsApi,
        markNotificationAsReadApi,
        markAllNotificationsAsReadApi,
        deleteNotificationApi,
        deleteAllNotificationsApi,
        registerNotificationListener,
        unregisterNotificationListener,
        getMarketplaceAuthUrl,
        refreshMarketplaceAuth,
        completeAppLaunchSession,
        getAppLaunchSession,
        fetchAppUpdateBranches,
        fetchAppUpdateCurrent,
        fetchWsServerVersion,
        fetchApiServerVersion
      }}
    >
      {contextHolder}
      {children}
      <Modal
        title={
          <Space size={'middle'}>
            <ExclamationOctagonIcon />
            Error
          </Space>
        }
        open={showErrorModal}
        okText='OK'
        style={{ maxWidth: 430 }}
        closable={false}
        centered
        maskClosable={true}
        footer={[
          <Button
            key='retry'
            onClick={() => {
              setShowErrorModal(false)
            }}
          >
            Close
          </Button>,
          <Button key='retry' icon={<ReloadIcon />} onClick={handleRetry}>
            Retry
          </Button>
        ]}
      >
        {errorModalContent}
      </Modal>
    </ApiServerContext.Provider>
  )
}

ApiServerProvider.propTypes = {
  children: PropTypes.node.isRequired
}

export { ApiServerContext, ApiServerProvider }
