import {
  Box,
  Button,
  List,
  ListItem,
  ListSubheader,
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableRow,
  TextField,
  Typography,
  useTheme
} from '@mui/material'
import axios from 'axios'
import saveAs from 'file-saver'
import JSZip from 'jszip'
import _ from 'lodash'
import { MuiFileInput } from 'mui-file-input'
import { EventTemplate } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { Link, useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify'
import placeholderAvatar from '../../assets/images/nostr-logo.jpg'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { MetadataController, NostrController } from '../../controllers'
import { getProfileRoute } from '../../routes'
import { State } from '../../store/rootReducer'
import { Meta, ProfileMetadata, User, UserRole } from '../../types'
import {
  decryptArrayBuffer,
  encryptArrayBuffer,
  generateEncryptionKey,
  getHash,
  hexToNpub,
  parseJson,
  readContentOfZipEntry,
  sendDM,
  shorten,
  signEventForMetaFile,
  uploadToFileStorage
} from '../../utils'
import styles from './style.module.scss'

enum SignedStatus {
  Fully_Signed,
  User_Is_Next_Signer,
  User_Is_Not_Next_Signer
}

export const VerifyPage = () => {
  const [searchParams] = useSearchParams()

  const [displayInput, setDisplayInput] = useState(false)

  const [selectedFile, setSelectedFile] = useState<File | null>(null)
  const [encryptionKey, setEncryptionKey] = useState('')

  const [zip, setZip] = useState<JSZip>()

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

  const [meta, setMeta] = useState<Meta | null>(null)
  const [signedStatus, setSignedStatus] = useState<SignedStatus>()

  const [nextSinger, setNextSinger] = useState<string>()

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

  const [authUrl, setAuthUrl] = useState<string>()
  const nostrController = NostrController.getInstance()

  useEffect(() => {
    if (meta) {
      setDisplayInput(false)

      // get list of users who have signed
      const signedBy = Object.keys(meta.signedEvents)

      if (meta.signers.length > 0) {
        // check if all signers have signed then its fully signed
        if (meta.signers.every((signer) => signedBy.includes(signer))) {
          setSignedStatus(SignedStatus.Fully_Signed)
        } else {
          for (const signer of meta.signers) {
            if (!signedBy.includes(signer)) {
              setNextSinger(signer)

              if (signer === usersPubkey) {
                // logged in user is the next signer
                setSignedStatus(SignedStatus.User_Is_Next_Signer)
              } else {
                setSignedStatus(SignedStatus.User_Is_Not_Next_Signer)
              }

              break
            }
          }
        }
      } else {
        // there's no signer just viewers. So its fully signed
        setSignedStatus(SignedStatus.Fully_Signed)
      }
    }
  }, [meta, usersPubkey])

  useEffect(() => {
    const fileUrl = searchParams.get('file')
    const key = searchParams.get('key')

    if (fileUrl && key) {
      setIsLoading(true)
      setLoadingSpinnerDesc('Fetching file from file server')

      axios
        .get(fileUrl, {
          responseType: 'arraybuffer'
        })
        .then((res) => {
          const fileName = fileUrl.split('/').pop()
          const file = new File([res.data], fileName!)

          decrypt(file, key).then((arrayBuffer) => {
            if (arrayBuffer) handleDecryptedArrayBuffer(arrayBuffer)
          })
        })
        .catch((err) => {
          console.error(`error occurred in getting file from ${fileUrl}`, err)
          toast.error(
            err.message || `error occurred in getting file from ${fileUrl}`
          )
        })
        .finally(() => {
          setIsLoading(false)
        })
    } else {
      setIsLoading(false)
      setDisplayInput(true)
    }
  }, [searchParams])

  const decrypt = async (file: File, key: string) => {
    setLoadingSpinnerDesc('Decrypting file')

    const encryptedArrayBuffer = await file.arrayBuffer()

    const arrayBuffer = await decryptArrayBuffer(encryptedArrayBuffer, key)
      .catch((err) => {
        console.log('err in decryption:>> ', err)
        toast.error(err.message || 'An error occurred in decrypting file.')
        return null
      })
      .finally(() => {
        setIsLoading(false)
      })

    return arrayBuffer
  }

  const handleDecryptedArrayBuffer = async (arrayBuffer: ArrayBuffer) => {
    const decryptedZipFile = new File([arrayBuffer], 'decrypted.zip')

    setLoadingSpinnerDesc('Parsing zip file')

    const zip = await JSZip.loadAsync(decryptedZipFile).catch((err) => {
      console.log('err in loading zip file :>> ', err)
      toast.error(err.message || 'An error occurred in loading zip file.')
      return null
    })

    if (!zip) return

    setZip(zip)

    setLoadingSpinnerDesc('Parsing meta.json')

    const metaFileContent = await readContentOfZipEntry(
      zip,
      'meta.json',
      'string'
    )

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

    const parsedMetaJson = await parseJson<Meta>(metaFileContent).catch(
      (err) => {
        console.log('err in parsing the content of meta.json :>> ', err)
        toast.error(
          err.message || 'error occurred in parsing the content of meta.json'
        )
        setIsLoading(false)
        return null
      }
    )

    setMeta(parsedMetaJson)
  }

  const handleDecrypt = async () => {
    if (!selectedFile || !encryptionKey) return

    setIsLoading(true)
    const arrayBuffer = await decrypt(selectedFile, encryptionKey)

    if (!arrayBuffer) return

    handleDecryptedArrayBuffer(arrayBuffer)
  }

  const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault()
    setIsDraggingOver(false)
    const file = event.dataTransfer.files[0]
    if (file.type === 'application/zip') setSelectedFile(file)
  }

  const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
    event.preventDefault()
    setIsDraggingOver(true)
  }

  const handleSign = async () => {
    if (!zip || !meta) return

    setIsLoading(true)
    setLoadingSpinnerDesc('parsing hashes.json file')

    const hashesFileContent = await readContentOfZipEntry(
      zip,
      'hashes.json',
      'string'
    )

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

    let hashes = await parseJson(hashesFileContent).catch((err) => {
      console.log('err in parsing the content of hashes.json :>> ', err)
      toast.error(
        err.message || 'error occurred in parsing the content of hashes.json'
      )
      setIsLoading(false)
      return null
    })

    if (!hashes) return

    setLoadingSpinnerDesc('Generating hashes for files')

    const fileHashes: { [key: string]: string } = {}
    const fileNames = Object.keys(meta.fileHashes)

    // generate hashes for all entries in files folder of zipArchive
    // these hashes can be used to verify the originality of files
    for (const fileName of fileNames) {
      const filePath = `files/${fileName}`
      const arrayBuffer = await readContentOfZipEntry(
        zip,
        filePath,
        'arraybuffer'
      )

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

      const hash = await getHash(arrayBuffer)
      if (!hash) {
        setIsLoading(false)
        return
      }

      fileHashes[fileName] = hash
    }

    setLoadingSpinnerDesc('Signing nostr event')
    const signedEvent = await signEventForMetaFile(
      fileHashes,
      nostrController,
      setIsLoading
    )

    if (!signedEvent) return

    const metaCopy = _.cloneDeep(meta)

    metaCopy.signedEvents = {
      ...metaCopy.signedEvents,
      [signedEvent.pubkey]: JSON.stringify(signedEvent, null, 2)
    }

    const stringifiedMeta = JSON.stringify(metaCopy, null, 2)
    zip.file('meta.json', stringifiedMeta)

    const metaHash = await getHash(stringifiedMeta)
    if (!metaHash) return

    hashes = {
      ...hashes,
      [usersPubkey!]: metaHash
    }

    zip.file('hashes.json', JSON.stringify(hashes, null, 2))

    const arrayBuffer = await zip
      .generateAsync({
        type: 'arraybuffer',
        compression: 'DEFLATE',
        compressionOptions: {
          level: 6
        }
      })
      .catch((err) => {
        console.log('err in zip:>> ', err)
        setIsLoading(false)
        toast.error(err.message || 'Error occurred in generating zip file')
        return null
      })

    if (!arrayBuffer) return

    const encryptionKey = await generateEncryptionKey()

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

    const blob = new Blob([encryptedArrayBuffer])

    setLoadingSpinnerDesc('Uploading zip file to file storage.')
    const fileUrl = await uploadToFileStorage(blob, nostrController)
      .then((url) => {
        toast.success('zip file uploaded to file storage')
        return url
      })
      .catch((err) => {
        console.log('err in upload:>> ', err)
        setIsLoading(false)
        toast.error(err.message || 'Error occurred in uploading zip file')
        return null
      })

    if (!fileUrl) return

    // check if the current user is the last signer
    const lastSignerIndex = meta.signers.length - 1
    const signerIndex = meta.signers.indexOf(usersPubkey!)
    const isLastSigner = signerIndex === lastSignerIndex

    // if current user is the last signer, then send DMs to all signers and viewers
    if (isLastSigner) {
      const userSet = new Set<string>()

      userSet.add(meta.submittedBy)

      meta.signers.forEach((signer) => {
        userSet.add(signer)
      })

      meta.viewers.forEach((viewer) => {
        userSet.add(viewer)
      })

      const users = Array.from(userSet)

      for (const user of users) {
        // todo: execute in parallel
        await sendDM(
          fileUrl,
          encryptionKey,
          user,
          nostrController,
          false,
          setAuthUrl
        )
      }
    } else {
      const nextSigner = meta.signers[signerIndex + 1]
      await sendDM(
        fileUrl,
        encryptionKey,
        nextSigner,
        nostrController,
        false,
        setAuthUrl
      )
    }

    setIsLoading(false)
  }

  const handleExport = async () => {
    if (!meta || !zip || !usersPubkey) return

    if (
      !meta.signers.includes(usersPubkey) &&
      !meta.viewers.includes(usersPubkey) &&
      meta.submittedBy !== usersPubkey
    )
      return

    setIsLoading(true)
    setLoadingSpinnerDesc('Signing nostr event')
    const event: EventTemplate = {
      kind: 1,
      content: '',
      created_at: Math.floor(Date.now() / 1000), // Current timestamp
      tags: []
    }

    // Sign the event
    const signedEvent = 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
    })

    if (!signedEvent) return

    const exportSignature = JSON.stringify(signedEvent, null, 2)

    const stringifiedMeta = JSON.stringify(
      {
        ...meta,
        exportSignature
      },
      null,
      2
    )
    zip.file('meta.json', stringifiedMeta)

    const arrayBuffer = await zip
      .generateAsync({
        type: 'arraybuffer',
        compression: 'DEFLATE',
        compressionOptions: {
          level: 6
        }
      })
      .catch((err) => {
        console.log('err in zip:>> ', err)
        setIsLoading(false)
        toast.error(err.message || 'Error occurred in generating zip file')
        return null
      })

    if (!arrayBuffer) return

    const blob = new Blob([arrayBuffer])
    saveAs(blob, 'exported.zip')

    setIsLoading(false)
  }

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

  return (
    <>
      {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
      <Box className={styles.container}>
        {displayInput && (
          <>
            <Typography component='label' variant='h6'>
              Select sigit file
            </Typography>

            <Box
              className={styles.inputBlock}
              onDrop={handleDrop}
              onDragOver={handleDragOver}
            >
              {isDraggingOver && (
                <Box className={styles.fileDragOver}>
                  <Typography variant='body1'>Drop file here</Typography>
                </Box>
              )}
              <MuiFileInput
                placeholder='Drop file here, or click to select'
                value={selectedFile}
                onChange={(value) => setSelectedFile(value)}
                InputProps={{
                  inputProps: {
                    accept: '.zip'
                  }
                }}
              />

              {selectedFile && (
                <TextField
                  label='Encryption Key'
                  variant='outlined'
                  value={encryptionKey}
                  onChange={(e) => setEncryptionKey(e.target.value)}
                />
              )}
            </Box>

            {selectedFile && encryptionKey && (
              <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
                <Button onClick={handleDecrypt} variant='contained'>
                  Decrypt
                </Button>
              </Box>
            )}
          </>
        )}

        {meta && signedStatus === SignedStatus.Fully_Signed && (
          <>
            <DisplayMeta meta={meta} nextSigner={nextSinger} />
            <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
              <Button onClick={handleExport} variant='contained'>
                Export
              </Button>
            </Box>
          </>
        )}

        {meta && signedStatus === SignedStatus.User_Is_Not_Next_Signer && (
          <>
            <DisplayMeta meta={meta} nextSigner={nextSinger} />
            <Typography component='h6'>
              Awaiting signature by {hexToNpub(nextSinger)}
            </Typography>
          </>
        )}

        {meta && signedStatus === SignedStatus.User_Is_Next_Signer && (
          <>
            <DisplayMeta meta={meta} nextSigner={nextSinger} />
            <Box sx={{ mt: 1, display: 'flex', justifyContent: 'center' }}>
              <Button onClick={handleSign} variant='contained'>
                Sign
              </Button>
            </Box>
          </>
        )}
      </Box>
    </>
  )
}

type DisplayMetaProps = {
  meta: Meta
  nextSigner?: string
}

const DisplayMeta = ({ meta, nextSigner }: DisplayMetaProps) => {
  const theme = useTheme()

  const textColor = theme.palette.getContrastText(
    theme.palette.background.paper
  )

  const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>(
    {}
  )
  const [users, setUsers] = useState<User[]>([])

  useEffect(() => {
    meta.signers.forEach((signer) => {
      setUsers((prev) => {
        if (prev.findIndex((user) => user.pubkey === signer) !== -1) return prev

        return [
          ...prev,
          {
            pubkey: signer,
            role: UserRole.signer
          }
        ]
      })
    })

    meta.viewers.forEach((viewer) => {
      setUsers((prev) => {
        if (prev.findIndex((user) => user.pubkey === viewer) !== -1) return prev

        return [
          ...prev,
          {
            pubkey: viewer,
            role: UserRole.viewer
          }
        ]
      })
    })
  }, [meta])

  useEffect(() => {
    const metadataController = new MetadataController()

    metadataController
      .findMetadata(meta.submittedBy)
      .then((metadataEvent) => {
        const metadataContent =
          metadataController.extractProfileMetadataContent(metadataEvent)
        if (metadataContent)
          setMetadata((prev) => ({
            ...prev,
            [meta.submittedBy]: metadataContent
          }))
      })
      .catch((err) => {
        console.error(
          `error occurred in finding metadata for: ${meta.submittedBy}`,
          err
        )
      })

    users.forEach((user) => {
      if (!(user.pubkey in metadata)) {
        metadataController
          .findMetadata(user.pubkey)
          .then((metadataEvent) => {
            const metadataContent =
              metadataController.extractProfileMetadataContent(metadataEvent)
            if (metadataContent)
              setMetadata((prev) => ({
                ...prev,
                [user.pubkey]: metadataContent
              }))
          })
          .catch((err) => {
            console.error(
              `error occurred in finding metadata for: ${user.pubkey}`,
              err
            )
          })
      }
    })
  }, [users, meta.submittedBy])

  const imageLoadError = (event: any) => {
    event.target.src = placeholderAvatar
  }

  const getRoboImageUrl = (pubkey: string) => {
    const npub = hexToNpub(pubkey)
    return `https://robohash.org/${npub}.png?set=set3`
  }

  return (
    <List
      sx={{
        bgcolor: 'background.paper',
        marginTop: 2
      }}
      subheader={
        <ListSubheader
          sx={{
            borderBottom: '0.5px solid',
            paddingBottom: 1,
            paddingTop: 1,
            fontSize: '1.5rem'
          }}
          className={styles.subHeader}
        >
          Meta Info
        </ListSubheader>
      }
    >
      <ListItem
        sx={{
          marginTop: 1,
          gap: '15px'
        }}
      >
        <Typography variant='h6' sx={{ color: textColor }}>
          Submitted By
        </Typography>
        <Box className={styles.user}>
          <img
            onError={imageLoadError}
            src={
              metadata[meta.submittedBy]?.picture ||
              getRoboImageUrl(meta.submittedBy)
            }
            alt='Profile Image'
            className='profile-image'
            style={{
              borderWidth: '3px',
              borderStyle: 'solid',
              borderColor: `#${meta.submittedBy.substring(0, 6)}`
            }}
          />
          <Link to={getProfileRoute(meta.submittedBy)}>
            <Typography component='label' className={styles.name}>
              {metadata[meta.submittedBy]?.display_name ||
                metadata[meta.submittedBy]?.name ||
                shorten(hexToNpub(meta.submittedBy))}
            </Typography>
          </Link>
        </Box>
      </ListItem>
      <ListItem
        sx={{
          marginTop: 1,
          flexDirection: 'column',
          alignItems: 'flex-start'
        }}
      >
        <Typography variant='h6' sx={{ color: textColor }}>
          Files
        </Typography>
        <ul>
          {Object.keys(meta.fileHashes).map((file, index) => (
            <li key={index} style={{ color: textColor }}>
              {file}
            </li>
          ))}
        </ul>
      </ListItem>
      <ListItem sx={{ marginTop: 1 }}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell className={styles.tableCell}>User</TableCell>
              <TableCell className={styles.tableCell}>Role</TableCell>
              <TableCell>Signed Status</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {users.map((user, index) => {
              const userMeta = metadata[user.pubkey]
              const npub = hexToNpub(user.pubkey)
              const roboUrl = `https://robohash.org/${npub}.png?set=set3`

              let signedStatus = '-'

              if (user.role === UserRole.viewer) {
                signedStatus = '-'
              } else {
                // check if user has signed the document
                if (user.pubkey in meta.signedEvents) {
                  signedStatus = 'Signed'
                }
                // check if user is the next signer
                else if (user.pubkey === nextSigner) {
                  signedStatus === 'Next Singer'
                }
                // if none of the above condition is true then it means user's signature is pending
                else {
                  signedStatus === 'Pending'
                }
              }

              return (
                <TableRow key={index}>
                  <TableCell className={styles.tableCell}>
                    <Box className={styles.user}>
                      <img
                        onError={imageLoadError}
                        src={userMeta?.picture || roboUrl}
                        alt='Profile Image'
                        className='profile-image'
                        style={{
                          borderWidth: '3px',
                          borderStyle: 'solid',
                          borderColor: `#${user.pubkey.substring(0, 6)}`
                        }}
                      />
                      <Link to={getProfileRoute(user.pubkey)}>
                        <Typography component='label' className={styles.name}>
                          {userMeta?.display_name ||
                            userMeta?.name ||
                            shorten(npub)}
                        </Typography>
                      </Link>
                    </Box>
                  </TableCell>
                  <TableCell className={styles.tableCell}>
                    {user.role}
                  </TableCell>
                  <TableCell>{signedStatus}</TableCell>
                </TableRow>
              )
            })}
          </TableBody>
        </Table>
      </ListItem>
    </List>
  )
}
