import { Clear, DragHandle } from '@mui/icons-material'
import {
  Box,
  Button,
  FormControl,
  IconButton,
  InputLabel,
  MenuItem,
  Paper,
  Select,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  TextField,
  Tooltip,
  Typography
} from '@mui/material'
import type { Identifier, XYCoord } from 'dnd-core'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, kinds } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { DndProvider, useDrag, useDrop } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import { useSelector } from 'react-redux'
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 } from '../../controllers'
import { appPrivateRoutes } from '../../routes'
import { State } from '../../store/rootReducer'
import {
  CreateSignatureEventContent,
  Meta,
  ProfileMetadata,
  User,
  UserRole
} from '../../types'
import {
  encryptArrayBuffer,
  formatTimestamp,
  generateEncryptionKey,
  generateKeys,
  generateKeysFile,
  getHash,
  hexToNpub,
  isOnline,
  now,
  npubToHex,
  queryNip05,
  sendNotification,
  shorten,
  signEventForMetaFile,
  updateUsersAppData,
  uploadToFileStorage
} from '../../utils'
import styles from './style.module.scss'
import { PdfFile } from '../../types/drawing'
import { DrawPDFFields } from '../../components/DrawPDFFields'
import { Mark } from '../../types/mark.ts'
import { Container } from '../../components/Container'

export const CreatePage = () => {
  const navigate = useNavigate()
  const location = useLocation()
  const { uploadedFile } = location.state || {}

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

  const [authUrl, setAuthUrl] = useState<string>()

  const [title, setTitle] = useState(`sigit_${formatTimestamp(Date.now())}`)
  const [selectedFiles, setSelectedFiles] = useState<File[]>([])

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

  const [users, setUsers] = useState<User[]>([])

  const usersPubkey = useSelector((state: State) => state.auth.usersPubkey)

  const nostrController = NostrController.getInstance()

  const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
    {}
  )
  const [drawnPdfs, setDrawnPdfs] = useState<PdfFile[]>([])

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

        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
            )
          })
      }
    })
  }, [])
  // Set up event listener for authentication event
  nostrController.on('nsecbunker-auth', (url) => {
    setAuthUrl(url)
  })

  useEffect(() => {
    if (uploadedFile) {
      setSelectedFiles([uploadedFile])
    }
  }, [uploadedFile])

  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 = 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()

    if (input.startsWith('npub')) {
      const pubkey = npubToHex(input)
      if (pubkey) {
        addUser(pubkey)
        setUserInput('')
      } else {
        setError('Provided npub is not valid. Please enter correct npub.')
      }
      return
    }

    if (input.includes('@')) {
      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
    }

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

  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))
  }

  /**
   * 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 = (files: File[]) => {
    setSelectedFiles((prev) => {
      const prevFileNames = prev.map((file) => file.name)

      const newFiles = files.filter(
        (file) => !prevFileNames.includes(file.name)
      )

      return [...prev, ...newFiles]
    })
  }

  const handleRemoveFile = (fileToRemove: File) => {
    setSelectedFiles((prevFiles) =>
      prevFiles.filter((file) => file.name !== fileToRemove.name)
    )
  }

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

    if (users.length === 0) {
      toast.error(
        'No signer/viewer is provided. At least add one signer or viewer.'
      )
      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: any) => {
    console.log(
      `Error while getting arrayBuffer of file ${file.name} :>> `,
      err
    )
    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 drawnPdfs.flatMap((drawnPdf) => {
      const fileHash = fileHashes[drawnPdf.file.name];
      return drawnPdf.pages.flatMap((page, index) => {
        return page.drawnFields.map((drawnField) => {
          return {
            type: drawnField.type,
            location: {
              page: index,
              top: drawnField.top,
              left: drawnField.left,
              height: drawnField.height,
              width: drawnField.width,
            },
            npub: drawnField.counterpart,
            pdfFileHash: fileHash
          }
        })
      })
    })
      .map((mark, index) => {
        return {...mark, id: index }
      });
  }

  // Handle errors during zip file generation
  const handleZipError = (err: any) => {
    console.log('Error in zip:>> ', err)
    setIsLoading(false)
    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 unixNow = now()
    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: any) => {
    console.log('Error in upload:>> ', err)
    setIsLoading(false)
    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 unixNow = now()
    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)
  }

  // Manage offline scenarios for signing or viewing the file
  const handleOfflineFlow = async (
    encryptedArrayBuffer: ArrayBuffer,
    encryptionKey: string
  ) => {
    const finalZipFile = await createFinalZipFile(
      encryptedArrayBuffer,
      encryptionKey
    )

    if (!finalZipFile) return

    saveAs(finalZipFile, 'request.sigit.zip')
  }

  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 (
    fileHashes: {
      [key: string]: string
    },
    zipUrl: string
  ) => {
    const signers = users.filter((user) => user.role === UserRole.signer)
    const viewers = users.filter((user) => user.role === UserRole.viewer)
    const markConfig = createMarks(fileHashes)

    const content: CreateSignatureEventContent = {
      signers: signers.map((signer) => hexToNpub(signer.pubkey)),
      viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)),
      fileHashes,
      markConfig,
      zipUrl,
      title
    }

    setLoadingSpinnerDesc('Signing nostr event for create signature')

    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 = (meta: Meta) => {
    const signers = users.filter((user) => user.role === UserRole.signer)
    const viewers = users.filter((user) => user.role === UserRole.viewer)

    // 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, meta)
    )
  }

  const handleCreate = async () => {
    if (!validateInputs()) return

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

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

    if (await isOnline()) {
      setLoadingSpinnerDesc('generating files.zip')
      const arrayBuffer = await generateFilesZip()
      if (!arrayBuffer) {
        setIsLoading(false)
        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) {
        setIsLoading(false)
        return
      }

      setLoadingSpinnerDesc('Generating create signature')
      const createSignature = await generateCreateSignature(fileHashes, fileUrl)
      if (!createSignature) {
        setIsLoading(false)
        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) {
        setIsLoading(false)
        return
      }
      const meta: Meta = {
        createSignature,
        keys,
        modifiedAt: now(),
        docSignatures: {}
      }

      setLoadingSpinnerDesc('Updating user app data')
      const event = await updateUsersAppData(meta)
      if (!event) {
        setIsLoading(false)
        return
      }

      setLoadingSpinnerDesc('Sending notifications to counterparties')
      const promises = sendNotifications(meta)

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

      navigate(appPrivateRoutes.sign, { state: { meta: meta } })
    } else {
      const zip = new JSZip()

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

      setLoadingSpinnerDesc('Generating create signature')
      const createSignature = await generateCreateSignature(fileHashes, '')
      if (!createSignature) {
        setIsLoading(false)
        return
      }

      const meta: Meta = {
        createSignature,
        modifiedAt: now(),
        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
      )

      await handleOfflineFlow(encryptedArrayBuffer, encryptionKey)
    }
  }

  const onDrawFieldsChange = (pdfFiles: PdfFile[]) => {
    setDrawnPdfs(pdfFiles)
  }

  if (authUrl) {
    return (
      <iframe
        title="Nsecbunker auth"
        src={authUrl}
        width="100%"
        height="500px"
      />
    )
  }

  return (
    <>
      {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
      <Container className={styles.container}>
        <TextField
          label="Title"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          variant="outlined"
        />

        <Box>
          <MuiFileInput
            fullWidth
            multiple
            placeholder="Choose Files"
            value={selectedFiles}
            onChange={(value) => handleSelectFiles(value)}
          />

          {selectedFiles.length > 0 && (
            <ul>
              {selectedFiles.map((file, index) => (
                <li key={index}>
                  <Typography component="label">{file.name}</Typography>
                  <IconButton onClick={() => handleRemoveFile(file)}>
                    <Clear style={{ color: 'red' }} />{' '}
                  </IconButton>
                </li>
              ))}
            </ul>
          )}
        </Box>

        <Typography component="label" variant="h6">
          Add Counterparts
        </Typography>
        <Box className={styles.inputBlock}>
          <Box className={styles.inputBlock}>
            <TextField
              label="nip05 / npub"
              value={userInput}
              onChange={(e) => setUserInput(e.target.value)}
              helperText={error}
              error={!!error}
            />
            <FormControl fullWidth>
              <InputLabel id="select-role-label">Role</InputLabel>
              <Select
                labelId="select-role-label"
                id="demo-simple-select"
                value={userRole}
                label="Role"
                onChange={(e) => setUserRole(e.target.value as UserRole)}
              >
                <MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
                <MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
              </Select>
            </FormControl>

            <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
              <Button
                disabled={!userInput}
                onClick={handleAddUser}
                variant="contained"
              >
                Add
              </Button>
            </Box>
          </Box>
        </Box>

        <DisplayUser
          metadata={metadata}
          users={users}
          handleUserRoleChange={handleUserRoleChange}
          handleRemoveUser={handleRemoveUser}
          moveSigner={moveSigner}
        />

        <DrawPDFFields
          metadata={metadata}
          users={users}
          selectedFiles={selectedFiles}
          onDrawFieldsChange={onDrawFieldsChange}
        />

        <Box sx={{ mt: 1, mb: 5, display: 'flex', justifyContent: 'center' }}>
          <Button onClick={handleCreate} variant="contained">
            Create
          </Button>
        </Box>
      </Container>
    </>
  )
}

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

const DisplayUser = ({
  metadata,
  users,
  handleUserRoleChange,
  handleRemoveUser,
  moveSigner
}: DisplayUsersProps) => {
  return (
    <TableContainer component={Paper} elevation={3} sx={{ marginTop: '20px' }}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell className={styles.tableHeaderCell}>User</TableCell>
            <TableCell className={styles.tableHeaderCell}>Role</TableCell>
            <TableCell>Action</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          <DndProvider backend={HTML5Backend}>
            {users
              .filter((user) => user.role === UserRole.signer)
              .map((user, index) => (
                <SignerRow
                  key={`signer-${index}`}
                  userMeta={metadata[user.pubkey]}
                  user={user}
                  index={index}
                  moveSigner={moveSigner}
                  handleUserRoleChange={handleUserRoleChange}
                  handleRemoveUser={handleRemoveUser}
                />
              ))}
          </DndProvider>
          {users
            .filter((user) => user.role === UserRole.viewer)
            .map((user, index) => {
              const userMeta = metadata[user.pubkey]
              return (
                <TableRow key={index}>
                  <TableCell className={styles.tableCell}>
                    <UserAvatar
                      pubkey={user.pubkey}
                      name={
                        userMeta?.display_name ||
                        userMeta?.name ||
                        shorten(hexToNpub(user.pubkey))
                      }
                      image={userMeta?.picture}
                    />
                  </TableCell>
                  <TableCell className={styles.tableCell}>
                    <Select
                      fullWidth
                      value={user.role}
                      onChange={(e) =>
                        handleUserRoleChange(
                          e.target.value as UserRole,
                          user.pubkey
                        )
                      }
                    >
                      <MenuItem value={UserRole.signer}>
                        {UserRole.signer}
                      </MenuItem>
                      <MenuItem value={UserRole.viewer}>
                        {UserRole.viewer}
                      </MenuItem>
                    </Select>
                  </TableCell>
                  <TableCell>
                    <Tooltip title="Remove User" arrow>
                      <IconButton onClick={() => handleRemoveUser(user.pubkey)}>
                        <Clear style={{ color: 'red' }} />
                      </IconButton>
                    </Tooltip>
                  </TableCell>
                </TableRow>
              )
            })}
        </TableBody>
      </Table>
    </TableContainer>
  )
}

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

type SignerRowProps = {
  userMeta: ProfileMetadata
  user: User
  index: number
  moveSigner: (dragIndex: number, hoverIndex: number) => void
  handleUserRoleChange: (role: UserRole, pubkey: string) => void
  handleRemoveUser: (pubkey: string) => void
}

const SignerRow = ({
  userMeta,
  user,
  index,
  moveSigner,
  handleUserRoleChange,
  handleRemoveUser
}: SignerRowProps) => {
  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: any) => ({
      isDragging: monitor.isDragging()
    })
  })

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

  return (
    <TableRow
      sx={{ cursor: 'move', opacity }}
      data-handler-id={handlerId}
      ref={ref}
    >
      <TableCell
        className={styles.tableCell}
        sx={{ display: 'flex', alignItems: 'center', gap: '10px' }}
      >
        <DragHandle />
        <UserAvatar
          pubkey={user.pubkey}
          name={
            userMeta?.display_name ||
            userMeta?.name ||
            shorten(hexToNpub(user.pubkey))
          }
          image={userMeta?.picture}
        />
      </TableCell>
      <TableCell className={styles.tableCell}>
        <Select
          fullWidth
          value={user.role}
          onChange={(e) =>
            handleUserRoleChange(e.target.value as UserRole, user.pubkey)
          }
        >
          <MenuItem value={UserRole.signer}>{UserRole.signer}</MenuItem>
          <MenuItem value={UserRole.viewer}>{UserRole.viewer}</MenuItem>
        </Select>
      </TableCell>
      <TableCell>
        <Tooltip title="Remove User" arrow>
          <IconButton onClick={() => handleRemoveUser(user.pubkey)}>
            <Clear style={{ color: 'red' }} />
          </IconButton>
        </Tooltip>
      </TableCell>
    </TableRow>
  )
}
