import { useCallback } from 'react'
import { toast } from 'react-toastify'

import { bytesToHex } from '@noble/hashes/utils'
import {
  NDKEvent,
  NDKFilter,
  NDKKind,
  NDKRelaySet,
  NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk'
import _ from 'lodash'
import {
  Event,
  generateSecretKey,
  getPublicKey,
  kinds,
  UnsignedEvent
} from 'nostr-tools'

import { useAppDispatch, useAppSelector, useNDKContext } from '.'
import { NostrController } from '../controllers'
import {
  updateProcessedGiftWraps,
  updateUserAppData as updateUserAppDataAction
} from '../store/actions'
import { Keys } from '../store/auth/types'
import {
  BlossomVersion,
  isSigitNotification,
  Meta,
  SigitNotification,
  UserAppData,
  UserRelaysType
} from '../types'
import {
  countLeadingZeroes,
  createWrap,
  deleteBlossomFile,
  fetchMetaFromFileStorage,
  getDTagForUserAppData,
  getUserAppDataFromBlossom,
  hexToNpub,
  parseJson,
  SIGIT_RELAY,
  unixNow,
  uploadUserAppDataToBlossom
} from '../utils'

export const useNDK = () => {
  const dispatch = useAppDispatch()
  const {
    ndk,
    fetchEvent,
    fetchEventsFromUserRelays,
    publish,
    getNDKRelayList
  } = useNDKContext()
  const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)
  const appData = useAppSelector((state) => state.userAppData)
  const processedEvents = useAppSelector(
    (state) => state.userAppData?.processedGiftWraps
  )

  /**
   * Fetches user application data based on user's public key.
   *
   * @returns The user application data or null if an error occurs or no data is found.
   */
  const getUsersAppData = useCallback(async (): Promise<UserAppData | null> => {
    if (!usersPubkey) return null

    // Get an instance of the NostrController
    const nostrController = NostrController.getInstance()

    // Decryption can fail down in the code if extension options changed
    // Forcefully log out the user if we detect missmatch between pubkeys
    if (usersPubkey !== (await nostrController.capturePublicKey())) {
      return null
    }

    // Generate an identifier for the user's nip78
    const dTag = await getDTagForUserAppData()
    if (!dTag) return null

    // Define a filter for fetching events
    const filter: NDKFilter = {
      kinds: [NDKKind.AppSpecificData],
      authors: [usersPubkey],
      '#d': [dTag]
    }

    const encryptedContent = await fetchEvent(filter, {
      cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
    })
      .then((event) => {
        if (event) return event.content

        // If no event is found, return an empty stringified object
        return '{}'
      })
      .catch((err) => {
        // Log error and show a toast notification if fetching event fails
        console.log(`An error occurred in finding kind 30078 event`, err)
        toast.error(
          'An error occurred in finding kind 30078 event for data storage'
        )
        return null
      })

    // Return null if encrypted content retrieval fails
    if (!encryptedContent) return null

    // Handle case where the encrypted content is an empty object
    if (encryptedContent === '{}') {
      // Generate ephemeral key pair
      const secret = generateSecretKey()
      const pubKey = getPublicKey(secret)

      return {
        sigits: {},
        processedGiftWraps: [],
        blossomVersions: [],
        keyPair: {
          private: bytesToHex(secret),
          public: pubKey
        }
      }
    }

    // Decrypt the encrypted content
    const decrypted = await nostrController
      .nip04Decrypt(usersPubkey, encryptedContent)
      .catch((err) => {
        // Log error and show a toast notification if decryption fails
        console.log('An error occurred while decrypting app data', err)
        toast.error('An error occurred while decrypting app data')
        return null
      })

    // Return null if decryption fails
    if (!decrypted) return null

    // Parse the decrypted content
    const parsedContent = await parseJson<{
      blossomVersions: BlossomVersion[]
      blossomUrls: string[]
      keyPair: Keys
    }>(decrypted).catch((err) => {
      // Log error and show a toast notification if parsing fails
      console.log(
        'An error occurred in parsing the content of kind 30078 event',
        err
      )
      toast.error(
        'An error occurred in parsing the content of kind 30078 event'
      )
      return null
    })

    // Return null if parsing fails
    if (!parsedContent) return null

    // If old property blossomUrls is found, convert it to new appraoch blossomVersions
    if (parsedContent.blossomUrls) {
      parsedContent.blossomVersions = parsedContent.blossomUrls.map((url) => {
        return {
          urls: [url]
        }
      })
    }

    const { blossomVersions, keyPair } = parsedContent

    // Return null if no blossom URLs are found
    if (blossomVersions.length === 0) return null

    // Fetch additional user app data from the last blossom version urls
    const dataFromBlossom = await getUserAppDataFromBlossom(
      blossomVersions[0],
      keyPair.private
    )

    // Return null if fetching data from blossom fails
    if (!dataFromBlossom) return null

    const { sigits, processedGiftWraps } = dataFromBlossom

    // Return the final user application data
    return {
      blossomVersions,
      keyPair,
      sigits,
      processedGiftWraps
    }
  }, [usersPubkey, fetchEvent])

  const updateUsersAppData = useCallback(
    async (metaArray: Meta[]) => {
      if (!appData || !appData.keyPair || !usersPubkey) return null

      const sigits = _.cloneDeep(appData.sigits)
      let isUpdated = false

      for (const meta of metaArray) {
        const createSignatureEvent = await parseJson<Event>(
          meta.createSignature
        ).catch((err) => {
          console.log('Error in parsing the createSignature event:', err)
          toast.error(
            err.message ||
              'Error occurred in parsing the create signature event'
          )
          return null
        })

        if (!createSignatureEvent) continue

        const id = createSignatureEvent.id

        // Check if sigit already exists
        if (id in sigits) {
          // Update meta only if incoming meta is more recent
          const existingMeta = sigits[id]
          if (existingMeta.modifiedAt < meta.modifiedAt) {
            sigits[id] = meta
            isUpdated = true
          }
        } else {
          sigits[id] = meta
          isUpdated = true
        }
      }

      if (!isUpdated) return null

      const blossomVersions = [...appData.blossomVersions]

      const newBlossomUrls = await uploadUserAppDataToBlossom(
        sigits,
        appData.processedGiftWraps,
        appData.keyPair.private
      ).catch((err) => {
        console.log(
          'Error uploading user app data file to Blossom server:',
          err
        )
        toast.error(
          'Error occurred in uploading user app data file to Blossom server'
        )
        return null
      })

      if (!newBlossomUrls) return null

      // insert new server (blossom) urls at the start of the array
      blossomVersions.unshift({
        urls: newBlossomUrls
      })

      // only keep last 10 blossom versions (urls), delete older ones
      // Every version can be uploaded to multiple servers
      if (blossomVersions.length > 10) {
        const versionsToDelete = blossomVersions.splice(10)
        versionsToDelete.forEach((version) => {
          for (const url of version.urls) {
            deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
              console.log(
                `An error occurred while removing an old file of user app data from the file server: ${url}`,
                err
              )
            })
          }
        })
      }

      // Encrypt content for storing in kind 30078 event
      const nostrController = NostrController.getInstance()
      const encryptedContent = await nostrController
        .nip04Encrypt(
          usersPubkey,
          JSON.stringify({
            blossomVersions: blossomVersions,
            keyPair: appData.keyPair
          })
        )
        .catch((err) => {
          console.log('Error encrypting content for app data:', err)
          toast.error(err.message || 'Error encrypting content for app data')
          return null
        })

      if (!encryptedContent) return null

      // Generate the identifier for user's appData event
      const dTag = await getDTagForUserAppData()
      if (!dTag) return null

      const updatedEvent: UnsignedEvent = {
        kind: kinds.Application,
        pubkey: usersPubkey,
        created_at: unixNow(),
        tags: [['d', dTag]],
        content: encryptedContent
      }

      const signedEvent = await nostrController
        .signEvent(updatedEvent)
        .catch((err) => {
          console.log('Error signing event:', err)
          toast.error(err.message || 'Error signing event')
          return null
        })

      if (!signedEvent) return null

      const ndkEvent = new NDKEvent(ndk, signedEvent)
      const publishResult = await publish(ndkEvent)

      if (publishResult.length === 0 || !publishResult) {
        toast.error('Unexpected error occurred in publishing updated app data')
        return null
      }

      console.count('updateUserAppData useNDK')

      // Update Redux store
      dispatch(
        updateUserAppDataAction({
          sigits,
          blossomVersions: blossomVersions,
          processedGiftWraps: [...appData.processedGiftWraps],
          keyPair: {
            ...appData.keyPair
          }
        })
      )

      return signedEvent
    },
    [appData, dispatch, ndk, publish, usersPubkey]
  )

  const processReceivedEvents = useCallback(
    async (events: NDKEvent[], difficulty: number = 5) => {
      if (!processedEvents) return

      const validMetaArray: Meta[] = [] // Array to store valid Meta objects
      const updatedProcessedEvents = [...processedEvents] // Keep track of processed event IDs

      for (const event of events) {
        // Skip already processed events
        if (processedEvents.includes(event.id)) continue

        // Validate PoW
        const leadingZeroes = countLeadingZeroes(event.id)
        if (leadingZeroes < difficulty) continue

        // Decrypt the content of the gift wrap event
        const nostrController = NostrController.getInstance()
        const decrypted = await nostrController
          .nip44Decrypt(event.pubkey, event.content)
          .catch((err) => {
            console.log('An error occurred in decrypting event content', err)
            return null
          })

        if (!decrypted) continue

        const internalUnsignedEvent = await parseJson<UnsignedEvent>(
          decrypted
        ).catch((err) => {
          console.log(
            'An error occurred in parsing the internal unsigned event',
            err
          )
          return null
        })

        if (!internalUnsignedEvent || internalUnsignedEvent.kind !== 938)
          continue

        const parsedContent = await parseJson<Meta | SigitNotification>(
          internalUnsignedEvent.content
        ).catch((err) => {
          console.log('An error occurred in parsing event content', err)
          return null
        })

        if (!parsedContent) continue

        let meta: Meta

        if (isSigitNotification(parsedContent)) {
          const notification = parsedContent
          if (!notification.keys || !usersPubkey) continue

          let encryptionKey: string | undefined
          const { sender, keys } = notification.keys
          const usersNpub = hexToNpub(usersPubkey)

          if (usersNpub in keys) {
            encryptionKey = await nostrController
              .nip04Decrypt(sender, keys[usersNpub])
              .catch((err) => {
                console.log(
                  'An error occurred in decrypting encryption key',
                  err
                )
                return undefined
              })
          }

          try {
            meta = await fetchMetaFromFileStorage(
              notification.metaUrls,
              encryptionKey
            )
          } catch (error) {
            console.error(
              'An error occurred fetching meta file from storage',
              error
            )
            continue
          }
        } else {
          meta = parsedContent
        }

        validMetaArray.push(meta) // Add valid Meta to the array
        updatedProcessedEvents.push(event.id) // Mark event as processed
      }

      // Update processed events in the Redux store
      dispatch(updateProcessedGiftWraps(updatedProcessedEvents))

      // Pass the array of Meta objects to updateUsersAppData
      if (validMetaArray.length > 0) {
        await updateUsersAppData(validMetaArray)
      }
    },
    [dispatch, processedEvents, updateUsersAppData, usersPubkey]
  )

  const subscribeForSigits = useCallback(
    async (pubkey: string) => {
      // Define the filter for the subscription
      const filter: NDKFilter = {
        kinds: [1059 as NDKKind],
        '#p': [pubkey]
      }

      // Process the received event synchronously
      const events = await fetchEventsFromUserRelays(
        filter,
        pubkey,
        UserRelaysType.Read,
        {
          cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
        }
      )

      await processReceivedEvents(events)
    },
    [fetchEventsFromUserRelays, processReceivedEvents]
  )

  /**
   * Function to send a notification to a specified receiver.
   * @param receiver - The recipient's public key.
   * @param notification - Url pointing to metadata associated with the notification on blossom and keys to decrypt.
   */
  const sendNotification = useCallback(
    async (receiver: string, notification: SigitNotification) => {
      if (!usersPubkey) return

      // Create an unsigned event object with the provided metadata
      const unsignedEvent: UnsignedEvent = {
        kind: 938,
        pubkey: usersPubkey,
        content: JSON.stringify(notification),
        tags: [],
        created_at: unixNow()
      }

      // Wrap the unsigned event with the receiver's information
      const wrappedEvent = createWrap(unsignedEvent, receiver)

      // Publish the notification event to the recipient's read relays
      const ndkEvent = new NDKEvent(ndk, wrappedEvent)

      const ndkRelayList = await getNDKRelayList(receiver)

      const readRelayUrls: string[] = []

      if (ndkRelayList?.readRelayUrls) {
        readRelayUrls.push(...ndkRelayList.readRelayUrls)
      }

      if (!readRelayUrls.includes(SIGIT_RELAY)) {
        readRelayUrls.push(SIGIT_RELAY)
      }

      await ndkEvent
        .publish(NDKRelaySet.fromRelayUrls(readRelayUrls, ndk, true))
        .then((publishedOnRelays) => {
          if (publishedOnRelays.size === 0) {
            throw new Error('Could not publish to any relay')
          }

          return publishedOnRelays
        })
        .catch((err) => {
          // Log an error if publishing the notification event fails
          console.log(
            `An error occurred while publishing notification event for ${hexToNpub(receiver)}`,
            err
          )
          throw err
        })
    },
    [ndk, usersPubkey, getNDKRelayList]
  )

  return {
    getUsersAppData,
    subscribeForSigits,
    updateUsersAppData,
    sendNotification
  }
}
