import {
  Event,
  EventTemplate,
  SimplePool,
  UnsignedEvent,
  finalizeEvent,
  nip04,
  nip19,
  verifyEvent,
  generateSecretKey,
  getPublicKey
} from 'nostr-tools'
import { EventEmitter } from 'tseep'
import { SignedEvent, Keys } from '../types'

export class NostrController extends EventEmitter {
  private static instance: NostrController

  private generatedKeys: Keys | undefined

  private constructor() {
    super()

    this.generatedKeys = this.generateKeys()
  }

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

  /**
   * Function will publish provided event to the provided relays
   */
  publishEvent = async (event: Event, relays: string[]): Promise<string[]> => {
    const simplePool = new SimplePool()
    const promises = simplePool.publish(relays, event)

    const results = await Promise.allSettled(promises)

    const publishedRelays: string[] = []

    results.forEach((result, index) => {
      if (result.status === 'fulfilled') publishedRelays.push(relays[index])
    })

    if (publishedRelays.length === 0) {
      const failedPublishes: Array<{ relay: string; error: string }> = []
      const fallbackRejectionReason =
        'Attempt to publish an event has been rejected with unknown reason.'

      results.forEach((res, index) => {
        if (res.status === 'rejected') {
          failedPublishes.push({
            relay: relays[index],
            error: res.reason
              ? res.reason.message || fallbackRejectionReason
              : fallbackRejectionReason
          })
        }
      })

      throw failedPublishes
    }

    return publishedRelays
  }

  /**
   * Signs an event with private key (if it is present in local storage) or
   * with browser extension (if it is present) or
   * with nSecBunker instance.
   * @param event - unsigned nostr event.
   * @returns - a promised that is resolved with signed nostr event.
   */
  signEvent = async (
    event: UnsignedEvent | EventTemplate
  ): Promise<SignedEvent> => {
    if (!this.generatedKeys) {
      throw new Error(`Private & public key pair is not found.`)
    }

    const { private: nsec } = this.generatedKeys
    const privateKey = nip19.decode(nsec).data as Uint8Array

    const signedEvent = finalizeEvent(event, privateKey)

    this.verifySignedEvent(signedEvent)

    return Promise.resolve(signedEvent)
  }

  nip04Encrypt = async (receiver: string, content: string): Promise<string> => {
    if (!this.generatedKeys) {
      throw new Error(`Private & public key pair is not found.`)
    }

    const { private: nsec } = this.generatedKeys
    const privateKey = nip19.decode(nsec).data as Uint8Array

    const encrypted = await nip04.encrypt(privateKey, receiver, content)

    return encrypted
  }

  /**
   * Sends a Direct Message (DM) to a recipient, encrypting the content and handling authentication.
   * @param fileUrl The URL of the encrypted zip file to be included in the DM.
   * @param encryptionKey The encryption key used to decrypt the zip file to be included in the DM.
   * @param pubkey The public key of the recipient.
   * @param isSigner Boolean indicating whether the recipient is a signer or viewer.
   * @param setAuthUrl Function to set the authentication URL in the component state.
   */
  sendDM = async (pubkey: string, message: string): Promise<string[]> => {
    // Set up timeout promise to handle encryption timeout
    const timeoutPromise = new Promise<never>((_, reject) => {
      setTimeout(() => {
        reject(new Error('Timeout occurred'))
      }, 60000) // Timeout duration = 60 seconds
    })

    // Encrypt the DM content, with timeout
    const encrypted = await Promise.race([
      this.nip04Encrypt(this.npubToHex(pubkey) as string, message),
      timeoutPromise
    ])

    // Return if encryption failed
    if (!encrypted) throw new Error('Message was not encrypted.')

    // Construct event metadata for the DM
    const event: EventTemplate = {
      kind: 4, // DM event type
      content: encrypted, // Encrypted DM content
      created_at: Math.floor(Date.now() / 1000), // Current timestamp
      tags: [['p', this.npubToHex(pubkey) as string]] // Tag with recipient's public key
    }

    // Sign the DM event
    const signedEvent = await this.signEvent(event)

    // Return if event signing failed
    if (!signedEvent) throw new Error('Message was not signed.')

    // These relay will be used to send a DM. Recipient has to read from these relays to receive a DM.
    const relays = [
      'wss://relay.damus.io/',
      'wss://nos.lol/',
      'wss://relay.snort.social'
    ]

    // Publish the signed DM event to the recipient's read relays
    return await this.publishEvent(signedEvent, relays)
  }

  /**
   * @param hexKey hex private or public key
   * @returns whether or not is key valid
   */
  validateHex = (hexKey: string): RegExpMatchArray | null => {
    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
   */
  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 (this.validateHex(pubKey)) return pubKey

    // Not a valid hex key
    return null
  }

  generateKeys = (): Keys => {
    const nsec = generateSecretKey()

    return { private: nip19.nsecEncode(nsec), public: getPublicKey(nsec) }
  }

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

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