import { NDKEvent, NDKUserProfile } from '@nostr-dev-kit/ndk'
import { hexToBytes } from '@noble/hashes/utils'
import axios from 'axios'
import { truncate } from 'lodash'
import {
  Event,
  EventTemplate,
  UnsignedEvent,
  finalizeEvent,
  generateSecretKey,
  getEventHash,
  getPublicKey,
  nip04,
  nip19,
  nip44,
  verifyEvent
} from 'nostr-tools'
import { NIP05_REGEX } from '../constants'
import store from '../store/store'
import {
  Meta,
  SignedEvent,
  FileServerPutResponse,
  BlossomVersion
} from '../types'
import { SIGIT_BLOSSOM } from './const.ts'
import { getHash } from './hash'
import { parseJson, removeLeadingSlash } from './string'
import { toast } from 'react-toastify'

/**
 * Generates a `d` tag for userAppData
 */
export const getDTagForUserAppData = async (): Promise<string | null> => {
  const isLoggedIn = store.getState().auth.loggedIn
  const pubkey = store.getState().auth?.usersPubkey

  if (!isLoggedIn || !pubkey) {
    throw new Error(
      'For generating d tag user must be logged in and a valid pubkey should exists in app Store'
    )
  }

  return getHash(`938_${pubkey}`)
}

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

  // Not a valid hex key
  return null
}

/**
 * If NSEC key is provided function will convert it to HEX
 * If HEX key Is provided function will validate the HEX format and return
 *
 * @param nsec or private key in HEX format
 */
export const nsecToHex = (nsec: string): string | null => {
  // If key is NSEC
  if (nsec.startsWith('nsec')) {
    try {
      return nip19.decode(nsec).data as string
    } catch (error) {
      return null
    }
  }

  // since it's not NSEC key we check if it's a valid hex key
  if (validateHex(nsec)) return nsec

  return null
}

export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
  if (hexPubkey.startsWith('npub1')) return hexPubkey as `npub1${string}`

  return nip19.npubEncode(hexPubkey)
}

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

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

/**
 * Function to query NIP-05 data and return the public key and relays.
 *
 * @param {string} nip05 - The NIP-05 identifier in the format "name@domain".
 * @returns {Promise<{ pubkey: string, relays: string[] }>} - The public key and an array of relay URLs.
 * @throws Will throw an error if the NIP-05 identifier is invalid or if there is an issue with the network request.
 */
export const queryNip05 = async (
  nip05: string
): Promise<{
  pubkey: string
  relays: string[]
}> => {
  const match = nip05.match(NIP05_REGEX)

  // Throw an error if the NIP-05 identifier is invalid
  if (!match) throw new Error('Invalid nip05')

  // Destructure the match result, assigning default value '_' to name if not provided
  // First variable from the match destructuring is ignored
  const [, name = '_', domain] = match

  // Construct the URL to query the NIP-05 data
  const url = `https://${domain}/.well-known/nostr.json?name=${name}`

  // Perform the network request to get the NIP-05 data
  const res = await axios(url)
    .then((res) => {
      return res.data
    })
    .catch((err) => {
      console.log('err :>> ', err)
      throw err
    })

  // Extract the public key from the response data
  const pubkey = res.names[name]
  const relays: string[] = []

  // If a public key is found
  if (pubkey) {
    // Function to add relays if they exist and are not empty
    const addRelays = (relayList?: string[]) => {
      if (relayList && relayList.length > 0) {
        relays.push(...relayList)
      }
    }

    // Check for user-specific relays in the NIP-46 section of the response data
    if (res.nip46) {
      addRelays(res.nip46[pubkey] as string[])
    }

    // Check for user-specific relays in the relays section of the response data if not found in NIP-46
    if (relays.length === 0 && res.relays) {
      addRelays(res.relays[pubkey] as string[])
    }

    // If no user-specific relays are found, check for root user relays
    if (relays.length === 0) {
      const root = res.names['_']
      if (root) {
        // Check for root user relays in both NIP-46 and relays sections
        addRelays(res.nip46?.[root] as string[])
        addRelays(res.relays?.[root] as string[])
      }
    }
  }

  // Return the public key and the array of relays
  return {
    pubkey,
    relays
  }
}

/**
 * @param pubkey in hex or npub format
 * @returns robohash.org url for the avatar
 */
export const getRoboHashPicture = (
  pubkey?: string,
  set: number = 1
): string => {
  if (!pubkey) return ''
  const npub = hexToNpub(pubkey)
  return `https://robohash.org/${npub}.png?set=set${set}`
}

export const unixNow = () => Math.round(Date.now() / 1000)
export const toUnixTimestamp = (date: number | Date) => {
  let time
  if (typeof date === 'number') {
    time = Math.round(date / 1000)
  } else if (date instanceof Date) {
    time = Math.round(date.getTime() / 1000)
  } else {
    throw Error('Unsupported type when converting to unix timestamp')
  }

  return time
}
export const fromUnixTimestamp = (unix: number) => {
  return unix * 1000
}

/**
 * Generate nip44 conversation key
 * @param privateKey
 * @param publicKey
 * @returns
 */
export const nip44ConversationKey = (
  privateKey: Uint8Array,
  publicKey: string
) => nip44.v2.utils.getConversationKey(privateKey, publicKey)

export const nip44Encrypt = (
  data: UnsignedEvent,
  privateKey: Uint8Array,
  publicKey: string
) =>
  nip44.v2.encrypt(
    JSON.stringify(data),
    nip44ConversationKey(privateKey, publicKey)
  )

export const nip44Decrypt = (data: Event, privateKey: Uint8Array) =>
  JSON.parse(
    nip44.v2.decrypt(
      data.content,
      nip44ConversationKey(privateKey, data.pubkey)
    )
  )

// Function to count leading zero bits
export const countLeadingZeroes = (hex: string) => {
  let count = 0

  for (let i = 0; i < hex.length; i++) {
    const nibble = parseInt(hex[i], 16)
    if (nibble === 0) {
      count += 4
    } else {
      count += Math.clz32(nibble) - 28
      break
    }
  }

  return count
}

/**
 * Function to create a wrapped event with PoW
 * @param event Original event to be wrapped
 * @param receiver Public key of the receiver
 * @param difficulty PoW difficulty level (default is 20)
 * @returns
 */
//
export const createWrap = (unsignedEvent: UnsignedEvent, receiver: string) => {
  // Generate a random secret key and its corresponding public key
  const randomKey = generateSecretKey()
  const pubkey = getPublicKey(randomKey)

  // Encrypt the event content using nip44 encryption
  const content = nip44Encrypt(unsignedEvent, randomKey, receiver)

  // Initialize nonce and leadingZeroes for PoW calculation
  let nonce = 0
  let leadingZeroes = 0
  const difficulty = Math.floor(Math.random() * 10) + 5 // random number between 5 & 10

  // Loop until a valid PoW hash is found
  // eslint-disable-next-line no-constant-condition
  while (true) {
    // Create an unsigned event with the necessary fields
    const event: UnsignedEvent = {
      kind: 1059, // Event kind
      content, // Encrypted content
      pubkey, // Public key of the creator
      created_at: unixNow(), // Current timestamp
      tags: [
        // Tags including receiver and nonce
        ['p', receiver],
        ['nonce', nonce.toString(), difficulty.toString()]
      ]
    }

    // Calculate the SHA-256 hash of the unsigned event
    const hash = getEventHash(event)

    // Count the number of leading zero bits in the hash
    leadingZeroes = countLeadingZeroes(hash)

    // Check if the leading zero bits meet the required difficulty
    if (leadingZeroes >= difficulty) {
      // Finalize the event (sign it) and return the result
      return finalizeEvent(event, randomKey)
    }

    // Increment the nonce for the next iteration
    nonce++
  }
}

export const deleteBlossomFile = async (url: string, privateKey: string) => {
  const pathname = new URL(url).pathname
  const hash = removeLeadingSlash(pathname)

  // Define event metadata for authorization
  const event: EventTemplate = {
    kind: 24242,
    content: 'Authorize Upload',
    created_at: unixNow(),
    tags: [
      ['t', 'delete'],
      ['expiration', String(unixNow() + 60 * 5)], // Set expiration time to 5 minutes from now
      ['x', hash]
    ]
  }

  const authEvent = finalizeEvent(event, hexToBytes(privateKey))

  // delete the file stored on file storage service using Axios
  const response = await axios.delete(url, {
    headers: {
      Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
    }
  })

  console.log('response.data :>> ', response.data)
}

/**
 * Function to upload user application data to the user preferred File (Blossom) servers.
 * @param sigits - An object containing metadata for the user application data.
 * @param processedGiftWraps - An array of processed gift wrap IDs.
 * @param privateKey - The private key used for encryption.
 * @returns A promise that resolves to the URL of the uploaded file.
 */
export const uploadUserAppDataToBlossom = async (
  sigits: { [key: string]: Meta },
  processedGiftWraps: string[],
  privateKey: string
) => {
  const preferredServersMap = store.getState().servers.map || {}
  const preferredServers = Object.keys(preferredServersMap)
  // If no servers found, use SIGIT as fallback
  if (!preferredServers.length) preferredServers.push(SIGIT_BLOSSOM)

  // Create an object containing the sigits and processed gift wraps
  const obj = {
    sigits,
    processedGiftWraps
  }
  // Convert the object to a JSON string
  const stringified = JSON.stringify(obj)

  // Convert the private key from hex to bytes
  const secretKey = hexToBytes(privateKey)
  // Encrypt the JSON string using the secret key
  const encrypted = await nip04.encrypt(
    secretKey,
    getPublicKey(secretKey),
    stringified
  )

  // Create a blob from the encrypted data
  const blob = new Blob([encrypted], { type: 'application/octet-stream' })
  // Create a file from the blob
  const file = new File([blob], 'encrypted.txt', {
    type: 'application/octet-stream'
  })

  const hash = await getHash(await file.arrayBuffer())
  if (!hash) {
    throw new Error("Can't get file hash.")
  }

  // Define event metadata for authorization
  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)]
    ]
  }

  // Finalize the event with the private key
  const authEvent = finalizeEvent(event, hexToBytes(privateKey))

  // Upload the file to the file storage services using Axios
  const responses = await Promise.all(
    preferredServers.map((preferredServer) => {
      return axios.put<FileServerPutResponse>(
        `${preferredServer}/upload`,
        file,
        {
          headers: {
            Authorization: 'Nostr ' + btoa(JSON.stringify(authEvent)) // Set authorization header
          }
        }
      )
    })
  )

  // Return the URLs of the uploaded files
  return responses.map((response) => response.data.url) as string[]
}

/**
 * Function to retrieve and decrypt user application data from file (Blossom) servers.
 * Since we pull from multiple servers, we will take the first one
 * @param blossomVersion - The URL to fetch the encrypted data from.
 * @param privateKey - The private key used for decryption.
 * @returns A promise that resolves to the decrypted and parsed user application data.
 */
export const getUserAppDataFromBlossom = async (
  blossomVersion: BlossomVersion,
  privateKey: string
) => {
  // Initialize errorCode to track HTTP error codes
  let errorCode = 0

  const blossomUrl = blossomVersion.urls[0]

  // Fetch the encrypted data from the provided URL
  const encrypted = await axios
    .get(blossomUrl, {
      responseType: 'blob' // Expect a blob response
    })
    .then(async (res) => {
      // Convert the blob response to a File object
      const file = new File([res.data], 'encrypted.txt')
      // Read the text content from the file
      const text = await file.text()
      return text
    })
    .catch((err) => {
      // Log and display an error message if the request fails
      console.error(
        `error occurred in getting file from ${blossomVersion}`,
        err
      )
      toast.error(
        err.message || `error occurred in getting file from ${blossomVersion}`
      )

      // Set errorCode to the HTTP status code if available
      if (err.request) {
        const { status } = err.request
        errorCode = status
      }

      return null
    })

  // Return a default value if the requested resource is not found (404)
  if (errorCode === 404) {
    return {
      sigits: {},
      processedGiftWraps: []
    }
  }

  // Return null if the encrypted data could not be retrieved
  if (!encrypted) return null

  // Convert the private key from hex to bytes
  const secret = hexToBytes(privateKey)
  // Get the public key corresponding to the private key
  const pubkey = getPublicKey(secret)

  // Decrypt the encrypted data using the secret and public key
  const decrypted = await nip04.decrypt(secret, pubkey, encrypted)

  // Parse the decrypted JSON content
  const parsedContent = await parseJson<{
    sigits: { [key: string]: Meta }
    processedGiftWraps: string[]
  }>(decrypted).catch((err) => {
    // Log and display an error message if parsing fails
    console.log(
      'An error occurred in parsing the user app data content from blossom server',
      err
    )
    toast.error(
      'An error occurred in parsing the user app data content from blossom server'
    )
    return null
  })

  // Return the parsed content
  return parsedContent
}

/**
 * Show user's name, first available in order: display_name, name, or npub as fallback
 * @param npub User identifier, it can be either pubkey or npub1 (we only show npub)
 * @param profile User profile
 */
export const getProfileUsername = (
  npub: `npub1${string}` | string,
  profile?: NDKUserProfile
) =>
  truncate(profile?.displayName || profile?.name || hexToNpub(npub), {
    length: 16
  })

/**
 * Orders an array of NDKEvent objects chronologically based on their `created_at` property.
 *
 * @param events - The array of NDKEvent objects to be sorted.
 * @param reverse - Optional flag to reverse the sorting order.
 * If true, sorts in ascending order (oldest first), otherwise sorts in descending order (newest first).
 *
 * @returns The sorted array of events.
 */
export function orderEventsChronologically(
  events: NDKEvent[],
  reverse: boolean = false
): NDKEvent[] {
  events.sort((e1: NDKEvent, e2: NDKEvent) => {
    if (reverse) return e1.created_at! - e2.created_at!
    else return e2.created_at! - e1.created_at!
  })

  return events
}
