import axios from 'axios'
import {
  Event,
  EventTemplate,
  finalizeEvent,
  generateSecretKey,
  getPublicKey,
  nip04,
  verifyEvent
} from 'nostr-tools'
import { toast } from 'react-toastify'
import { NostrController } from '../controllers'
import { AuthState } from '../store/auth/types'
import store from '../store/store'
import { CreateSignatureEventContent, Meta } from '../types'
import { hexToNpub, unixNow } from './nostr'
import { parseJson } from './string'
import { hexToBytes } from '@noble/hashes/utils'
import { getHash } from './hash.ts'
import { SIGIT_BLOSSOM } from './const.ts'

/**
 * Uploads a file to a file storage service.
 * @param blob The Blob object representing the file to upload.
 * @param nostrController The NostrController instance for handling authentication.
 * @returns The URL of the uploaded file.
 */
export const uploadToFileStorage = async (file: File) => {
  // Define event metadata for authorization
  const hash = await getHash(await file.arrayBuffer())
  if (!hash) {
    throw new Error("Can't get file hash.")
  }

  const event: EventTemplate = {
    kind: 24242,
    content: 'Authorize Upload',
    created_at: unixNow(),
    tags: [
      ['t', 'upload'],
      ['x', hash],
      ['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
      ['name', file.name],
      ['size', String(file.size)]
    ]
  }

  const key = store.getState().userAppData?.keyPair?.private
  if (!key) {
    throw new Error(
      'Key to interact with blossom server is not defined in user app data'
    )
  }

  // Sign the authorization event using the dedicated key stored in user app data
  const authEvent = finalizeEvent(event, hexToBytes(key))

  // Upload the file to the file storage service using Axios
  const response = await axios.put(`${SIGIT_BLOSSOM}/upload`, file, {
    headers: {
      Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)), // Set authorization header
      'Content-Type': 'application/sigit' // Set content type header
    }
  })

  // Return the URL of the uploaded file
  return response.data.url as string
}

/**
 * Signs an event for a meta.json file.
 * @param content contains content for event.
 * @param nostrController The NostrController instance for signing the event.
 * @param setIsLoading Function to set loading state in the component.
 * @returns A Promise resolving to the signed event, or null if signing fails.
 */
export const signEventForMetaFile = async (
  content: string,
  nostrController: NostrController,
  setIsLoading: (value: React.SetStateAction<boolean>) => void
) => {
  // Construct the event metadata for the meta file
  const event: EventTemplate = {
    kind: 27235, // Event type for meta file
    content: content, // content for event
    created_at: unixNow(), // Current timestamp
    tags: [['-']] // For understanding why "-" tag is used here see: https://github.com/nostr-protocol/nips/blob/protected-events-tag/70.md
  }

  // Sign the event
  return await nostrController.signEvent(event).catch((err) => {
    console.error(err)
    toast.error(err.message || 'Error occurred in signing nostr event')
    setIsLoading(false) // Set loading state to false
    return null
  })
}

/**
 * Generates the content for keys.json file.
 *
 * @param users - An array of public keys.
 * @param key - The key that will be encrypted for each user.
 * @returns A promise that resolves to a JSON string containing the sender's public key and encrypted keys, or null if an error occurs.
 */
export const generateKeysFile = async (
  users: string[],
  key: string
): Promise<string | null> => {
  // Generate a random private key to act as the sender
  const privateKey = generateSecretKey()

  // Calculate the required length to be a multiple of 10
  const requiredLength = Math.ceil(users.length / 10) * 10
  const additionalKeysCount = requiredLength - users.length

  if (additionalKeysCount > 0) {
    // generate random public keys to make the keys array multiple of 10
    const additionalPubkeys = Array.from({ length: additionalKeysCount }, () =>
      getPublicKey(generateSecretKey())
    )

    users.push(...additionalPubkeys)
  }

  // Encrypt the key for each user's public key
  const promises = users.map((pubkey) => nip04.encrypt(privateKey, pubkey, key))

  // Wait for all encryption promises to resolve
  const keys = await Promise.all(promises).catch((err) => {
    console.log('Error while generating keys :>> ', err)
    toast.error(err.message || 'An error occurred while generating key')
    return null
  })

  // If any encryption promise failed, return null
  if (!keys) return null

  try {
    // Return a JSON string containing the sender's public key and encrypted keys
    return JSON.stringify({ sender: getPublicKey(privateKey), keys })
  } catch (error) {
    // Return null if an error occurs during JSON stringification
    return null
  }
}

/**
 * Encrypt decryption key for each users pubkey.
 *
 * @param pubkeys - An array of public keys.
 * @param key - The key that will be encrypted for each user.
 * @returns A promise that resolves to a JSON object containing a random pubkey as sender and encrypted keys for each user, or null if an error occurs.
 */
export const generateKeys = async (
  pubkeys: string[],
  key: string
): Promise<{
  sender: string
  keys: { [user: `npub1${string}`]: string }
} | null> => {
  // Generate a random private key to act as the sender
  const privateKey = generateSecretKey()

  const keys: { [user: `npub1${string}`]: string } = {}

  // Encrypt the key for each user's public key
  for (const pubkey of pubkeys) {
    const npub = hexToNpub(pubkey)

    const encryptedKey = await nip04
      .encrypt(privateKey, pubkey, key)
      .catch((err) => {
        console.log(`An error occurred in encrypting key for ${npub}`, err)
        toast.error('An error occurred in key encryption')
        return null
      })

    if (!encryptedKey) return null

    keys[npub] = encryptedKey
  }

  return { sender: getPublicKey(privateKey), keys }
}

/**
 * Function to extract the ZIP URL and encryption key from the provided metadata.
 * @param meta - The metadata object containing the create signature and encryption keys.
 * @returns A promise that resolves to an object containing the create signature event,
 *          create signature content, ZIP URL, and decrypted encryption key.
 */
export const extractZipUrlAndEncryptionKey = async (meta: Meta) => {
  // Parse the create signature event from the metadata
  const createSignatureEvent = await parseJson<Event>(
    meta.createSignature
  ).catch((err) => {
    // Log and display an error message if parsing fails
    console.log('err in parsing the createSignature event:>> ', err)
    toast.error(
      err.message || 'error occurred in parsing the create signature event'
    )
    return null
  })

  // Return null if the create signature event could not be parsed
  if (!createSignatureEvent) return null

  // Verify the validity of the create signature event
  const isValidCreateSignature = verifyEvent(createSignatureEvent)
  if (!isValidCreateSignature) {
    toast.error('Create signature is invalid')
    return null
  }

  // Parse the content of the create signature event
  const createSignatureContent = await parseJson<CreateSignatureEventContent>(
    createSignatureEvent.content
  ).catch((err) => {
    // Log and display an error message if parsing fails
    console.log(`err in parsing the createSignature event's content :>> `, err)
    toast.error(
      `error occurred in parsing the create signature event's content`
    )
    return null
  })

  // Return null if the create signature content could not be parsed
  if (!createSignatureContent) return null

  // Extract the ZIP URL from the create signature content
  const zipUrl = createSignatureContent.zipUrl

  // Retrieve the user's public key from the state
  const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
  const usersNpub = hexToNpub(usersPubkey)

  // Return null if the metadata does not contain keys
  if (!meta.keys) return null

  const { sender, keys } = meta.keys

  // Check if the user's public key is in the keys object
  if (usersNpub in keys) {
    // Instantiate the NostrController to decrypt the encryption key
    const nostrController = NostrController.getInstance()
    const decrypted = await nostrController
      .nip04Decrypt(sender, keys[usersNpub])
      .catch((err) => {
        // Log and display an error message if decryption fails
        console.log('An error occurred in decrypting encryption key', err)
        toast.error('An error occurred in decrypting encryption key')
        return null
      })

    // Return null if the encryption key could not be decrypted
    if (!decrypted) return null

    // Return the parsed and decrypted data
    return {
      createSignatureEvent,
      createSignatureContent,
      zipUrl,
      encryptionKey: decrypted
    }
  }

  return null
}
