import { Event, Filter, kinds, UnsignedEvent } from 'nostr-tools'
import { RelayList } from 'nostr-tools/kinds'
import { getRelayInfo, unixNow } from '.'
import { NostrController, relayController } from '../controllers'
import { localCache } from '../services'
import { RelayMap, RelaySet } from '../types'
import {
  DEFAULT_LOOK_UP_RELAY_LIST,
  ONE_DAY_IN_MS,
  ONE_WEEK_IN_MS,
  SIGIT_RELAY
} from './const'

const READ_MARKER = 'read'
const WRITE_MARKER = 'write'

/**
 * Attempts to find a relay list from the provided lookUpRelays.
 * If the relay list is found, it will be added to the user relay list metadata.
 * @param lookUpRelays
 * @param hexKey
 * @return found relay list or null
 */
const findRelayListAndUpdateCache = async (
  lookUpRelays: string[],
  hexKey: string
): Promise<Event | null> => {
  try {
    const eventFilter: Filter = {
      kinds: [RelayList],
      authors: [hexKey]
    }

    const event = await relayController.fetchEvent(eventFilter, lookUpRelays)
    if (event) {
      await localCache.addUserRelayListMetadata(event)
    }
    return event
  } catch (error) {
    console.error(error)
    return null
  }
}

/**
 * Attempts to find a relay list in cache. If it is present, it will check that the cached event is not
 * older than one week.
 * @param hexKey
 * @return RelayList event if it's not older than a week; otherwise null
 */
const findRelayListInCache = async (hexKey: string): Promise<Event | null> => {
  try {
    // Attempt to retrieve the metadata event from the local cache
    const cachedRelayListMetadataEvent =
      await localCache.getUserRelayListMetadata(hexKey)

    // Check if the cached event is not older than one week
    if (
      cachedRelayListMetadataEvent &&
      !isOlderThanOneWeek(cachedRelayListMetadataEvent.cachedAt)
    ) {
      return cachedRelayListMetadataEvent.event
    }

    return null
  } catch (error) {
    console.error(error)
    return null
  }
}

/**
 * Transforms a list of relay tags from a Nostr Event to a RelaySet.
 * @param tags
 */
const getUserRelaySet = (tags: string[][]): RelaySet => {
  return tags
    .filter(isRelayTag)
    .reduce<RelaySet>(toRelaySet, getDefaultRelaySet())
}

const getDefaultRelaySet = (): RelaySet => ({
  read: [SIGIT_RELAY],
  write: [SIGIT_RELAY]
})

const getDefaultRelayMap = (): RelayMap => ({
  [SIGIT_RELAY]: { write: true, read: true }
})

const isOlderThanOneWeek = (cachedAt: number) => {
  return Date.now() - cachedAt > ONE_WEEK_IN_MS
}

const isOlderThanOneDay = (cachedAt: number) => {
  return Date.now() - cachedAt > ONE_DAY_IN_MS
}

const isRelayTag = (tag: string[]): boolean => tag[0] === 'r'

const toRelaySet = (obj: RelaySet, tag: string[]): RelaySet => {
  if (tag.length >= 3) {
    const marker = tag[2]

    if (marker === READ_MARKER) {
      obj.read.push(tag[1])
    } else if (marker === WRITE_MARKER) {
      obj.write.push(tag[1])
    }
  }
  if (tag.length === 2) {
    obj.read.push(tag[1])
    obj.write.push(tag[1])
  }

  return obj
}

/**
 * Provides relay map.
 * @param npub - user's npub
 * @returns - promise that resolves into relay map and a timestamp when it has been updated.
 */
const getRelayMap = async (
  npub: string
): Promise<{ map: RelayMap; mapUpdated?: number }> => {
  // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
  const eventFilter: Filter = {
    kinds: [kinds.RelayList],
    authors: [npub]
  }

  const event = await relayController
    .fetchEvent(eventFilter, DEFAULT_LOOK_UP_RELAY_LIST)
    .catch((err) => {
      return Promise.reject(err)
    })

  if (event) {
    // Handle founded 10002 event
    const relaysMap: RelayMap = {}

    // 'r' stands for 'relay'
    const relayTags = event.tags.filter((tag) => tag[0] === 'r')

    relayTags.forEach((tag) => {
      const uri = tag[1]
      const relayType = tag[2]

      // if 3rd element of relay tag is undefined, relay is WRITE and READ
      relaysMap[uri] = {
        write: relayType ? relayType === 'write' : true,
        read: relayType ? relayType === 'read' : true
      }
    })

    Object.keys(relaysMap).forEach((relayUrl) =>
      relayController.connectRelay(relayUrl)
    )

    getRelayInfo(Object.keys(relaysMap))

    return Promise.resolve({ map: relaysMap, mapUpdated: event.created_at })
  } else {
    return Promise.resolve({ map: getDefaultRelayMap() })
  }
}

/**
 * Publishes relay map.
 * @param relayMap - relay map.
 * @param npub - user's npub.
 * @param extraRelaysToPublish - optional relays to publish relay map.
 * @returns - promise that resolves into a string representing publishing result.
 */
const publishRelayMap = async (
  relayMap: RelayMap,
  npub: string,
  extraRelaysToPublish?: string[]
): Promise<string> => {
  const timestamp = unixNow()
  const relayURIs = Object.keys(relayMap)

  // More info about this kind of event available https://github.com/nostr-protocol/nips/blob/master/65.md
  const tags: string[][] = relayURIs.map((relayURI) =>
    [
      'r',
      relayURI,
      relayMap[relayURI].read && relayMap[relayURI].write
        ? ''
        : relayMap[relayURI].write
          ? 'write'
          : 'read'
    ].filter((value) => value !== '')
  )

  const newRelayMapEvent: UnsignedEvent = {
    kind: kinds.RelayList,
    tags,
    content: '',
    pubkey: npub,
    created_at: timestamp
  }

  const nostrController = NostrController.getInstance()
  const signedEvent = await nostrController.signEvent(newRelayMapEvent)

  let relaysToPublish = relayURIs

  // Add extra relays if provided
  if (extraRelaysToPublish) {
    relaysToPublish = [...relaysToPublish, ...extraRelaysToPublish]
  }

  // If relay map is empty, use most popular relay URIs
  if (!relaysToPublish.length) {
    relaysToPublish = DEFAULT_LOOK_UP_RELAY_LIST
  }
  const publishResult = await relayController.publish(
    signedEvent,
    relaysToPublish
  )

  if (publishResult && publishResult.length) {
    return Promise.resolve(
      `Relay Map published on: ${publishResult.join('\n')}`
    )
  }

  return Promise.reject('Publishing updated relay map was unsuccessful.')
}

export {
  findRelayListAndUpdateCache,
  findRelayListInCache,
  getDefaultRelayMap,
  getDefaultRelaySet,
  getRelayMap,
  getUserRelaySet,
  isOlderThanOneDay,
  isOlderThanOneWeek,
  publishRelayMap
}
