import browser from 'webextension-polyfill'
import {
  validateEvent,
  finalizeEvent,
  getEventHash,
  getPublicKey,
  nip19,
  utils
} from 'nostr-tools'
import { nip04 } from 'nostr-tools'
import * as nip44 from 'nostr-tools/nip44'
import { Mutex } from 'async-mutex'
import { LRUCache } from './utils'

const { hexToBytes } = utils

import {
  NO_PERMISSIONS_REQUIRED,
  getPermissionStatus,
  updatePermission,
  showNotification,
  getPosition
} from './common'

const { encrypt, decrypt } = nip04

let openPrompt = null
const promptMutex = new Mutex()
let releasePromptMutex = () => {}
const secretsCache = new LRUCache(100)
const previousSk = null

function getSharedSecret(sk, peer) {
  if (previousSk !== sk) {
    secretsCache.clear()
  }

  let key = secretsCache.get(peer)

  if (!key) {
    key = nip44.v2.utils.getConversationKey(sk, peer)
    secretsCache.set(peer, key)
  }

  return key
}

const width = 440
const height = 420

browser.runtime.onInstalled.addListener((_, __, reason) => {
  if (reason === 'install') browser.runtime.openOptionsPage()
})

browser.runtime.onMessage.addListener(async (req, sender) => {
  const { prompt } = req

  if (prompt) {
    handlePromptMessage(req, sender)
  } else {
    return handleContentScriptMessage(req)
  }
})

browser.runtime.onMessageExternal.addListener(
  async ({ type, params }, sender) => {
    const extensionId = new URL(sender.url).host
    return handleContentScriptMessage({ type, params, host: extensionId })
  }
)

browser.windows.onRemoved.addListener((_windowId) => {
  if (openPrompt) {
    // calling this with a simple "no" response will not store anything, so it's fine
    // it will just return a failure
    handlePromptMessage({ accept: false }, null)
  }
})

async function handleContentScriptMessage({ type, params, host }) {
  if (NO_PERMISSIONS_REQUIRED[type]) {
    switch (type) {
      case 'peekPublicKey': {
        const allowed = await getPermissionStatus(host, 'getPublicKey')
        if (allowed === true) return performOperation('getPublicKey', params)
        return ''
      }
      case 'replaceURL': {
        const { protocol_handler: ph } = await browser.storage.local.get([
          'protocol_handler'
        ])
        if (!ph) return false

        const { url } = params
        const raw = url.split('nostr:')[1]
        const { type, data } = nip19.decode(raw)
        const replacements = {
          raw,
          hrp: type,
          hex:
            type === 'npub' || type === 'note'
              ? data
              : type === 'nprofile'
                ? data.pubkey
                : type === 'nevent'
                  ? data.id
                  : null,
          p_or_e: { npub: 'p', note: 'e', nprofile: 'p', nevent: 'e' }[type],
          u_or_n: { npub: 'u', note: 'n', nprofile: 'u', nevent: 'n' }[type],
          relay0: type === 'nprofile' ? data.relays[0] : null,
          relay1: type === 'nprofile' ? data.relays[1] : null,
          relay2: type === 'nprofile' ? data.relays[2] : null
        }
        let result = ph
        Object.entries(replacements).forEach(([pattern, value]) => {
          result = result.replace(new RegExp(`{ *${pattern} *}`, 'g'), value)
        })

        return result
      }
    }

    return
  } else {
    // acquire mutex here before reading policies
    releasePromptMutex = await promptMutex.acquire()

    const allowed = await getPermissionStatus(
      host,
      type,
      type === 'signEvent' ? params.event : undefined
    )

    if (allowed === true) {
      // authorized, proceed
      releasePromptMutex()
      showNotification(host, allowed, type, params)
    } else if (allowed === false) {
      // denied, just refuse immediately
      releasePromptMutex()
      showNotification(host, allowed, type, params)
      return {
        error: 'denied'
      }
    } else {
      // ask for authorization
      try {
        const id = Math.random().toString().slice(4)
        const qs = new URLSearchParams({
          host,
          id,
          params: JSON.stringify(params),
          type
        })

        // prompt will be resolved with true or false
        const accept = await new Promise((resolve, reject) => {
          openPrompt = { resolve, reject }
          const url = `${browser.runtime.getURL(
            'prompt.html'
          )}?${qs.toString()}`

          // center prompt
          const { top, left } = getPosition(width, height)

          if (browser.windows) {
            browser.windows.create({
              url,
              type: 'popup',
              width: width,
              height: height,
              top: top,
              left: left
            })
          } else {
            browser.tabs.create({
              url,
              active: true
            })
          }
        })

        // denied, stop here
        if (!accept) return { error: { message: 'denied' } }
      } catch (err) {
        // errored, stop here
        releasePromptMutex()
        return {
          error: { message: err.message, stack: err.stack }
        }
      }
    }
  }

  // if we're here this means it was accepted
  const results = await browser.storage.local.get('private_key')
  if (!results?.private_key) {
    return { error: 'no private key found' }
  }

  const sk = results.private_key

  try {
    switch (type) {
      case 'getPublicKey': {
        return getPublicKey(hexToBytes(sk))
      }
      case 'getRelays': {
        const results = await browser.storage.local.get('relays')
        return results.relays || {}
      }
      case 'signEvent': {
        const { event } = params

        if (!event.pubkey) event.pubkey = getPublicKey(hexToBytes(sk))
        if (!event.id) event.id = getEventHash(event)
        if (!validateEvent(event))
          return { error: { message: 'invalid event' } }

        const signedEvent = finalizeEvent(event, hexToBytes(sk))
        return signedEvent
      }
      case 'nip04.encrypt': {
        const { peer, plaintext } = params
        return encrypt(sk, peer, plaintext)
      }
      case 'nip04.decrypt': {
        const { peer, ciphertext } = params
        return decrypt(sk, peer, ciphertext)
      }
      case 'nip44.encrypt': {
        const { peer, plaintext } = params
        const key = getSharedSecret(sk, peer)

        return nip44.v2.encrypt(plaintext, key)
      }
      case 'nip44.decrypt': {
        const { peer, ciphertext } = params
        const key = getSharedSecret(sk, peer)

        return nip44.v2.decrypt(ciphertext, key)
      }
    }
  } catch (error) {
    return { error: { message: error.message, stack: error.stack } }
  }
}

async function handlePromptMessage({ host, type, accept, conditions }, sender) {
  // return response
  openPrompt?.resolve?.(accept)

  // update policies
  if (conditions) {
    await updatePermission(host, type, accept, conditions)
  }

  // cleanup this
  openPrompt = null

  // release mutex here after updating policies
  releasePromptMutex()

  // close prompt
  if (sender) {
    if (browser.windows) {
      browser.windows.remove(sender.tab.windowId)
    } else {
      // Android Firefox
      browser.tabs.remove(sender.tab.id)
    }
  }
}
