import NDK, {
  getRelayListForUser,
  Hexpubkey,
  NDKEvent,
  NDKFilter,
  NDKRelayList,
  NDKRelaySet,
  NDKSubscriptionCacheUsage,
  NDKSubscriptionOptions,
  NDKUser,
  NDKUserProfile
} from '@nostr-dev-kit/ndk'

import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'

import { Dexie } from 'dexie'
import { createContext, ReactNode, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify'
import { UserRelaysType } from '../types'
import {
  DEFAULT_LOOK_UP_RELAY_LIST,
  hexToNpub,
  orderEventsChronologically,
  SIGIT_RELAY,
  timeout
} from '../utils'

export interface NDKContextType {
  ndk: NDK
  fetchEvents: (
    filter: NDKFilter,
    opts?: NDKSubscriptionOptions
  ) => Promise<NDKEvent[]>
  fetchEvent: (
    filter: NDKFilter,
    opts?: NDKSubscriptionOptions
  ) => Promise<NDKEvent | null>
  fetchEventsFromUserRelays: (
    filter: NDKFilter | NDKFilter[],
    hexKey: string,
    userRelaysType: UserRelaysType,
    opts?: NDKSubscriptionOptions
  ) => Promise<NDKEvent[]>
  fetchEventFromUserRelays: (
    filter: NDKFilter | NDKFilter[],
    hexKey: string,
    userRelaysType: UserRelaysType,
    opts?: NDKSubscriptionOptions
  ) => Promise<NDKEvent | null>
  findMetadata: (
    pubkey: string,
    opts?: NDKSubscriptionOptions
  ) => Promise<NDKUserProfile | null>
  getNDKRelayList: (pubkey: Hexpubkey) => Promise<NDKRelayList>
  publish: (event: NDKEvent, explicitRelayUrls?: string[]) => Promise<string[]>
}

// Create the context with an initial value of `null`
export const NDKContext = createContext<NDKContextType | null>(null)

// Create a provider component to wrap around parts of your app
export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
  useEffect(() => {
    window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
      event.preventDefault()
      if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
        console.log(
          'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
        )
        await Dexie.delete('degmod-db')
        // Must reload to open a brand new DB
        window.location.reload()
      }
    }
  }, [])

  const ndk = useMemo(() => {
    if (import.meta.env.MODE === 'development') {
      localStorage.setItem('debug', '*')
    }
    const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'sigit-db' })
    dexieAdapter.locking = true
    const ndk = new NDK({
      enableOutboxModel: true,
      autoConnectUserRelays: true,
      autoFetchUserMutelist: true,
      explicitRelayUrls: [...DEFAULT_LOOK_UP_RELAY_LIST],
      cacheAdapter: dexieAdapter
    })
    ndk.connect()

    return ndk
  }, [])

  /**
   * Asynchronously retrieves multiple event based on a provided filter.
   *
   * @param filter - The filter criteria to find the event.
   * @returns  Returns a promise that resolves to the found event or null if not found.
   */
  const fetchEvents = async (
    filter: NDKFilter,
    opts?: NDKSubscriptionOptions
  ): Promise<NDKEvent[]> => {
    return ndk
      .fetchEvents(filter, {
        closeOnEose: true,
        cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
        ...opts
      })
      .then((ndkEventSet) => {
        const ndkEvents = Array.from(ndkEventSet)
        return orderEventsChronologically(ndkEvents)
      })
      .catch((err) => {
        // Log the error and show a notification if fetching fails
        console.error('An error occurred in fetching events', err)
        toast.error('An error occurred in fetching events') // Show error notification
        return [] // Return an empty array in case of an error
      })
  }

  /**
   * Asynchronously retrieves an event based on a provided filter.
   *
   * @param filter - The filter criteria to find the event.
   * @returns  Returns a promise that resolves to the found event or null if not found.
   */
  const fetchEvent = async (
    filter: NDKFilter,
    opts?: NDKSubscriptionOptions
  ) => {
    const events = await fetchEvents(filter, opts)
    if (events.length === 0) return null
    return events[0]
  }

  /**
   * Asynchronously retrieves multiple events from the user's relays based on a specified filter.
   * The function first retrieves the user's relays, and then fetches the events using the provided filter.
   *
   * @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
   * @param hexKey - The hexadecimal representation of the user's public key.
   * @param userRelaysType - The type of relays to search (e.g., write, read).
   * @returns  A promise that resolves with an array of events.
   */
  const fetchEventsFromUserRelays = async (
    filter: NDKFilter | NDKFilter[],
    hexKey: string,
    userRelaysType: UserRelaysType,
    opts?: NDKSubscriptionOptions
  ): Promise<NDKEvent[]> => {
    // Find the user's relays (10s timeout).
    const relayUrls = await Promise.race([
      getRelayListForUser(hexKey, ndk),
      timeout(3000)
    ])
      .then((ndkRelayList) => {
        if (ndkRelayList) return ndkRelayList[userRelaysType]
        return [] // Return an empty array if ndkRelayList is undefined
      })
      .catch((err) => {
        console.error(
          `An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
          err
        )
        return [] as string[]
      })

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

    return ndk
      .fetchEvents(
        filter,
        {
          closeOnEose: true,
          cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
          ...opts
        },
        relayUrls.length
          ? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
          : undefined
      )
      .then((ndkEventSet) => {
        const ndkEvents = Array.from(ndkEventSet)
        return orderEventsChronologically(ndkEvents)
      })
      .catch((err) => {
        // Log the error and show a notification if fetching fails
        console.error('An error occurred in fetching events', err)
        toast.error('An error occurred in fetching events') // Show error notification
        return [] // Return an empty array in case of an error
      })
  }

  /**
   * Fetches an event from the user's relays based on a specified filter.
   * The function first retrieves the user's relays, and then fetches the event using the provided filter.
   *
   * @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
   * @param hexKey - The hexadecimal representation of the user's public key.
   * @param userRelaysType - The type of relays to search (e.g., write, read).
   * @returns  A promise that resolves to the fetched event or null if the operation fails.
   */
  const fetchEventFromUserRelays = async (
    filter: NDKFilter | NDKFilter[],
    hexKey: string,
    userRelaysType: UserRelaysType,
    opts?: NDKSubscriptionOptions
  ) => {
    const events = await fetchEventsFromUserRelays(
      filter,
      hexKey,
      userRelaysType,
      opts
    )
    if (events.length === 0) return null
    return events[0]
  }

  /**
   * Finds metadata for a given pubkey.
   *
   * @param hexKey - The pubkey to search for metadata.
   * @returns A promise that resolves to the metadata event.
   */
  const findMetadata = async (
    pubkey: string,
    opts?: NDKSubscriptionOptions
  ): Promise<NDKUserProfile | null> => {
    const npub = hexToNpub(pubkey)

    const user = new NDKUser({ npub })
    user.ndk = ndk

    return await user.fetchProfile({
      cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
      ...(opts || {})
    })
  }

  const getNDKRelayList = async (pubkey: Hexpubkey) => {
    const ndkRelayList = await Promise.race([
      getRelayListForUser(pubkey, ndk),
      timeout(10000)
    ]).catch(() => {
      const relayList = new NDKRelayList(ndk)
      relayList.bothRelayUrls = [SIGIT_RELAY]
      return relayList
    })

    return ndkRelayList
  }

  const publish = async (
    event: NDKEvent,
    explicitRelayUrls?: string[]
  ): Promise<string[]> => {
    if (!event.sig) throw new Error('Before publishing first sign the event!')

    let ndkRelaySet: NDKRelaySet | undefined

    if (explicitRelayUrls && explicitRelayUrls.length > 0) {
      if (!explicitRelayUrls.includes(SIGIT_RELAY)) {
        explicitRelayUrls = [...explicitRelayUrls, SIGIT_RELAY]
      }
      ndkRelaySet = NDKRelaySet.fromRelayUrls(explicitRelayUrls, ndk)
    }

    return await Promise.race([event.publish(ndkRelaySet), timeout(3000)])
      .then((res) => {
        const relaysPublishedOn = Array.from(res)
        return relaysPublishedOn.map((relay) => relay.url)
      })
      .catch((err) => {
        console.error(`An error occurred in publishing event`, err)
        return []
      })
  }

  return (
    <NDKContext.Provider
      value={{
        ndk,
        fetchEvents,
        fetchEvent,
        fetchEventsFromUserRelays,
        fetchEventFromUserRelays,
        findMetadata,
        getNDKRelayList,
        publish
      }}
    >
      {children}
    </NDKContext.Provider>
  )
}
