import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
import axios from 'axios'
import _ from 'lodash'
import {
  Event,
  EventTemplate,
  Filter,
  UnsignedEvent,
  finalizeEvent,
  generateSecretKey,
  getEventHash,
  getPublicKey,
  kinds,
  nip04,
  nip19,
  nip44,
  verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { NIP05_REGEX } from '../constants'
import {
  MetadataController,
  NostrController,
  relayController
} from '../controllers'
import {
  updateProcessedGiftWraps,
  updateUserAppData as updateUserAppDataAction
} from '../store/actions'
import { AuthState, Keys } from '../store/auth/types'
import { RelaysState } from '../store/relays/types'
import store from '../store/store'
import { Meta, SignedEvent, UserAppData } from '../types'
import { getDefaultRelayMap } from './relays'
import { parseJson, removeLeadingSlash } from './string'
import { timeout } from './utils'
import { getHash } from './hash'
import { SIGIT_BLOSSOM } from './const.ts'

/**
 * Generates a `d` tag for userAppData
 */
const getDTagForUserAppData = async (): Promise<string | null> => {
  const isLoggedIn = store.getState().auth?.loggedIn
  const pubkey = store.getState().auth?.usersPubkey

  if (!isLoggedIn || !pubkey) {
    throw new Error(
      'For generating d tag user must be logged in and a valid pubkey should exists in app Store'
    )
  }

  return getHash(`938_${pubkey}`)
}

/**
 * @param hexKey hex private or public key
 * @returns whether or not is key valid
 */
const validateHex = (hexKey: string) => {
  return hexKey.match(/^[a-f0-9]{64}$/)
}

/**
 * NPUB provided - it will convert NPUB to HEX
 * HEX provided - it will return HEX
 *
 * @param pubKey in NPUB, HEX format
 * @returns HEX format
 */
export const npubToHex = (pubKey: string): string | null => {
  // If key is NPUB
  if (pubKey.startsWith('npub1')) {
    try {
      return nip19.decode(pubKey).data as string
    } catch (error) {
      return null
    }
  }

  // valid hex key
  if (validateHex(pubKey)) return pubKey

  // Not a valid hex key
  return null
}

/**
 * If NSEC key is provided function will convert it to HEX
 * If HEX key Is provided function will validate the HEX format and return
 *
 * @param nsec or private key in HEX format
 */
export const nsecToHex = (nsec: string): string | null => {
  // If key is NSEC
  if (nsec.startsWith('nsec')) {
    try {
      return nip19.decode(nsec).data as string
    } catch (error) {
      return null
    }
  }

  // since it's not NSEC key we check if it's a valid hex key
  if (validateHex(nsec)) return nsec

  return null
}

export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
  if (hexPubkey.startsWith('npub1')) return hexPubkey as `npub1${string}`

  return nip19.npubEncode(hexPubkey)
}

export const verifySignedEvent = (event: SignedEvent) => {
  const isGood = verifyEvent(event)

  if (!isGood) {
    throw new Error(
      'Signed event did not pass verification. Check sig, id and pubkey.'
    )
  }
}

/**
 * Function to query NIP-05 data and return the public key and relays.
 *
 * @param {string} nip05 - The NIP-05 identifier in the format "name@domain".
 * @returns {Promise<{ pubkey: string, relays: string[] }>} - The public key and an array of relay URLs.
 * @throws Will throw an error if the NIP-05 identifier is invalid or if there is an issue with the network request.
 */
export const queryNip05 = async (
  nip05: string
): Promise<{
  pubkey: string
  relays: string[]
}> => {
  const match = nip05.match(NIP05_REGEX)

  // Throw an error if the NIP-05 identifier is invalid
  if (!match) throw new Error('Invalid nip05')

  // Destructure the match result, assigning default value '_' to name if not provided
  // First variable from the match destructuring is ignored
  const [, name = '_', domain] = match

  // Construct the URL to query the NIP-05 data
  const url = `https://${domain}/.well-known/nostr.json?name=${name}`

  // Perform the network request to get the NIP-05 data
  const res = await axios(url)
    .then((res) => {
      return res.data
    })
    .catch((err) => {
      console.log('err :>> ', err)
      throw err
    })

  // Extract the public key from the response data
  const pubkey = res.names[name]
  const relays: string[] = []

  // If a public key is found
  if (pubkey) {
    // Function to add relays if they exist and are not empty
    const addRelays = (relayList?: string[]) => {
      if (relayList && relayList.length > 0) {
        relays.push(...relayList)
      }
    }

    // Check for user-specific relays in the NIP-46 section of the response data
    if (res.nip46) {
      addRelays(res.nip46[pubkey] as string[])
    }

    // Check for user-specific relays in the relays section of the response data if not found in NIP-46
    if (relays.length === 0 && res.relays) {
      addRelays(res.relays[pubkey] as string[])
    }

    // If no user-specific relays are found, check for root user relays
    if (relays.length === 0) {
      const root = res.names['_']
      if (root) {
        // Check for root user relays in both NIP-46 and relays sections
        addRelays(res.nip46?.[root] as string[])
        addRelays(res.relays?.[root] as string[])
      }
    }
  }

  // Return the public key and the array of relays
  return {
    pubkey,
    relays
  }
}

export const base64EncodeSignedEvent = (event: SignedEvent) => {
  try {
    const authEventSerialized = JSON.stringify(event)
    const token = btoa(authEventSerialized)
    return token
  } catch (error) {
    throw new Error('An error occurred in JSON.stringify of signedAuthEvent')
  }
}

export const base64DecodeAuthToken = (authToken: string): SignedEvent => {
  const decodedToken = atob(authToken)

  try {
    const signedEvent = JSON.parse(decodedToken)
    return signedEvent
  } catch (error) {
    throw new Error('An error occurred in JSON.parse of the auth token')
  }
}

/**
 * @param pubkey in hex or npub format
 * @returns robohash.org url for the avatar
 */
export const getRoboHashPicture = (
  pubkey?: string,
  set: number = 1
): string => {
  if (!pubkey) return ''
  const npub = hexToNpub(pubkey)
  return `https://robohash.org/${npub}.png?set=set${set}`
}

export const unixNow = () => Math.round(Date.now() / 1000)
export const toUnixTimestamp = (date: number | Date) => {
  let time
  if (typeof date === 'number') {
    time = Math.round(date / 1000)
  } else if (date instanceof Date) {
    time = Math.round(date.getTime() / 1000)
  } else {
    throw Error('Unsupported type when converting to unix timestamp')
  }

  return time
}
export const fromUnixTimestamp = (unix: number) => {
  return unix * 1000
}

/**
 * Generate nip44 conversation key
 * @param privateKey
 * @param publicKey
 * @returns
 */
export const nip44ConversationKey = (
  privateKey: Uint8Array,
  publicKey: string
) => nip44.v2.utils.getConversationKey(privateKey, publicKey)

export const nip44Encrypt = (
  data: UnsignedEvent,
  privateKey: Uint8Array,
  publicKey: string
) =>
  nip44.v2.encrypt(
    JSON.stringify(data),
    nip44ConversationKey(privateKey, publicKey)
  )

export const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
  JSON.parse(
    nip44.v2.decrypt(
      data.content,
      nip44ConversationKey(privateKey, data.pubkey)
    )
  )

// Function to count leading zero bits
export const countLeadingZeroes = (hex: string) => {
  let count = 0

  for (let i = 0; i < hex.length; i++) {
    const nibble = parseInt(hex[i], 16)
    if (nibble === 0) {
      count += 4
    } else {
      count += Math.clz32(nibble) - 28
      break
    }
  }

  return count
}

/**
 * Function to create a wrapped event with PoW
 * @param event Original event to be wrapped
 * @param receiver Public key of the receiver
 * @param difficulty PoW difficulty level (default is 20)
 * @returns
 */
//
export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
  // Generate a random secret key and its corresponding public key
  const randomKey = generateSecretKey()
  const pubkey = getPublicKey(randomKey)

  // Encrypt the event content using nip44 encryption
  const content = nip44Encrypt(unsignedEvent, randomKey, receiver)

  // Initialize nonce and leadingZeroes for PoW calculation
  let nonce = 0
  let leadingZeroes = 0
  const difficulty = Math.floor(Math.random() * 10) + 5 // random number between 5 & 10

  // Loop until a valid PoW hash is found
  // eslint-disable-next-line no-constant-condition
  while (true) {
    // Create an unsigned event with the necessary fields
    const event: UnsignedEvent = {
      kind: 1059, // Event kind
      content, // Encrypted content
      pubkey, // Public key of the creator
      created_at: unixNow(), // Current timestamp
      tags: [
        // Tags including receiver and nonce
        ['p', receiver],
        ['nonce', nonce.toString(), difficulty.toString()]
      ]
    }

    // Calculate the SHA-256 hash of the unsigned event
    const hash = getEventHash(event)

    // Count the number of leading zero bits in the hash
    leadingZeroes = countLeadingZeroes(hash)

    // Check if the leading zero bits meet the required difficulty
    if (leadingZeroes >= difficulty) {
      // Finalize the event (sign it) and return the result
      return finalizeEvent(event, randomKey)
    }

    // Increment the nonce for the next iteration
    nonce++
  }
}

/**
 * Fetches user application data based on user's public key and stored metadata.
 *
 * @returns The user application data or null if an error occurs or no data is found.
 */
export const getUsersAppData = async (): Promise<UserAppData | null> => {
  // Initialize an array to hold relay URLs
  const relays: string[] = []

  // Retrieve the user's public key and relay map from the Redux store
  const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
  const relayMap = store.getState().relays?.map

  // Check if relayMap is undefined in the Redux store
  if (!relayMap) {
    // If relayMap is not present, fetch relay list metadata
    const metadataController = MetadataController.getInstance()
    const relaySet = await metadataController
      .findRelayListMetadata(usersPubkey)
      .catch((err) => {
        // Log error and return null if fetching metadata fails
        console.log(
          `An error occurred while finding relay list metadata for ${hexToNpub(usersPubkey)}`,
          err
        )
        return null
      })

    // Return null if metadata retrieval failed
    if (!relaySet) return null

    // Ensure that the relay list is not empty
    if (relaySet.write.length === 0) return null

    // Add write relays to the relays array
    relays.push(...relaySet.write)
  } else {
    // If relayMap exists, filter and add write relays from the stored map
    const writeRelays = Object.keys(relayMap).filter(
      (key) => relayMap[key].write
    )
    relays.push(...writeRelays)
  }

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

  // Define a filter for fetching events
  const filter: Filter = {
    kinds: [kinds.Application],
    '#d': [dTag]
  }

  const encryptedContent = await relayController
    .fetchEvent(filter, relays)
    .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 === '{}') {
    const secret = generateSecretKey()
    const pubKey = getPublicKey(secret)

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

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

  // 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<{
    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

  const { blossomUrls, keyPair } = parsedContent

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

  // Fetch additional user app data from the first blossom URL
  const dataFromBlossom = await getUserAppDataFromBlossom(
    blossomUrls[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 {
    blossomUrls,
    keyPair,
    sigits,
    processedGiftWraps
  }
}

export const updateUsersAppData = async (meta: Meta) => {
  const appData = store.getState().userAppData
  if (!appData || !appData.keyPair) return null

  const sigits = _.cloneDeep(appData.sigits)

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

  if (!createSignatureEvent) return null

  const id = createSignatureEvent.id
  let isUpdated = false

  // check if sigit already exists
  if (id in sigits) {
    // update meta only if incoming meta is more recent
    // than already existing one
    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 blossomUrls = [...appData.blossomUrls]

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

  if (!newBlossomUrl) return null

  // insert new blossom url at the start of the array
  blossomUrls.unshift(newBlossomUrl)

  // only keep last 10 blossom urls, delete older ones
  if (blossomUrls.length > 10) {
    const filesToDelete = blossomUrls.splice(10)
    filesToDelete.forEach((url) => {
      deleteBlossomFile(url, appData.keyPair!.private).catch((err) => {
        console.log(
          'An error occurred in removing old file of user app data from blossom server',
          err
        )
      })
    })
  }

  const usersPubkey = (store.getState().auth as AuthState).usersPubkey!

  // encrypt content for storing in kind 30078 event
  const nostrController = NostrController.getInstance()
  const encryptedContent = await nostrController
    .nip04Encrypt(
      usersPubkey,
      JSON.stringify({
        blossomUrls,
        keyPair: appData.keyPair
      })
    )
    .catch((err) => {
      console.log(
        'An error occurred in encryption of content for app data',
        err
      )
      toast.error(
        err.message || 'An error occurred in encryption of 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('An error occurred in signing event', err)
      toast.error(err.message || 'An error occurred in signing event')
      return null
    })

  if (!signedEvent) return null

  const relayMap =
    (store.getState().relays as RelaysState).map || getDefaultRelayMap()
  const writeRelays = Object.keys(relayMap).filter((key) => relayMap[key].write)

  const publishResult = await Promise.race([
    relayController.publish(signedEvent, writeRelays),
    timeout(40 * 1000)
  ]).catch((err) => {
    console.log('err :>> ', err)
    if (err.message === 'Timeout') {
      toast.error('Timeout occurred in publishing updated app data')
    } else if (Array.isArray(err)) {
      err.forEach((errResult) => {
        toast.error(
          `Publishing to ${errResult.relay} caused the following error: ${errResult.error}`
        )
      })
    } else {
      toast.error(
        'An unexpected error occurred in publishing updated app data '
      )
    }

    return null
  })

  if (!publishResult) return null

  // update redux store
  store.dispatch(
    updateUserAppDataAction({
      sigits,
      blossomUrls,
      processedGiftWraps: [...appData.processedGiftWraps],
      keyPair: {
        ...appData.keyPair
      }
    })
  )

  return signedEvent
}

const deleteBlossomFile = async (url: string, privateKey: string) => {
  const pathname = new URL(url).pathname
  const hash = removeLeadingSlash(pathname)

  // Define event metadata for authorization
  const event: EventTemplate = {
    kind: 24242,
    content: 'Authorize Upload',
    created_at: unixNow(),
    tags: [
      ['t', 'delete'],
      ['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
      ['x', hash]
    ]
  }

  const authEvent = finalizeEvent(event, hexToBytes(privateKey))

  // delete the file stored on file storage service using Axios
  const response = await axios.delete(url, {
    headers: {
      Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
    }
  })

  console.log('response.data :>> ', response.data)
}

/**
 * Function to upload user application data to the Blossom server.
 * @param sigits - An object containing metadata for the user application data.
 * @param processedGiftWraps - An array of processed gift wrap IDs.
 * @param privateKey - The private key used for encryption.
 * @returns A promise that resolves to the URL of the uploaded file.
 */
const uploadUserAppDataToBlossom = async (
  sigits: { [key: string]: Meta },
  processedGiftWraps: string[],
  privateKey: string
) => {
  // Create an object containing the sigits and processed gift wraps
  const obj = {
    sigits,
    processedGiftWraps
  }
  // Convert the object to a JSON string
  const stringified = JSON.stringify(obj)

  // Convert the private key from hex to bytes
  const secretKey = hexToBytes(privateKey)
  // Encrypt the JSON string using the secret key
  const encrypted = await nip04.encrypt(
    secretKey,
    getPublicKey(secretKey),
    stringified
  )

  // Create a blob from the encrypted data
  const blob = new Blob([encrypted], { type: 'application/octet-stream' })
  // Create a file from the blob
  const file = new File([blob], 'encrypted.txt', {
    type: 'application/octet-stream'
  })

  const hash = await getHash(await file.arrayBuffer())
  if (!hash) {
    throw new Error("Can't get file hash.")
  }

  // Define event metadata for authorization
  const event: EventTemplate = {
    kind: 24242,
    content: 'Authorize Upload',
    created_at: unixNow(),
    tags: [
      ['t', 'upload'],
      ['x', hash],
      ['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
      ['name', file.name],
      ['size', String(file.size)]
    ]
  }

  // Finalize the event with the private key
  const authEvent = finalizeEvent(event, hexToBytes(privateKey))

  // Upload the file to the file storage service using Axios
  const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
    headers: {
      Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
    }
  })

  // Return the URL of the uploaded file
  return response.data.url as string
}

/**
 * Function to retrieve and decrypt user application data from Blossom server.
 * @param url - The URL to fetch the encrypted data from.
 * @param privateKey - The private key used for decryption.
 * @returns A promise that resolves to the decrypted and parsed user application data.
 */
const getUserAppDataFromBlossom = async (url: string, privateKey: string) => {
  // Initialize errorCode to track HTTP error codes
  let errorCode = 0

  // Fetch the encrypted data from the provided URL
  const encrypted = await axios
    .get(url, {
      responseType: 'blob' // Expect a blob response
    })
    .then(async (res) => {
      // Convert the blob response to a File object
      const file = new File([res.data], 'encrypted.txt')
      // Read the text content from the file
      const text = await file.text()
      return text
    })
    .catch((err) => {
      // Log and display an error message if the request fails
      console.error(`error occurred in getting file from ${url}`, err)
      toast.error(err.message || `error occurred in getting file from ${url}`)

      // Set errorCode to the HTTP status code if available
      if (err.request) {
        const { status } = err.request
        errorCode = status
      }

      return null
    })

  // Return a default value if the requested resource is not found (404)
  if (errorCode === 404) {
    return {
      sigits: {},
      processedGiftWraps: []
    }
  }

  // Return null if the encrypted data could not be retrieved
  if (!encrypted) return null

  // Convert the private key from hex to bytes
  const secret = hexToBytes(privateKey)
  // Get the public key corresponding to the private key
  const pubkey = getPublicKey(secret)

  // Decrypt the encrypted data using the secret and public key
  const decrypted = await nip04.decrypt(secret, pubkey, encrypted)

  // Parse the decrypted JSON content
  const parsedContent = await parseJson<{
    sigits: { [key: string]: Meta }
    processedGiftWraps: string[]
  }>(decrypted).catch((err) => {
    // Log and display an error message if parsing fails
    console.log(
      'An error occurred in parsing the user app data content from blossom server',
      err
    )
    toast.error(
      'An error occurred in parsing the user app data content from blossom server'
    )
    return null
  })

  // Return the parsed content
  return parsedContent
}

/**
 * Function to subscribe to sigits notifications for a specified public key.
 * @param pubkey - The public key to subscribe to.
 * @returns A promise that resolves when the subscription is successful.
 */
export const subscribeForSigits = async (pubkey: string) => {
  // Instantiate the MetadataController to retrieve relay list metadata
  const metadataController = MetadataController.getInstance()
  const relaySet = await metadataController
    .findRelayListMetadata(pubkey)
    .catch((err) => {
      // Log an error if retrieving relay list metadata fails
      console.log(
        `An error occurred while finding relay list metadata for ${hexToNpub(pubkey)}`,
        err
      )
      return null
    })

  // Return if metadata retrieval failed
  if (!relaySet) return

  // Ensure relay list is not empty
  if (relaySet.read.length === 0) return

  // Define the filter for the subscription
  const filter: Filter = {
    kinds: [1059],
    '#p': [pubkey]
  }

  // Process the received event synchronously
  const events = await relayController.fetchEvents(filter, relaySet.read)
  for (const e of events) {
    await processReceivedEvent(e)
  }

  // Async processing of the events has a race condition
  // relayController.subscribeForEvents(filter, relaySet.read, (event) => {
  //   processReceivedEvent(event)
  // })
}

const processReceivedEvent = async (event: Event, difficulty: number = 5) => {
  const processedEvents = (store.getState().userAppData as UserAppData)
    .processedGiftWraps
  if (processedEvents.includes(event.id)) return

  store.dispatch(updateProcessedGiftWraps([...processedEvents, event.id]))

  // validate PoW
  // Count the number of leading zero bits in the hash
  const leadingZeroes = countLeadingZeroes(event.id)
  if (leadingZeroes < difficulty) return

  // decrypt the content of gift wrap event
  const nostrController = NostrController.getInstance()
  const decrypted = await nostrController.nip44Decrypt(
    event.pubkey,
    event.content
  )

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

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

  if (!meta) return

  await updateUsersAppData(meta)
}

/**
 * Function to send a notification to a specified receiver.
 * @param receiver - The recipient's public key.
 * @param meta - Metadata associated with the notification.
 */
export const sendNotification = async (receiver: string, meta: Meta) => {
  // Retrieve the user's public key from the state
  const usersPubkey = (store.getState().auth as AuthState).usersPubkey!

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

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

  // Instantiate the MetadataController to retrieve relay list metadata
  const metadataController = MetadataController.getInstance()
  const relaySet = await metadataController
    .findRelayListMetadata(receiver)
    .catch((err) => {
      // Log an error if retrieving relay list metadata fails
      console.log(
        `An error occurred while finding relay list metadata for ${hexToNpub(receiver)}`,
        err
      )
      return null
    })

  // Return if metadata retrieval failed
  if (!relaySet) return

  // Ensure relay list is not empty
  if (relaySet.read.length === 0) return

  // Publish the notification event to the recipient's read relays
  await Promise.race([
    relayController.publish(wrappedEvent, relaySet.read),
    timeout(40 * 1000)
  ]).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
  })
}
