import {
  Event,
  Filter,
  VerifiedEvent,
  kinds,
  validateEvent,
  verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { EventEmitter } from 'tseep'
import { NostrController, relayController } from '.'
import { localCache } from '../services'
import { ProfileMetadata, RelaySet } from '../types'
import {
  findRelayListAndUpdateCache,
  findRelayListInCache,
  getDefaultRelaySet,
  getUserRelaySet,
  isOlderThanOneDay,
  unixNow
} from '../utils'
import { DEFAULT_LOOK_UP_RELAY_LIST } from '../utils/const'

export class MetadataController extends EventEmitter {
  private static instance: MetadataController
  private nostrController: NostrController
  private specialMetadataRelay = 'wss://purplepag.es'
  private pendingFetches = new Map<string, Promise<Event | null>>() // Track pending fetches

  constructor() {
    super()
    this.nostrController = NostrController.getInstance()
  }

  public static getInstance(): MetadataController {
    if (!MetadataController.instance) {
      MetadataController.instance = new MetadataController()
    }
    return MetadataController.instance
  }

  /**
   * Asynchronously checks for more recent metadata events authored by a specific key.
   * If a more recent metadata event is found, it is handled and returned.
   * If no more recent event is found, the current event is returned.
   * @param hexKey The hexadecimal key of the author to filter metadata events.
   * @param currentEvent The current metadata event, if any, to compare with newer events.
   * @returns A promise resolving to the most recent metadata event found, or null if none is found.
   */
  private async checkForMoreRecentMetadata(
    hexKey: string,
    currentEvent: Event | null
  ): Promise<Event | null> {
    // Return the ongoing fetch promise if one exists for the same hexKey
    if (this.pendingFetches.has(hexKey)) {
      return this.pendingFetches.get(hexKey)!
    }

    // Define the event filter to only include metadata events authored by the given key
    const eventFilter: Filter = {
      kinds: [kinds.Metadata],
      authors: [hexKey]
    }

    const fetchPromise = relayController
      .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
      .catch((err) => {
        console.error(err)
        return null
      })
      .finally(() => {
        this.pendingFetches.delete(hexKey)
      })

    this.pendingFetches.set(hexKey, fetchPromise)

    const metadataEvent = await fetchPromise

    if (
      metadataEvent &&
      validateEvent(metadataEvent) &&
      verifyEvent(metadataEvent)
    ) {
      if (
        !currentEvent ||
        metadataEvent.created_at >= currentEvent.created_at
      ) {
        this.handleNewMetadataEvent(metadataEvent)
      }
      return metadataEvent
    }

    // todo/implement: if no valid metadata event is found in DEFAULT_LOOK_UP_RELAY_LIST
    // try to query user relay list

    // if current event is null we should cache empty metadata event for provided hexKey
    if (!currentEvent) {
      const emptyMetadata = this.getEmptyMetadataEvent(hexKey)
      this.handleNewMetadataEvent(emptyMetadata as VerifiedEvent)
    }

    return currentEvent
  }

  /**
   * Handle new metadata events and emit them to subscribers
   */
  private async handleNewMetadataEvent(event: VerifiedEvent) {
    // update the event in local cache
    localCache.addUserMetadata(event)
    // Emit the event to subscribers.
    this.emit(event.pubkey, event.kind, event)
  }

  /**
   * Finds metadata for a given hexadecimal key.
   *
   * @param hexKey - The hexadecimal key to search for metadata.
   * @returns A promise that resolves to the metadata event.
   */
  public findMetadata = async (hexKey: string): Promise<Event | null> => {
    // Attempt to retrieve the metadata event from the local cache
    const cachedMetadataEvent = await localCache.getUserMetadata(hexKey)

    // If cached metadata is found, check its validity
    if (cachedMetadataEvent) {
      // Check if the cached metadata is older than one day
      if (isOlderThanOneDay(cachedMetadataEvent.cachedAt)) {
        // If older than one week, find the metadata from relays in background
        this.checkForMoreRecentMetadata(hexKey, cachedMetadataEvent.event)
      }

      // Return the cached metadata event
      return cachedMetadataEvent.event
    }

    // If no cached metadata is found, retrieve it from relays
    return this.checkForMoreRecentMetadata(hexKey, null)
  }

  /**
   * Based on the hexKey of the current user, this method attempts to retrieve a relay set.
   * @func findRelayListInCache first checks if there is already an up-to-date
   * relay list available in cache; if not -
   * @func findRelayListAndUpdateCache checks if the relevant relay event is available from
   * the purple pages relay;
   * @func findRelayListAndUpdateCache will run again if the previous two calls return null and
   * check if the relevant relay event can be obtained from 'most popular relays'
   * If relay event is found, it will be saved in cache for future use
   * @param hexKey of the current user
   * @return RelaySet which will contain either relays extracted from the user Relay Event
   * or a fallback RelaySet with Sigit's Relay
   */
  public findRelayListMetadata = async (hexKey: string): Promise<RelaySet> => {
    const relayEvent =
      (await findRelayListInCache(hexKey)) ||
      (await findRelayListAndUpdateCache(DEFAULT_LOOK_UP_RELAY_LIST, hexKey))

    return relayEvent ? getUserRelaySet(relayEvent.tags) : getDefaultRelaySet()
  }

  public extractProfileMetadataContent = (event: Event) => {
    try {
      if (!event.content) return {}
      return JSON.parse(event.content) as ProfileMetadata
    } catch (error) {
      console.log('error in parsing metadata event content :>> ', error)
      return null
    }
  }

  /**
   * Function will not sign provided event if the SIG exists
   */
  public publishMetadataEvent = async (event: Event) => {
    let signedMetadataEvent = event

    if (event.sig.length < 1) {
      const timestamp = unixNow()

      // Metadata event to publish to the wss://purplepag.es relay
      const newMetadataEvent: Event = {
        ...event,
        created_at: timestamp
      }

      signedMetadataEvent =
        await this.nostrController.signEvent(newMetadataEvent)
    }

    await relayController
      .publish(signedMetadataEvent, [this.specialMetadataRelay])
      .then((relays) => {
        if (relays.length) {
          toast.success(`Metadata event published on: ${relays.join('\n')}`)
          this.handleNewMetadataEvent(signedMetadataEvent as VerifiedEvent)
        } else {
          toast.error('Could not publish metadata event to any relay!')
        }
      })
      .catch((err) => {
        toast.error(err.message)
      })
  }

  public validate = (event: Event) => validateEvent(event) && verifyEvent(event)

  public getEmptyMetadataEvent = (pubkey?: string): Event => {
    return {
      content: '',
      created_at: new Date().valueOf(),
      id: '',
      kind: 0,
      pubkey: pubkey || '',
      sig: '',
      tags: []
    }
  }
}
