import NDK, {
  getRelayListForUser,
  NDKEvent,
  NDKFilter,
  NDKKind,
  NDKRelaySet,
  NDKSubscriptionCacheUsage
} from '@nostr-dev-kit/ndk'
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts'
import { Dexie } from 'dexie'
import { createContext, ReactNode, useEffect, useMemo } from 'react'
import { toast } from 'react-toastify'
import { ModDetails } from 'types'
import {
  constructModListFromEvents,
  log,
  LogType,
  npubToHex,
  orderEventsChronologically
} from 'utils'

type FetchModsOptions = {
  source?: string
  until?: number
  since?: number
  limit?: number
}

interface NDKContextType {
  ndk: NDK
  fetchMods: (opts: FetchModsOptions) => Promise<ModDetails[]>
  fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise<NDKEvent[]>
}

// 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()
      console.log(event.reason)
      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(() => {
    localStorage.setItem('debug', '*')
    const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' })
    const ndk = new NDK({
      enableOutboxModel: true,
      autoConnectUserRelays: true,
      autoFetchUserMutelist: true,
      explicitRelayUrls: [
        'wss://user.kindpag.es',
        'wss://purplepag.es',
        'wss://relay.damus.io/',
        import.meta.env.VITE_APP_RELAY
      ],
      cacheAdapter: dexieAdapter
    })

    ndk.connect()

    return ndk
  }, [])

  /**
   * Fetches a list of mods based on the provided source.
   *
   * @param source - The source URL to filter the mods. If it matches the current window location,
   *                 it adds a filter condition to the request.
   * @param until - Optional timestamp to filter events until this time.
   * @param since - Optional timestamp to filter events from this time.
   * @returns A promise that resolves to an array of `ModDetails` objects. In case of an error,
   *          it logs the error and shows a notification, then returns an empty array.
   */
  const fetchMods = async ({
    source,
    until,
    since,
    limit
  }: FetchModsOptions): Promise<ModDetails[]> => {
    const relays = new Set<string>()
    relays.add(import.meta.env.VITE_APP_RELAY)

    const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')

    const promises = adminNpubs.map((npub) => {
      const hexKey = npubToHex(npub)
      if (!hexKey) return null

      return getRelayListForUser(hexKey, ndk)
        .then((ndkRelayList) => {
          if (ndkRelayList) {
            ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
          }
        })
        .catch((err) => {
          log(
            true,
            LogType.Error,
            `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
            err
          )
        })
    })

    await Promise.allSettled(promises)

    // Define the filter criteria for fetching mods
    const filter: NDKFilter = {
      kinds: [NDKKind.Classified], // Specify the kind of events to fetch
      limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
      '#t': [T_TAG_VALUE],
      until, // Optional filter to fetch events until this timestamp
      since // Optional filter to fetch events from this timestamp
    }

    // If the source matches the current window location, add a filter condition
    if (source === window.location.host) {
      filter['#r'] = [window.location.host] // Add a tag filter for the current host
    }

    return ndk
      .fetchEvents(
        filter,
        { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
        NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true)
      )
      .then((ndkEventSet) => {
        const ndkEvents = Array.from(ndkEventSet)
        orderEventsChronologically(ndkEvents)

        // Convert the fetched events into a list of mods
        const modList = constructModListFromEvents(ndkEvents)
        return modList // Return the list of mods
      })
      .catch((err) => {
        // Log the error and show a notification if fetching fails
        log(
          true,
          LogType.Error,
          'An error occurred in fetching mods from relays',
          err
        )
        toast.error('An error occurred in fetching mods from relays') // Show error notification
        return [] // Return an empty array in case of an error
      })
  }

  const fetchEvents = async (
    filter: NDKFilter,
    relayUrls: string[] = []
  ): Promise<NDKEvent[]> => {
    const relays = new Set<string>()

    // add all the relays passed to relay set
    relayUrls.forEach((relayUrl) => {
      relays.add(relayUrl)
    })

    relays.add(import.meta.env.VITE_APP_RELAY)

    const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')

    const promises = adminNpubs.map((npub) => {
      const hexKey = npubToHex(npub)
      if (!hexKey) return null

      return getRelayListForUser(hexKey, ndk)
        .then((ndkRelayList) => {
          if (ndkRelayList) {
            ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
          }
        })
        .catch((err) => {
          log(
            true,
            LogType.Error,
            `❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
            err
          )
        })
    })

    await Promise.allSettled(promises)

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

  return (
    <NDKContext.Provider value={{ ndk, fetchMods, fetchEvents }}>
      {children}
    </NDKContext.Provider>
  )
}
