import styles from './style.module.scss'
import {
  Box,
  Button,
  CircularProgress,
  FormHelperText,
  TextField,
  Tooltip
} from '@mui/material'
import type { Identifier, XYCoord } from 'dnd-core'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import { Event, kinds } from 'nostr-tools'
import { useCallback, useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
import { MultiBackend } from 'react-dnd-multi-backend'
import { HTML5toTouch } from 'rdndmb-html5-to-touch'
import { useAppSelector } from '../../hooks/store'
import { useLocation, useNavigate } from 'react-router-dom'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { UserAvatar } from '../../components/UserAvatar'
import {
  MetadataController,
  NostrController,
  RelayController
} from '../../controllers'
import { appPrivateRoutes, appPublicRoutes } from '../../routes'
import {
  CreateSignatureEventContent,
  KeyboardCode,
  Meta,
  ProfileMetadata,
  SigitNotification,
  SignedEvent,
  User,
  UserRole
} from '../../types'
import {
  encryptArrayBuffer,
  formatTimestamp,
  generateEncryptionKey,
  generateKeys,
  generateKeysFile,
  getHash,
  hexToNpub,
  unixNow,
  npubToHex,
  queryNip05,
  sendNotification,
  signEventForMetaFile,
  updateUsersAppData,
  uploadToFileStorage,
  DEFAULT_TOOLBOX,
  settleAllFullfilfedPromises,
  DEFAULT_LOOK_UP_RELAY_LIST,
  uploadMetaToFileStorage
} from '../../utils'
import { Container } from '../../components/Container'
import fileListStyles from '../../components/FileList/style.module.scss'
import { DrawTool } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
  faDownload,
  faEllipsis,
  faEye,
  faFile,
  faFileCirclePlus,
  faGripLines,
  faPen,
  faPlus,
  faSearch,
  faToolbox,
  faTrash,
  faUpload
} from '@fortawesome/free-solid-svg-icons'
import { getSigitFile, SigitFile } from '../../utils/file.ts'
import { generateTimestamp } from '../../utils/opentimestamps.ts'
import { Autocomplete } from '@mui/material'
import _, { truncate } from 'lodash'
import * as React from 'react'
import { AvatarIconButton } from '../../components/UserAvatarIconButton'
import { useImmer } from 'use-immer'
import { ButtonUnderline } from '../../components/ButtonUnderline/index.tsx'

type FoundUser = Event & { npub: string }

export const CreatePage = () => {
  const navigate = useNavigate()
  const location = useLocation()
  const { uploadedFiles } = location.state || {}
  const [currentFile, setCurrentFile] = useState<File>()
  const isActive = (file: File) => file.name === currentFile?.name

  const [isLoading, setIsLoading] = useState(false)
  const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')

  const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)

  const [selectedFiles, setSelectedFiles] = useState<File[]>([...uploadedFiles])
  const fileInputRef = useRef<HTMLInputElement>(null)
  const handleUploadButtonClick = () => {
    if (fileInputRef.current) {
      fileInputRef.current.click()
    }
  }

  const [userInput, setUserInput] = useState('')
  const [userSearchInput, setUserSearchInput] = useState('')

  const [userRole] = useState<UserRole>(UserRole.signer)
  const [error, setError] = useState<string>()

  const [users, setUsers] = useState<User[]>([])
  const signers = users.filter((u) => u.role === UserRole.signer)
  const viewers = users.filter((u) => u.role === UserRole.viewer)

  const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)!

  const nostrController = NostrController.getInstance()

  const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
    {}
  )
  const [drawnFiles, updateDrawnFiles] = useImmer<SigitFile[]>([])
  const [parsingPdf, setIsParsing] = useState<boolean>(false)

  const searchFieldRef = useRef<HTMLInputElement>(null)

  const [selectedTool, setSelectedTool] = useState<DrawTool>()

  const [foundUsers, setFoundUsers] = useState<FoundUser[]>([])
  const [searchUsersLoading, setSearchUsersLoading] = useState<boolean>(false)
  const [pastedUserNpubOrNip05, setPastedUserNpubOrNip05] = useState<
    string | undefined
  >()

  /**
   * Fired when user select
   */
  const handleSearchUserChange = useCallback(
    (_event: React.SyntheticEvent, value: string | FoundUser | null) => {
      if (typeof value === 'object') {
        const ndkEvent = value as FoundUser
        if (ndkEvent?.pubkey) {
          setUserInput(hexToNpub(ndkEvent.pubkey))
        }
      }
    },
    [setUserInput]
  )

  const handleSearchUserNip05 = async (
    nip05: string
  ): Promise<string | null> => {
    const { pubkey } = await queryNip05(nip05).catch((err) => {
      console.error(err)
      return { pubkey: null }
    })

    return pubkey
  }

  const handleSearchUsers = async (searchValue?: string) => {
    const searchString = searchValue || userSearchInput || undefined

    if (!searchString) return

    setSearchUsersLoading(true)

    const relayController = RelayController.getInstance()
    const metadataController = MetadataController.getInstance()

    const relaySet = await metadataController.findRelayListMetadata(usersPubkey)

    DEFAULT_LOOK_UP_RELAY_LIST.forEach((relay) => {
      if (!relaySet.write.includes(relay)) relaySet.write.push(relay)
      if (!relaySet.read.includes(relay)) relaySet.read.push(relay)
    })

    const uniqueReadRelaySet = [...new Set(relaySet.read)]

    const searchTerm = searchString.trim()

    relayController
      .fetchEvents(
        {
          kinds: [0],
          search: searchTerm
        },
        uniqueReadRelaySet
      )
      .then((events) => {
        console.log('events', events)

        const fineFilteredEvents: FoundUser[] = events
          .filter((event) => {
            const lowercaseContent = event.content.toLowerCase()

            return (
              lowercaseContent.includes(
                `"name":"${searchTerm.toLowerCase()}"`
              ) ||
              lowercaseContent.includes(
                `"display_name":"${searchTerm.toLowerCase()}"`
              ) ||
              lowercaseContent.includes(
                `"username":"${searchTerm.toLowerCase()}"`
              ) ||
              lowercaseContent.includes(`"nip05":"${searchTerm.toLowerCase()}"`)
            )
          })
          .reduce((uniqueEvents: FoundUser[], event: Event) => {
            if (!uniqueEvents.some((e: Event) => e.pubkey === event.pubkey)) {
              uniqueEvents.push({
                ...event,
                npub: hexToNpub(event.pubkey)
              })
            }
            return uniqueEvents
          }, [])

        console.info('fineFilteredEvents', fineFilteredEvents)
        setFoundUsers(fineFilteredEvents)

        if (!fineFilteredEvents.length)
          toast.info('No user found with the provided search term')
      })
      .catch((error) => {
        console.error(error)
      })
      .finally(() => {
        setSearchUsersLoading(false)
      })
  }

  useEffect(() => {
    setTimeout(() => {
      if (foundUsers.length) {
        if (searchFieldRef.current) {
          searchFieldRef.current.blur()
          searchFieldRef.current.focus()
        }
      }
    })
  }, [foundUsers])

  const handleInputKeyDown = async (
    event: React.KeyboardEvent<HTMLDivElement>
  ) => {
    if (
      event.code === KeyboardCode.Enter ||
      event.code === KeyboardCode.NumpadEnter
    ) {
      event.preventDefault()

      // If pasted user npub of nip05 is present, we just add the user to the counterparts list
      if (pastedUserNpubOrNip05) {
        setUserInput(pastedUserNpubOrNip05)
        setPastedUserNpubOrNip05(undefined)
      } else {
        // Otherwize if search already provided some results, user must manually click the search button
        if (!foundUsers.length) {
          // If it's NIP05 (includes @ or is a valid domain) send request to .well-known
          const domainRegex = /^[a-zA-Z0-9@.-]+\.[a-zA-Z]{2,}$/
          if (domainRegex.test(userSearchInput)) {
            setSearchUsersLoading(true)

            const pubkey = await handleSearchUserNip05(userSearchInput)

            setSearchUsersLoading(false)

            if (pubkey) {
              setUserInput(userSearchInput)
            } else {
              toast.error(`No user found with the NIP05: ${userSearchInput}`)
            }
          } else {
            handleSearchUsers()
          }
        }
      }
    }
  }

  useEffect(() => {
    if (selectedFiles) {
      /**
       * Reads the binary files and converts to an internal file type
       * and sets to a state (adds images if it's a PDF)
       */
      const parsePages = async () => {
        const files = await settleAllFullfilfedPromises(
          selectedFiles,
          getSigitFile
        )
        updateDrawnFiles((draft) => {
          // Existing files are untouched

          // Handle removed files
          // Remove in reverse to avoid index issues
          for (let i = draft.length - 1; i >= 0; i--) {
            if (
              !files.some(
                (f) => f.name === draft[i].name && f.size === draft[i].size
              )
            ) {
              draft.splice(i, 1)
            }
          }

          // Add new files
          files.forEach((f) => {
            if (!draft.some((d) => d.name === f.name && d.size === f.size)) {
              draft.push(f)
            }
          })
        })
      }

      setIsParsing(true)

      parsePages().finally(() => {
        setIsParsing(false)
      })
    }
  }, [selectedFiles, updateDrawnFiles])

  /**
   * Changes the drawing tool
   * @param drawTool to draw with
   */
  const handleToolSelect = (drawTool: DrawTool) => {
    // If clicked on the same tool, unselect
    if (drawTool.identifier === selectedTool?.identifier) {
      setSelectedTool(undefined)
      return
    }

    setSelectedTool(drawTool)
  }

  useEffect(() => {
    users.forEach((user) => {
      if (!(user.pubkey in metadata)) {
        const metadataController = MetadataController.getInstance()

        const handleMetadataEvent = (event: Event) => {
          const metadataContent =
            metadataController.extractProfileMetadataContent(event)
          if (metadataContent)
            setMetadata((prev) => ({
              ...prev,
              [user.pubkey]: metadataContent
            }))
        }

        metadataController.on(user.pubkey, (kind: number, event: Event) => {
          if (kind === kinds.Metadata) {
            handleMetadataEvent(event)
          }
        })

        metadataController
          .findMetadata(user.pubkey)
          .then((metadataEvent) => {
            if (metadataEvent) handleMetadataEvent(metadataEvent)
          })
          .catch((err) => {
            console.error(
              `error occurred in finding metadata for: ${user.pubkey}`,
              err
            )
          })
      }
    })
  }, [metadata, users])

  useEffect(() => {
    if (usersPubkey) {
      setUsers((prev) => {
        const existingUserIndex = prev.findIndex(
          (user) => user.pubkey === usersPubkey
        )

        // make logged in user the first signer by default
        if (existingUserIndex === -1)
          return [{ pubkey: usersPubkey, role: UserRole.signer }, ...prev]

        return prev
      })
    }
  }, [usersPubkey])

  const handleAddUser = useCallback(async () => {
    setError(undefined)

    const addUser = (pubkey: string) => {
      setUsers((prev) => {
        const signers = prev.filter((user) => user.role === UserRole.signer)
        const viewers = prev.filter((user) => user.role === UserRole.viewer)

        const existingUserIndex = prev.findIndex(
          (user) => user.pubkey === pubkey
        )

        // add new
        if (existingUserIndex === -1) {
          if (userRole === UserRole.signer) {
            return [...signers, { pubkey, role: userRole }, ...viewers]
          } else {
            return [...signers, ...viewers, { pubkey, role: userRole }]
          }
        }

        const existingUser = prev[existingUserIndex]

        // return existing
        if (existingUser.role === userRole) return prev

        // change user role
        const updatedUsers = [...prev]
        const updatedUser = { ...updatedUsers[existingUserIndex] }
        updatedUser.role = userRole
        updatedUsers[existingUserIndex] = updatedUser

        // signers should be placed at the start of the  array
        return [
          ...updatedUsers.filter((user) => user.role === UserRole.signer),
          ...updatedUsers.filter((user) => user.role === UserRole.viewer)
        ]
      })
    }

    const input = userInput.toLowerCase()

    setUserSearchInput('')

    if (input.startsWith('npub')) {
      return handleAddNpubUser(input)
    }

    if (input.includes('@')) {
      return await handleAddNip05User(input)
    }

    // If the user enters the domain (w/o @) assume it's the "root" and append _@
    // https://github.com/nostr-protocol/nips/blob/master/05.md#showing-just-the-domain-as-an-identifier
    if (input.includes('.')) {
      return await handleAddNip05User(`_@${input}`)
    }

    setError('Invalid input! Make sure to provide correct npub or nip05.')

    async function handleAddNip05User(input: string) {
      setIsLoading(true)
      setLoadingSpinnerDesc('Querying for nip05')
      const nip05Profile = await queryNip05(input)
        .catch((err) => {
          console.error(`error occurred in querying nip05: ${input}`, err)
          return null
        })
        .finally(() => {
          setIsLoading(false)
          setLoadingSpinnerDesc('')
        })

      if (nip05Profile && nip05Profile.pubkey) {
        const pubkey = nip05Profile.pubkey
        addUser(pubkey)
        setUserInput('')
      } else {
        setError('Provided nip05 is not valid. Please enter correct nip05.')
      }
      return
    }

    function handleAddNpubUser(input: string) {
      const pubkey = npubToHex(input)
      if (pubkey) {
        addUser(pubkey)
        setUserInput('')
      } else {
        setError('Provided npub is not valid. Please enter correct npub.')
      }
      return
    }
  }, [
    userInput,
    userRole,
    setError,
    setUsers,
    setUserSearchInput,
    setIsLoading,
    setLoadingSpinnerDesc,
    setUserInput
  ])

  useEffect(() => {
    if (userInput?.length > 0) handleAddUser()
  }, [handleAddUser, userInput])

  const handleUserRoleChange = (role: UserRole, pubkey: string) => {
    setUsers((prevUsers) =>
      prevUsers.map((user) => {
        if (user.pubkey === pubkey) {
          return {
            ...user,
            role
          }
        }

        return user
      })
    )
  }

  const handleRemoveUser = (pubkey: string) => {
    setUsers((prev) => prev.filter((user) => user.pubkey !== pubkey))

    // Set counterpart to ''
    const drawnFilesCopy = _.cloneDeep(drawnFiles)
    drawnFilesCopy.forEach((s) => {
      s.pages?.forEach((p) => {
        p.drawnFields.forEach((d) => {
          if (d.counterpart === hexToNpub(pubkey)) {
            d.counterpart = ''
          }
        })
      })
    })
    updateDrawnFiles(drawnFilesCopy)
  }

  /**
   * changes the position of signer in the signers list
   *
   * @param dragIndex represents the current position of user
   * @param hoverIndex represents the target position of user
   */
  const moveSigner = (dragIndex: number, hoverIndex: number) => {
    setUsers((prevUsers) => {
      const updatedUsers = [...prevUsers]
      const [draggedUser] = updatedUsers.splice(dragIndex, 1)
      updatedUsers.splice(hoverIndex, 0, draggedUser)
      return updatedUsers
    })
  }

  const handleSelectFiles = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files) {
      // Get the uploaded files
      const files = Array.from(event.target.files)

      // Remove duplicates based on the file.name
      setSelectedFiles((p) => {
        const unique = [...p, ...files].filter(
          (file, i, array) => i === array.findIndex((t) => t.name === file.name)
        )
        navigate('.', {
          state: { uploadedFiles: unique },
          replace: true
        })
        return unique
      })
    }
  }

  const handleFileClick = (id: string) => {
    document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' })
  }

  const handleRemoveFile = (
    event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
    fileToRemove: File
  ) => {
    event.stopPropagation()

    setSelectedFiles((prevFiles) => {
      const files = prevFiles.filter((file) => file.name !== fileToRemove.name)
      navigate('.', {
        state: { uploadedFiles: files },
        replace: true
      })
      return files
    })
  }

  // Validate inputs before proceeding
  const validateInputs = (): boolean => {
    if (!title.trim()) {
      toast.error('Title can not be empty')
      return false
    }

    if (!users.some((u) => u.role === UserRole.signer)) {
      toast.error('No signer is provided. At least add one signer.')
      return false
    }

    if (selectedFiles.length === 0) {
      toast.error('No file is selected. Select at least 1 file')
      return false
    }

    return true
  }

  // Handle errors during file arrayBuffer conversion
  const handleFileError = (file: File) => (err: unknown) => {
    console.log(
      `Error while getting arrayBuffer of file ${file.name} :>> `,
      err
    )
    if (err instanceof Error) {
      toast.error(
        err.message || `Error while getting arrayBuffer of file ${file.name}`
      )
    }
    return null
  }

  // Generate hash for each selected file
  const generateFileHashes = async (): Promise<{
    [key: string]: string
  } | null> => {
    const fileHashes: { [key: string]: string } = {}

    for (const file of selectedFiles) {
      const arraybuffer = await file.arrayBuffer().catch(handleFileError(file))
      if (!arraybuffer) return null

      const hash = await getHash(arraybuffer)
      if (!hash) {
        return null
      }

      fileHashes[file.name] = hash
    }

    return fileHashes
  }

  const createMarks = (fileHashes: { [key: string]: string }): Mark[] => {
    return drawnFiles
      .flatMap((file) => {
        const fileHash = fileHashes[file.name]
        return (
          file.pages?.flatMap((page, index) => {
            return page.drawnFields.map((drawnField) => {
              if (!drawnField.counterpart) {
                throw new Error('Missing counterpart')
              }
              return {
                type: drawnField.type,
                location: {
                  page: index,
                  top: drawnField.top,
                  left: drawnField.left,
                  height: drawnField.height,
                  width: drawnField.width
                },
                npub: drawnField.counterpart,
                pdfFileHash: fileHash,
                fileName: file.name
              }
            })
          }) || []
        )
      })
      .map((mark, index) => {
        return { ...mark, id: index }
      })
  }

  // Handle errors during zip file generation
  const handleZipError = (err: unknown) => {
    console.log('Error in zip:>> ', err)
    setIsLoading(false)
    if (err instanceof Error) {
      toast.error(err.message || 'Error occurred in generating zip file')
    }
    return null
  }

  // Generate the zip file
  const generateZipFile = async (zip: JSZip): Promise<ArrayBuffer | null> => {
    setLoadingSpinnerDesc('Generating zip file')

    return await zip
      .generateAsync({
        type: 'arraybuffer',
        compression: 'DEFLATE',
        compressionOptions: { level: 6 }
      })
      .catch(handleZipError)
  }

  // Encrypt the zip file with the generated encryption key
  const encryptZipFile = async (
    arraybuffer: ArrayBuffer,
    encryptionKey: string
  ): Promise<ArrayBuffer> => {
    setLoadingSpinnerDesc('Encrypting zip file')
    return encryptArrayBuffer(arraybuffer, encryptionKey)
  }

  // create final zip file for offline mode
  const createFinalZipFile = async (
    encryptedArrayBuffer: ArrayBuffer,
    encryptionKey: string
  ): Promise<File | null> => {
    // Get the current timestamp in seconds
    const blob = new Blob([encryptedArrayBuffer])
    // Create a File object with the Blob data
    const file = new File([blob], `compressed.sigit`, {
      type: 'application/sigit'
    })

    const firstSigner = users.filter((user) => user.role === UserRole.signer)[0]

    const keysFileContent = await generateKeysFile(
      [firstSigner.pubkey],
      encryptionKey
    )
    if (!keysFileContent) return null

    const zip = new JSZip()
    zip.file(`compressed.sigit`, file)
    zip.file('keys.json', keysFileContent)

    const arraybuffer = await zip
      .generateAsync({
        type: 'arraybuffer',
        compression: 'DEFLATE',
        compressionOptions: { level: 6 }
      })
      .catch(handleZipError)

    if (!arraybuffer) return null

    return new File([new Blob([arraybuffer])], `${unixNow()}.sigit.zip`, {
      type: 'application/zip'
    })
  }

  // Handle errors during file upload
  const handleUploadError = (err: unknown) => {
    console.log('Error in upload:>> ', err)
    setIsLoading(false)
    if (err instanceof Error) {
      toast.error(err.message || 'Error occurred in uploading file')
    }
    return null
  }

  // Upload the file to the storage
  const uploadFile = async (
    arrayBuffer: ArrayBuffer
  ): Promise<string | null> => {
    const blob = new Blob([arrayBuffer])
    // Create a File object with the Blob data
    const file = new File([blob], `compressed-${unixNow()}.sigit`, {
      type: 'application/sigit'
    })

    return await uploadToFileStorage(file)
      .then((url) => {
        toast.success('files.zip uploaded to file storage')
        return url
      })
      .catch(handleUploadError)
  }

  const generateFilesZip = async (): Promise<ArrayBuffer | null> => {
    const zip = new JSZip()
    selectedFiles.forEach((file) => {
      zip.file(file.name, file)
    })

    return await zip
      .generateAsync({
        type: 'arraybuffer',
        compression: 'DEFLATE',
        compressionOptions: { level: 6 }
      })
      .catch(handleZipError)
  }

  const generateCreateSignature = async (
    markConfig: Mark[],
    fileHashes: {
      [key: string]: string
    },
    zipUrl: string
  ) => {
    const content: CreateSignatureEventContent = {
      signers: signers.map((signer) => hexToNpub(signer.pubkey)),
      viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
      fileHashes,
      markConfig,
      zipUrl,
      title
    }

    setLoadingSpinnerDesc('Preparing document(s) for signing')

    const createSignature = await signEventForMetaFile(
      JSON.stringify(content),
      nostrController,
      setIsLoading
    ).catch(() => {
      console.log('An error occurred in signing event for meta file', error)
      toast.error('An error occurred in signing event for meta file')
      return null
    })

    if (!createSignature) return null

    return JSON.stringify(createSignature, null, 2)
  }

  // Send notifications to signers and viewers
  const sendNotifications = (notification: SigitNotification) => {
    // no need to send notification to self so remove it from the list
    const receivers = (
      signers.length > 0
        ? [signers[0].pubkey]
        : viewers.map((viewer) => viewer.pubkey)
    ).filter((receiver) => receiver !== usersPubkey)

    return receivers.map((receiver) => sendNotification(receiver, notification))
  }

  const extractNostrId = (stringifiedEvent: string): string => {
    const e = JSON.parse(stringifiedEvent) as SignedEvent
    return e.id
  }

  const initCreation = async () => {
    try {
      if (!validateInputs()) return

      setIsLoading(true)
      setLoadingSpinnerDesc('Generating file hashes')
      const fileHashes = await generateFileHashes()
      if (!fileHashes) return

      setLoadingSpinnerDesc('Generating encryption key')
      const encryptionKey = await generateEncryptionKey()

      setLoadingSpinnerDesc('Creating marks')
      const markConfig = createMarks(fileHashes)

      return {
        encryptionKey,
        markConfig,
        fileHashes
      }
    } catch (error) {
      if (error instanceof Error) {
        toast.error(error.message)
      }
      console.error(error)
      setIsLoading(false)
    }
  }

  const handleCreate = async () => {
    try {
      const result = await initCreation()
      if (!result) return

      const { encryptionKey, markConfig, fileHashes } = result

      setLoadingSpinnerDesc('generating files.zip')
      const arrayBuffer = await generateFilesZip()
      if (!arrayBuffer) return

      setLoadingSpinnerDesc('Encrypting files.zip')
      const encryptedArrayBuffer = await encryptZipFile(
        arrayBuffer,
        encryptionKey
      )

      setLoadingSpinnerDesc('Uploading files.zip to file storage')
      const fileUrl = await uploadFile(encryptedArrayBuffer)
      if (!fileUrl) return

      setLoadingSpinnerDesc('Generating create signature')
      const createSignature = await generateCreateSignature(
        markConfig,
        fileHashes,
        fileUrl
      )
      if (!createSignature) return

      setLoadingSpinnerDesc('Generating keys for decryption')

      // generate key pairs for decryption
      const pubkeys = users.map((user) => user.pubkey)
      // also add creator in the list
      if (pubkeys.includes(usersPubkey!)) {
        pubkeys.push(usersPubkey!)
      }

      const keys = await generateKeys(pubkeys, encryptionKey)
      if (!keys) return

      setLoadingSpinnerDesc('Generating an open timestamp.')

      const timestamp = await generateTimestamp(extractNostrId(createSignature))

      const meta: Meta = {
        createSignature,
        keys,
        modifiedAt: unixNow(),
        docSignatures: {}
      }

      if (timestamp) {
        meta.timestamps = [timestamp]
      }

      setLoadingSpinnerDesc('Updating user app data')

      const event = await updateUsersAppData(meta)
      if (!event) return

      const metaUrl = await uploadMetaToFileStorage(meta, encryptionKey)

      setLoadingSpinnerDesc('Sending notifications to counterparties')
      const promises = sendNotifications({
        metaUrl,
        keys: meta.keys
      })

      await Promise.all(promises)
        .then(() => {
          toast.success('Notifications sent successfully')
        })
        .catch(() => {
          toast.error('Failed to publish notifications')
        })

      const isFirstSigner = signers[0].pubkey === usersPubkey

      if (isFirstSigner) {
        navigate(appPrivateRoutes.sign, { state: { meta } })
      } else {
        const createSignatureJson = JSON.parse(createSignature)
        navigate(`${appPublicRoutes.verify}/${createSignatureJson.id}`)
      }
    } catch (error) {
      if (error instanceof Error) {
        toast.error(error.message)
      }
      console.error(error)
    } finally {
      setIsLoading(false)
    }
  }

  const handleCreateOffline = async () => {
    try {
      const result = await initCreation()
      if (!result) return

      const { encryptionKey, markConfig, fileHashes } = result

      const zip = new JSZip()

      selectedFiles.forEach((file) => {
        zip.file(`files/${file.name}`, file)
      })

      setLoadingSpinnerDesc('Generating create signature')
      const createSignature = await generateCreateSignature(
        markConfig,
        fileHashes,
        ''
      )
      if (!createSignature) return

      const meta: Meta = {
        createSignature,
        modifiedAt: unixNow(),
        docSignatures: {}
      }

      // add meta to zip
      try {
        const stringifiedMeta = JSON.stringify(meta, null, 2)
        zip.file('meta.json', stringifiedMeta)
      } catch (err) {
        console.error(err)
        toast.error('An error occurred in converting meta json to string')
        return null
      }

      const arrayBuffer = await generateZipFile(zip)
      if (!arrayBuffer) return

      setLoadingSpinnerDesc('Encrypting zip file')
      const encryptedArrayBuffer = await encryptZipFile(
        arrayBuffer,
        encryptionKey
      )

      const finalZipFile = await createFinalZipFile(
        encryptedArrayBuffer,
        encryptionKey
      )

      if (!finalZipFile) {
        setIsLoading(false)
        return
      }

      saveAs(finalZipFile, `request-${unixNow()}.sigit.zip`)

      // If user is the next signer, we can navigate directly to sign page
      if (signers[0].pubkey === usersPubkey) {
        navigate(appPrivateRoutes.sign, {
          state: { uploadedZip: finalZipFile }
        })
      } else {
        navigate(appPrivateRoutes.homePage)
      }
    } catch (error) {
      if (error instanceof Error) {
        toast.error(error.message)
      }
      console.error(error)
    } finally {
      setIsLoading(false)
    }
  }

  /**
   * Handles the user search textfield change
   * If it's not valid npub or nip05, search will be automatically triggered
   */
  const handleSearchAutocompleteTextfieldChange = async (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    const value = e.target.value

    const disarmAddOnEnter = () => {
      setPastedUserNpubOrNip05(undefined)
    }

    // Seems like it's npub format
    if (value.startsWith('npub')) {
      // We will try to convert npub to hex and if it's successfull that means
      // npub is valid
      const validHexPubkey = npubToHex(value)

      if (validHexPubkey) {
        // Arm the manual user npub add after enter is hit, we don't want to trigger search
        setPastedUserNpubOrNip05(value)
      } else {
        disarmAddOnEnter()
      }
    } else {
      // Disarm the add user on enter hit, and trigger search after 1 second
      disarmAddOnEnter()
    }

    setUserSearchInput(value)
  }

  const parseContent = (event: Event) => {
    try {
      return JSON.parse(event.content)
    } catch (e) {
      return undefined
    }
  }

  return (
    <>
      <Container className={styles.container}>
        <StickySideColumns
          left={
            <div className={styles.flexWrap}>
              <div className={styles.inputWrapper}>
                <TextField
                  fullWidth
                  placeholder="Title"
                  size="small"
                  type="text"
                  value={title}
                  onChange={(e) => setTitle(e.target.value)}
                />
              </div>
              <ol className={`${styles.paperGroup} ${styles.orderedFilesList}`}>
                {selectedFiles.length > 0 &&
                  selectedFiles.map((file, index) => (
                    <li
                      key={index}
                      className={`${fileListStyles.fileItem} ${isActive(file) && fileListStyles.active}`}
                      onClick={() => {
                        handleFileClick('file-' + file.name)
                        setCurrentFile(file)
                      }}
                    >
                      <span className={styles.fileName}>{file.name}</span>
                      <Button
                        aria-label={`delete ${file.name}`}
                        variant="text"
                        onClick={(event) => handleRemoveFile(event, file)}
                      >
                        <FontAwesomeIcon icon={faTrash} />
                      </Button>
                    </li>
                  ))}
              </ol>
              <Button variant="contained" onClick={handleUploadButtonClick}>
                <FontAwesomeIcon icon={faUpload} />
                <span className={styles.uploadFileText}>Upload new files</span>
              </Button>
              <input
                ref={fileInputRef}
                hidden={true}
                multiple={true}
                type="file"
                aria-label="file-upload"
                onChange={handleSelectFiles}
              />
            </div>
          }
          right={
            <div className={styles.flexWrap}>
              <div className={`${styles.paperGroup} ${styles.users}`}>
                <DisplayUser
                  users={users}
                  handleUserRoleChange={handleUserRoleChange}
                  handleRemoveUser={handleRemoveUser}
                  moveSigner={moveSigner}
                />
              </div>

              <div className={styles.addCounterpart}>
                <div className={styles.inputWrapper}>
                  <Autocomplete
                    sx={{ width: 300 }}
                    options={foundUsers}
                    onChange={handleSearchUserChange}
                    inputValue={userSearchInput}
                    disableClearable
                    openOnFocus
                    autoHighlight
                    freeSolo
                    filterOptions={(x) => x}
                    getOptionLabel={(option) => {
                      let label: string = (option as FoundUser).npub

                      const contentJson = parseContent(option as FoundUser)

                      if (contentJson?.name) {
                        label = contentJson.name
                      } else {
                        label = option as string
                      }

                      return label
                    }}
                    renderOption={(props, option) => {
                      const { ...optionProps } = props

                      const contentJson = parseContent(option)

                      return (
                        <Box
                          component="li"
                          sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
                          {...optionProps}
                          key={option.pubkey}
                        >
                          <AvatarIconButton
                            src={contentJson.picture}
                            hexKey={option.pubkey}
                            color="inherit"
                            sx={{
                              padding: '0 10px 0 0'
                            }}
                          />

                          <div>
                            {contentJson.name}{' '}
                            {usersPubkey === option.pubkey ? (
                              <span
                                style={{
                                  color: '#4c82a3',
                                  fontWeight: 'bold'
                                }}
                              >
                                Me
                              </span>
                            ) : (
                              ''
                            )}{' '}
                            ({truncate(option.npub, { length: 16 })})
                          </div>
                        </Box>
                      )
                    }}
                    renderInput={(params) => (
                      <TextField
                        {...params}
                        key={params.id}
                        inputRef={searchFieldRef}
                        label="Add/Search counterpart"
                        onKeyDown={handleInputKeyDown}
                        onChange={handleSearchAutocompleteTextfieldChange}
                      />
                    )}
                  />
                </div>
                {!pastedUserNpubOrNip05 ? (
                  <Button
                    disabled={!userSearchInput || searchUsersLoading}
                    onClick={() => handleSearchUsers()}
                    variant="contained"
                    aria-label="Add"
                    className={styles.counterpartToggleButton}
                  >
                    {searchUsersLoading ? (
                      <CircularProgress size={14} />
                    ) : (
                      <FontAwesomeIcon icon={faSearch} />
                    )}
                  </Button>
                ) : (
                  <Button
                    onClick={() => {
                      setUserInput(userSearchInput)
                    }}
                    variant="contained"
                    aria-label="Add"
                    className={styles.counterpartToggleButton}
                  >
                    <FontAwesomeIcon icon={faPlus} />
                  </Button>
                )}
              </div>

              <div className={`${styles.paperGroup} ${styles.toolbox}`}>
                {DEFAULT_TOOLBOX.filter((drawTool) => !drawTool.isHidden).map(
                  (drawTool: DrawTool, index: number) => {
                    return (
                      <div
                        key={index}
                        {...(!drawTool.isComingSoon && {
                          onClick: () => handleToolSelect(drawTool)
                        })}
                        className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''} ${drawTool.isComingSoon ? styles.comingSoon : ''}
                      `}
                      >
                        <FontAwesomeIcon
                          fontSize={'15px'}
                          icon={drawTool.icon}
                        />
                        {drawTool.label}
                        {!drawTool.isComingSoon ? (
                          <FontAwesomeIcon
                            fontSize={'15px'}
                            icon={faEllipsis}
                          />
                        ) : (
                          <span className={styles.comingSoonPlaceholder}>
                            Coming soon
                          </span>
                        )}
                      </div>
                    )
                  }
                )}
              </div>

              <Button onClick={handleCreate} variant="contained">
                Publish
              </Button>

              <ButtonUnderline onClick={handleCreateOffline}>
                <FontAwesomeIcon icon={faDownload} />
                Create and export locally
              </ButtonUnderline>

              {!!error && (
                <FormHelperText error={!!error}>{error}</FormHelperText>
              )}
            </div>
          }
          leftIcon={faFileCirclePlus}
          centerIcon={faFile}
          rightIcon={faToolbox}
        >
          <DrawPDFFields
            users={users}
            metadata={metadata}
            selectedTool={selectedTool}
            sigitFiles={drawnFiles}
            updateSigitFiles={updateDrawnFiles}
          />
          {parsingPdf && <LoadingSpinner variant="small" />}
        </StickySideColumns>
      </Container>
      {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
    </>
  )
}

type DisplayUsersProps = {
  users: User[]
  handleUserRoleChange: (role: UserRole, pubkey: string) => void
  handleRemoveUser: (pubkey: string) => void
  moveSigner: (dragIndex: number, hoverIndex: number) => void
}

const DisplayUser = ({
  users,
  handleUserRoleChange,
  handleRemoveUser,
  moveSigner
}: DisplayUsersProps) => {
  return (
    <>
      <DndProvider backend={MultiBackend} options={HTML5toTouch}>
        {users
          .filter((user) => user.role === UserRole.signer)
          .map((user, index) => (
            <SignerCounterpart
              key={`signer-${user.pubkey}`}
              user={user}
              index={index}
              moveSigner={moveSigner}
              handleUserRoleChange={handleUserRoleChange}
              handleRemoveUser={handleRemoveUser}
            />
          ))}
      </DndProvider>
      {users
        .filter((user) => user.role === UserRole.viewer)
        .map((user) => {
          return (
            <div className={styles.user} key={`viewer-${user.pubkey}`}>
              <Counterpart
                user={user}
                handleUserRoleChange={handleUserRoleChange}
                handleRemoveUser={handleRemoveUser}
              />
            </div>
          )
        })}
    </>
  )
}

interface DragItem {
  index: number
  id: string
  type: string
}

type CounterpartProps = {
  user: User
  handleUserRoleChange: (role: UserRole, pubkey: string) => void
  handleRemoveUser: (pubkey: string) => void
}

type SignerCounterpartProps = CounterpartProps & {
  index: number
  moveSigner: (dragIndex: number, hoverIndex: number) => void
}

const SignerCounterpart = ({
  user,
  index,
  moveSigner,
  handleUserRoleChange,
  handleRemoveUser
}: SignerCounterpartProps) => {
  const ref = useRef<HTMLTableRowElement>(null)

  const [{ handlerId }, drop] = useDrop<
    DragItem,
    void,
    { handlerId: Identifier | null }
  >({
    accept: 'row',
    collect(monitor) {
      return {
        handlerId: monitor.getHandlerId()
      }
    },
    hover(item: DragItem, monitor) {
      if (!ref.current) {
        return
      }
      const dragIndex = item.index
      const hoverIndex = index

      // Don't replace items with themselves
      if (dragIndex === hoverIndex) {
        return
      }

      // Determine rectangle on screen
      const hoverBoundingRect = ref.current?.getBoundingClientRect()

      // Get vertical middle
      const hoverMiddleY =
        (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2

      // Determine mouse position
      const clientOffset = monitor.getClientOffset()

      // Get pixels to the top
      const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top

      // Only perform the move when the mouse has crossed half of the items height
      // When dragging downwards, only move when the cursor is below 50%
      // When dragging upwards, only move when the cursor is above 50%

      // Dragging downwards
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return
      }

      // Dragging upwards
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return
      }

      // Time to actually perform the action
      moveSigner(dragIndex, hoverIndex)

      // Note: we're mutating the monitor item here!
      // Generally it's better to avoid mutations,
      // but it's good here for the sake of performance
      // to avoid expensive index searches.
      item.index = hoverIndex
    }
  })

  const [{ isDragging }, drag] = useDrag({
    type: 'row',
    item: () => {
      return { id: user.pubkey, index }
    },
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    })
  })

  const opacity = isDragging ? 0.2 : 1
  drag(drop(ref))

  return (
    <div
      className={styles.user}
      style={{ cursor: 'move', opacity }}
      data-handler-id={handlerId}
      ref={ref}
    >
      <FontAwesomeIcon width={'14px'} fontSize={'14px'} icon={faGripLines} />
      <Counterpart
        user={user}
        handleRemoveUser={handleRemoveUser}
        handleUserRoleChange={handleUserRoleChange}
      />
    </div>
  )
}

const Counterpart = ({
  user,
  handleUserRoleChange,
  handleRemoveUser
}: CounterpartProps) => {
  return (
    <>
      <div className={styles.avatar}>
        <UserAvatar pubkey={user.pubkey} isNameVisible={true} />
      </div>
      <Tooltip title="Toggle User Role" arrow disableInteractive>
        <Button
          onClick={() =>
            handleUserRoleChange(
              user.role === UserRole.signer ? UserRole.viewer : UserRole.signer,
              user.pubkey
            )
          }
          className={styles.counterpartRowToggleButton}
          data-variant="primary"
        >
          <FontAwesomeIcon
            icon={user.role === UserRole.signer ? faPen : faEye}
          />
        </Button>
      </Tooltip>
      <Tooltip title="Remove User" arrow disableInteractive>
        <Button
          onClick={() => handleRemoveUser(user.pubkey)}
          className={styles.counterpartRowToggleButton}
          data-variant="secondary"
        >
          <FontAwesomeIcon icon={faTrash} />
        </Button>
      </Tooltip>
    </>
  )
}