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

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

const ApiServerContext = createContext()

const ApiServerProvider = ({ children }) => {
  const { token, userProfile, 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 [notificationApi] = notification.useNotification()
  const [fetchLoading, setFetchLoading] = useState(false)
  const [showErrorModal, setShowErrorModal] = useState(false)
  const [errorModalContent, setErrorModalContent] = useState('')
  const [retryCallback, setRetryCallback] = useState(null)
  const subscribedCallbacksRef = useRef(new Map())
  const subscribedLockCallbacksRef = useRef(new Map())

  const handleLockUpdate = useCallback(
    async (lockData) => {
      logger.debug('Notifying lock update:', lockData)
      const objectId = lockData._id || lockData.id

      if (
        objectId &&
        subscribedLockCallbacksRef.current.has(objectId) &&
        lockData.user != userProfile?._id
      ) {
        const callbacks = subscribedLockCallbacksRef.current.get(objectId)
        logger.debug(
          `Calling ${callbacks.length} lock callbacks for object:`,
          objectId
        )
        callbacks.forEach((callback) => {
          try {
            callback(lockData)
          } catch (error) {
            logger.error('Error in lock update callback:', error)
          }
        })
      } else {
        logger.debug(
          `No lock callbacks found for object: ${objectId}, subscribed lock callbacks:`,
          Array.from(subscribedLockCallbacksRef.current.keys())
        )
      }
    },
    [userProfile?._id]
  )

  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 }, (result) => {
          console.log('Auth result', result)
          setConnecting(false)
          setConnected(true)
          setError(null)
        })
      })

      newSocket.on('objectUpdate', handleObjectUpdate)
      newSocket.on('objectEvent', handleObjectEvent)
      newSocket.on('objectNew', handleObjectNew)
      newSocket.on('objectDelete', handleObjectDelete)
      newSocket.on('lockUpdate', handleLockUpdate)

      newSocket.on('disconnect', () => {
        logger.debug('Api Server disconnected')
        setError('Api Server disconnected')
        setConnected(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')
        setConnected(false)
      })

      newSocket.on('bridge.notification', (data) => {
        notificationApi[data.type]({
          title: data.title,
          message: data.message
        })
      })

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

      socketRef.current = newSocket
    }
  }, [token, authenticated, messageApi, notificationApi, handleLockUpdate])

  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
    }

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

  const lockObject = (id, type) => {
    logger.debug('Locking ' + id)
    if (socketRef.current && socketRef.current.connected) {
      socketRef.current.emit('lock', { _id: id, type: type })
      logger.debug('Sent lock command for object:', id)
    }
  }

  const unlockObject = (id, type) => {
    logger.debug('Unlocking ' + id)
    if (socketRef.current && socketRef.current.connected == true) {
      socketRef.current.emit('unlock', { _id: id, type: type })
      logger.debug('Sent unlock command for object:', id)
    }
  }

  const fetchObjectLock = async (id, type) => {
    if (socketRef.current && socketRef.current.connected == true) {
      logger.debug('Fetching lock status for ' + id)
      return new Promise((resolve) => {
        socketRef.current.emit(
          'getLock',
          {
            _id: id,
            type: type
          },
          (lockEvent) => {
            logger.debug('Received lock status for object:', id, lockEvent)
            if (lockEvent.user != userProfile?._id) {
              resolve(lockEvent)
            } else {
              resolve(null)
            }
          }
        )
        logger.debug('Sent fetch lock command for object:', id)
      })
    }
  }

  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 handleObjectNew = async (data) => {
    logger.debug('Notifying object new:', data)
    const objectType = data.objectType || 'unknown'

    if (objectType && subscribedCallbacksRef.current.has(objectType)) {
      const callbacks = subscribedCallbacksRef.current.get(objectType)
      logger.debug(
        `Calling ${callbacks.length} callbacks for type:`,
        objectType
      )
      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: ${objectType}, subscribed callbacks:`,
        Array.from(subscribedCallbacksRef.current.keys())
      )
    }
  }

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

    if (objectType && subscribedCallbacksRef.current.has(objectType)) {
      const callbacks = subscribedCallbacksRef.current.get(objectType)
      logger.debug(
        `Calling ${callbacks.length} callbacks for type:`,
        objectType
      )
      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: ${objectType}, 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) {
          subscribedCallbacksRef.current.delete(callbacksRefKey)
          socketRef.current.emit('unsubscribeObjectUpdate', {
            id: id,
            objectType: objectType
          })
        } else {
          subscribedCallbacksRef.current.set(callbacksRefKey, callbacks)
        }
      }
    }
  }, [])

  const offObjectTypeUpdatesEvent = useCallback((objectType, callback) => {
    if (socketRef.current && socketRef.current.connected == true) {
      // Remove callback from the subscribed callbacks map
      console.log(
        'Unsubscribing from type',
        objectType,
        subscribedCallbacksRef.current.has(objectType)
      )
      if (subscribedCallbacksRef.current.has(objectType)) {
        const callbacks = subscribedCallbacksRef.current
          .get(objectType)
          .filter((cb) => cb !== callback)
        console.log('API: CALLBACKS', callbacks)

        if (callbacks.length === 0) {
          subscribedCallbacksRef.current.delete(objectType)
          socketRef.current.emit('unsubscribeObjectTypeUpdate', {
            objectType: objectType
          })
        } else {
          subscribedCallbacksRef.current.set(objectType, 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]
  )

  const subscribeToObjectTypeUpdates = useCallback(
    (objectType, callback) => {
      logger.debug('Subscribing to type updates:', objectType)
      if (socketRef.current && socketRef.current.connected == true) {
        // Add callback to the subscribed callbacks map immediately
        if (!subscribedCallbacksRef.current.has(objectType)) {
          subscribedCallbacksRef.current.set(objectType, [])
        }
        subscribedCallbacksRef.current.get(objectType).push(callback)
        logger.debug(
          `Added callback for type ${objectType}, total callbacks: ${subscribedCallbacksRef.current.get(objectType).length}`
        )

        socketRef.current.emit(
          'subscribeToObjectTypeUpdate',
          { objectType: objectType },
          (result) => {
            if (result.success) {
              logger.info('Subscribed to objectType:', objectType)
            }
          }
        )
        logger.debug('Registered type event listener for object:', objectType)

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

  const offLockEvent = useCallback((id, callback) => {
    if (socketRef.current && socketRef.current.connected == true) {
      // Remove callback from the subscribed lock callbacks map
      if (subscribedLockCallbacksRef.current.has(id)) {
        const callbacks = subscribedLockCallbacksRef.current
          .get(id)
          .filter((cb) => cb !== callback)
        if (callbacks.length === 0) {
          subscribedLockCallbacksRef.current.delete(id)
        } else {
          subscribedLockCallbacksRef.current.set(id, callbacks)
        }
      }

      logger.debug('Removed lock 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)
      }
    },
    [offObjectUpdatesEvent]
  )

  const subscribeToObjectLock = useCallback(
    (id, type, callback) => {
      logger.debug('Subscribing to lock for object:', id, 'type:', type)
      if (socketRef.current && socketRef.current.connected == true) {
        // Add callback to the subscribed lock callbacks map immediately
        if (!subscribedLockCallbacksRef.current.has(id)) {
          subscribedLockCallbacksRef.current.set(id, [])
        }
        subscribedLockCallbacksRef.current.get(id).push(callback)
        logger.debug(
          `Added lock callback for object ${id}, total lock callbacks: ${subscribedLockCallbacksRef.current.get(id).length}`
        )

        socketRef.current.emit('subscribe_lock', { id: id, type: type })
        logger.debug('Registered lock event listener for object:', id)

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

  const showError = (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)
  }

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

  // Generalized fetchObject function
  const fetchObject = async (id, type) => {
    const fetchUrl = `${config.backendUrl}/${type}s/${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
    logger.debug('Fetching table data from:', type, {
      page,
      limit,
      filter,
      sorter
    })

    try {
      const response = await axios.get(
        `${config.backendUrl}/${type.toLowerCase()}s`,
        {
          params: {
            page,
            limit,
            ...filter,
            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 []
    }
  }

  // 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}/${type.toLowerCase()}s/properties`,
        {
          params: {
            ...filter,
            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 []
    }
  }

  // Update filament information
  const updateObject = async (id, type, value) => {
    const updateUrl = `${config.backendUrl}/${type.toLowerCase()}s/${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 filament information
  const deleteObject = async (id, type) => {
    const deleteUrl = `${config.backendUrl}/${type.toLowerCase()}s/${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}/${type.toLowerCase()}s`
    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 {}
    }
  }

  // Download GCode file content
  const fetchObjectContent = async (id, type, fileName) => {
    try {
      const response = await axios.get(
        `${config.backendUrl}/${type.toLowerCase()}s/${id}/content`,
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )

      const fileURL = window.URL.createObjectURL(new Blob([response.data]))
      const fileLink = document.createElement('a')
      fileLink.href = fileURL
      fileLink.setAttribute('download', fileName)
      document.body.appendChild(fileLink)
      fileLink.click()
      fileLink.parentNode.removeChild(fileLink)
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchObjectContent(id, type, fileName)
      })
    }
  }

  // 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) => {
    logger.debug('Fetching spotlight data with query:', query)
    try {
      const response = await axios.get(
        `${config.backendUrl}/spotlight/${query}`,
        {
          headers: {
            Accept: 'application/json',
            Authorization: `Bearer ${token}`
          }
        }
      )
      return response.data
    } catch (err) {
      console.error(err)
      showError(err, () => {
        fetchSpotlightData(query)
      })
    }
  }

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

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

  return (
    <ApiServerContext.Provider
      value={{
        apiServer: socketRef.current,
        error,
        connecting,
        connected,
        lockObject,
        unlockObject,
        fetchObjectLock,
        updateObject,
        createObject,
        deleteObject,
        subscribeToObjectUpdates,
        subscribeToObjectEvent,
        subscribeToObjectTypeUpdates,
        subscribeToObjectLock,
        fetchObject,
        fetchObjects,
        fetchObjectsByProperty,
        fetchSpotlightData,
        fetchLoading,
        showError,
        fetchObjectContent,
        fetchTemplatePreview,
        fetchNotes,
        fetchHostOTP,
        sendObjectAction
      }}
    >
      {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 }
