import { Box, Button, Typography } from '@mui/material'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
import {
  DocSignatureEvent,
  Meta,
  SignedEvent,
  OpenTimestamp,
  OpenTimestampUpgradeVerifyResponse
} from '../../types'
import {
  decryptArrayBuffer,
  getHash,
  hexToNpub,
  unixNow,
  parseJson,
  readContentOfZipEntry,
  signEventForMetaFile,
  getCurrentUserFiles,
  npubToHex,
  generateEncryptionKey,
  encryptArrayBuffer,
  generateKeysFile,
  ARRAY_BUFFER,
  DEFLATE,
  uploadMetaToFileStorage
} from '../../utils'
import styles from './style.module.scss'
import { useLocation, useParams } from 'react-router-dom'
import axios from 'axios'
import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
import { useAppSelector, useNDK } from '../../hooks'
import { getLastSignersSig } from '../../utils/sign.ts'
import { saveAs } from 'file-saver'
import { Container } from '../../components/Container'
import { useSigitMeta } from '../../hooks/useSigitMeta.tsx'
import { StickySideColumns } from '../../layouts/StickySideColumns.tsx'
import { UsersDetails } from '../../components/UsersDetails.tsx'
import FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts'
import React from 'react'
import {
  convertToSigitFile,
  getZipWithFiles,
  SigitFile
} from '../../utils/file.ts'
import { FileDivider } from '../../components/FileDivider.tsx'
import { ExtensionFileBox } from '../../components/ExtensionFileBox.tsx'
import { useScale } from '../../hooks/useScale.tsx'
import {
  faCircleInfo,
  faFile,
  faFileDownload
} from '@fortawesome/free-solid-svg-icons'
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
import _ from 'lodash'
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'

interface PdfViewProps {
  files: CurrentUserFile[]
  /**
   * Currently, loading spinner is present if `files` array is of length 0,
   * Which means if no files are found, loading spinner will be spinning indefinitely
   * For that reason `noFiles` is introduced to set the loading off when fetching is finished.
   */
  noFiles?: boolean
  currentFile: CurrentUserFile | null
  parsedSignatureEvents: {
    [signer: `npub1${string}`]: DocSignatureEvent
  }
}

const SlimPdfView = ({
  files,
  noFiles,
  currentFile,
  parsedSignatureEvents
}: PdfViewProps) => {
  const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
  const { from } = useScale()
  useEffect(() => {
    if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
      pdfRefs.current[currentFile.id]?.scrollIntoView({
        behavior: 'smooth'
      })
    }
  }, [currentFile])
  return (
    <div className="files-wrapper">
      {files.length > 0 ? (
        files.map((currentUserFile, i) => {
          const { hash, file, id } = currentUserFile
          const signatureEvents = Object.keys(parsedSignatureEvents)
          if (!hash) return
          return (
            <React.Fragment key={file.name}>
              <div
                id={file.name}
                ref={(el) => (pdfRefs.current[id] = el)}
                className="file-wrapper"
              >
                {file.isPdf &&
                  file.pages?.map((page, i) => {
                    const marks: Mark[] = []

                    signatureEvents.forEach((e) => {
                      const m = parsedSignatureEvents[
                        e as `npub1${string}`
                      ].parsedContent?.marks.filter(
                        (m) =>
                          (m.pdfFileHash
                            ? m.pdfFileHash == hash
                            : m.fileHash == hash) && m.location.page == i
                      )
                      if (m) {
                        marks.push(...m)
                      }
                    })
                    return (
                      <div className="image-wrapper" key={i}>
                        <img
                          draggable="false"
                          src={page.image}
                          alt={`page ${i} of ${file.name}`}
                        />
                        {marks.map((m) => {
                          return (
                            <div
                              className={`file-mark ${styles.mark}`}
                              key={m.id}
                              style={{
                                left: inPx(from(page.width, m.location.left)),
                                top: inPx(from(page.width, m.location.top)),
                                width: inPx(from(page.width, m.location.width)),
                                height: inPx(
                                  from(page.width, m.location.height)
                                ),
                                fontFamily: FONT_TYPE,
                                fontSize: inPx(from(page.width, FONT_SIZE))
                              }}
                            >
                              <MarkRender
                                markType={m.type}
                                value={m.value}
                                mark={m}
                              />
                            </div>
                          )
                        })}
                      </div>
                    )
                  })}
                {file.isImage && (
                  <img
                    className="file-image"
                    src={file.objectUrl}
                    alt={file.name}
                  />
                )}
                {!(file.isPdf || file.isImage) && (
                  <ExtensionFileBox extension={file.extension} />
                )}
              </div>
              {i < files.length - 1 && <FileDivider />}
            </React.Fragment>
          )
        })
      ) : noFiles ? (
        ''
      ) : (
        <LoadingSpinner variant="small" />
      )}
    </div>
  )
}

export const VerifyPage = () => {
  const location = useLocation()
  const params = useParams()
  const { updateUsersAppData, sendNotification } = useNDK()

  const usersAppData = useAppSelector((state) => state.userAppData)
  const usersPubkey = useAppSelector((state) => state.auth.usersPubkey)

  const nostrController = NostrController.getInstance()

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

  /**
   * uploadedZip will be received from home page when a user uploads a sigit zip wrapper that contains meta.json
   * meta will be received in navigation from create & home page in online mode
   */
  let metaInNavState = location?.state?.meta || undefined
  const { uploadedZip } = location.state || {}
  const [selectedFile, setSelectedFile] = useState<File | null>(null)

  /**
   * If `userAppData` is present it means user is logged in and we can extract list of `sigits` from the store.
   * If ID is present in the URL we search in the `sigits` list
   * Otherwise sigit is set from the `location.state.meta`
   */
  if (usersAppData) {
    const sigitCreateId = params.id

    if (sigitCreateId) {
      const sigit = usersAppData.sigits[sigitCreateId]

      if (sigit) {
        metaInNavState = sigit
      }
    }
  }

  useEffect(() => {
    if (uploadedZip) {
      setSelectedFile(uploadedZip)
    }
  }, [uploadedZip])

  const [meta, setMeta] = useState<Meta>(metaInNavState)

  const {
    submittedBy,
    zipUrls,
    encryptionKey,
    signers,
    viewers,
    fileHashes,
    parsedSignatureEvents,
    timestamps
  } = useSigitMeta(meta)

  const [files, setFiles] = useState<{ [filename: string]: SigitFile }>({})

  const [currentFile, setCurrentFile] = useState<CurrentUserFile | null>(null)
  const [currentFileHashes, setCurrentFileHashes] = useState<{
    [key: string]: string | null
  }>({})

  const signTimestampEvent = async (signerContent: {
    timestamps: OpenTimestamp[]
  }): Promise<SignedEvent | null> => {
    return await signEventForMetaFile(
      JSON.stringify(signerContent),
      nostrController,
      setIsLoading
    )
  }

  useEffect(() => {
    if (Object.entries(files).length > 0) {
      const tmp = getCurrentUserFiles(files, currentFileHashes, fileHashes)
      setCurrentFile(tmp[0])
    }
  }, [currentFileHashes, fileHashes, files])

  useEffect(() => {
    if (
      timestamps &&
      timestamps.length > 0 &&
      usersPubkey &&
      submittedBy &&
      parsedSignatureEvents
    ) {
      if (timestamps.every((t) => !!t.verification)) {
        return
      }
      const upgradeT = async (timestamps: OpenTimestamp[]) => {
        try {
          setLoadingSpinnerDesc('Upgrading your timestamps.')

          const findCreatorTimestamp = (timestamps: OpenTimestamp[]) => {
            if (usersPubkey === submittedBy) {
              return timestamps[0]
            }
          }

          const findSignerTimestamp = (timestamps: OpenTimestamp[]) => {
            const parsedEvent = parsedSignatureEvents[hexToNpub(usersPubkey)]
            if (parsedEvent?.id) {
              return timestamps.find((t) => t.nostrId === parsedEvent.id)
            }
          }

          /**
           * Checks if timestamp verification has been achieved for the first time.
           * Note that the upgrade flag is separate from verification. It is possible for a timestamp
           * to not be upgraded, but to be verified for the first time.
           * @param upgradedTimestamp
           * @param timestamps
           */
          const isNewlyVerified = (
            upgradedTimestamp: OpenTimestampUpgradeVerifyResponse,
            timestamps: OpenTimestamp[]
          ) => {
            if (!upgradedTimestamp.verified) return false
            const oldT = timestamps.find(
              (t) => t.nostrId === upgradedTimestamp.timestamp.nostrId
            )
            if (!oldT) return false
            if (!oldT.verification && upgradedTimestamp.verified) return true
          }

          const userTimestamps: OpenTimestamp[] = []

          const creatorTimestamp = findCreatorTimestamp(timestamps)
          if (creatorTimestamp) {
            userTimestamps.push(creatorTimestamp)
          }

          const signerTimestamp = findSignerTimestamp(timestamps)
          if (signerTimestamp) {
            userTimestamps.push(signerTimestamp)
          }

          if (userTimestamps.every((t) => !!t.verification)) {
            return
          }

          const upgradedUserTimestamps = await Promise.all(
            userTimestamps.map(upgradeAndVerifyTimestamp)
          )

          const upgradedTimestamps = upgradedUserTimestamps
            .filter((t) => t.upgraded || isNewlyVerified(t, userTimestamps))
            .map((t) => {
              const timestamp: OpenTimestamp = { ...t.timestamp }
              if (t.verified) {
                timestamp.verification = t.verification
              }
              return timestamp
            })

          if (upgradedTimestamps.length === 0) {
            return
          }

          setLoadingSpinnerDesc('Signing a timestamp upgrade event.')

          const signedEvent = await signTimestampEvent({
            timestamps: upgradedTimestamps
          })
          if (!signedEvent) return

          const finalTimestamps = timestamps.map((t) => {
            const upgraded = upgradedTimestamps.find(
              (tu) => tu.nostrId === t.nostrId
            )
            if (upgraded) {
              return {
                ...upgraded,
                signature: JSON.stringify(signedEvent, null, 2)
              }
            }
            return t
          })

          const updatedMeta = _.cloneDeep(meta)
          updatedMeta.timestamps = [...finalTimestamps]
          updatedMeta.modifiedAt = unixNow()

          const updatedEvent = await updateUsersAppData([updatedMeta])
          if (!updatedEvent) return

          const metaUrl = await uploadMetaToFileStorage(
            updatedMeta,
            encryptionKey
          )

          const userSet = new Set<`npub1${string}`>()
          signers.forEach((signer) => {
            if (signer !== usersPubkey) {
              userSet.add(signer)
            }
          })

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

          const users = Array.from(userSet)
          const promises = users.map((user) =>
            sendNotification(npubToHex(user)!, {
              metaUrls: metaUrl,
              keys: meta.keys!
            })
          )

          await Promise.all(promises)

          toast.success('Timestamp updates have been sent successfully.')

          setMeta(meta)
        } catch (err) {
          console.error(err)
          toast.error(
            'There was an error upgrading or verifying your timestamps!'
          )
        }
      }
      upgradeT(timestamps)
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [timestamps, submittedBy, parsedSignatureEvents])

  useEffect(() => {
    if (metaInNavState && encryptionKey) {
      const processSigit = async () => {
        setIsLoading(true)

        // We have multiple zipUrls, we should fetch one by one and take the first one which successfully decrypts
        // If a file is altered decrytption will fail
        setLoadingSpinnerDesc('Fetching file from file server')

        if (!zipUrls || zipUrls.length === 0) {
          toast.warning('No zip files found in the zipUrls')
          setIsLoading(false)
          return
        }

        for (let i = 0; i < zipUrls.length; i++) {
          const zipUrl = zipUrls[i]
          const isLastZipUrl = i === zipUrls.length - 1

          try {
            // Fetch zip data
            const res = await axios.get(zipUrl, {
              responseType: 'arraybuffer'
            })

            // Prepare file from response
            const fileName = zipUrl.split('/').pop()
            const file = new File([res.data], fileName!)
            const encryptedArrayBuffer = await file.arrayBuffer()

            // Decrypt the array buffer
            const arrayBuffer = await decryptArrayBuffer(
              encryptedArrayBuffer,
              encryptionKey
            ).catch((err) => {
              const error = err.message
                ? `Decryption error: ${err.message}`
                : 'An error occurred in decrypting file.'
              console.error('Error in decryption:>> ', err)
              toast.error(error)
              return null // Continue iteration for next zipUrl
            })

            if (!arrayBuffer) {
              if (!isLastZipUrl) continue // Skip to next zipUrl if decryption fails
              break // If last zipUrl break out of loop
            }

            // Load zip archive
            const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => {
              console.error('Error in loading zip file :>> ', err)
              toast.error(
                err.message || 'An error occurred in loading zip file.'
              )
              return null // Skip to next zipUrl
            })

            if (!zip) {
              if (!isLastZipUrl) continue // Skip to next zipUrl
              // If it's the last zip url, and still no `zip` found, break out of loop,
              // it means no files were successfully fetched or all files failed the validation (hash check).
              break
            }

            const files: { [fileName: string]: SigitFile } = {}
            const fileHashes: { [key: string]: string | null } = {}
            const fileNames = Object.values(zip.files).map(
              (entry) => entry.name
            )

            // Generate hashes for all entries in the files folder of zipArchive
            for (const entryFileName of fileNames) {
              const entryArrayBuffer = await readContentOfZipEntry(
                zip,
                entryFileName,
                'arraybuffer'
              )
              if (entryArrayBuffer) {
                files[entryFileName] = await convertToSigitFile(
                  entryArrayBuffer,
                  entryFileName
                )

                fileHashes[entryFileName.replace(/^files\//, '')] =
                  await getHash(entryArrayBuffer)
              } else {
                fileHashes[entryFileName.replace(/^files\//, '')] = null
              }
            }

            setCurrentFileHashes(fileHashes)
            setFiles(files)
            setIsLoading(false)
          } catch (err) {
            const message = `error occurred in getting file from ${zipUrl}`
            console.error(message, err)
            if (err instanceof Error) toast.error(err.message)
            else toast.error(message)
          } finally {
            setIsLoading(false)
          }
        }
      }

      processSigit()
    }
  }, [encryptionKey, metaInNavState, zipUrls])

  const handleVerify = async () => {
    if (!selectedFile) return
    setIsLoading(true)

    const zip = await JSZip.loadAsync(selectedFile).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

    const files: { [filename: string]: SigitFile } = {}
    const fileHashes: { [key: string]: string | null } = {}
    const fileNames = Object.values(zip.files)
      .filter((entry) => entry.name.startsWith('files/') && !entry.dir)
      .map((entry) => entry.name)

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

      const fileName = zipFilePath.replace(/^files\//, '')
      if (arrayBuffer) {
        files[fileName] = await convertToSigitFile(arrayBuffer, fileName)
        const hash = await getHash(arrayBuffer)

        if (hash) {
          fileHashes[fileName] = hash
        }
      } else {
        fileHashes[fileName] = null
      }
    }

    setFiles(files)
    setCurrentFileHashes(fileHashes)

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

    if (!parsedMetaJson) return

    setMeta(parsedMetaJson)

    setIsLoading(false)
  }

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

  // Check if the current user is the last signer
  const checkIsLastSigner = (signers: string[]): boolean => {
    const usersNpub = hexToNpub(usersPubkey!)
    const lastSignerIndex = signers.length - 1
    const signerIndex = signers.indexOf(usersNpub)
    return signerIndex === lastSignerIndex
  }

  // create final zip file
  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 isLastSigner = checkIsLastSigner(signers)

    const userSet = new Set<string>()

    if (isLastSigner) {
      if (submittedBy) {
        userSet.add(submittedBy)
      }

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

      viewers.forEach((viewer) => {
        userSet.add(npubToHex(viewer)!)
      })
    } else {
      const usersNpub = hexToNpub(usersPubkey!)
      const signerIndex = signers.indexOf(usersNpub)
      const nextSigner = signers[signerIndex + 1]
      userSet.add(npubToHex(nextSigner)!)
    }

    const keysFileContent = await generateKeysFile(
      Array.from(userSet),
      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'
    })
  }

  const handleExport = async () => {
    const arrayBuffer = await prepareZipExport()
    if (!arrayBuffer) return

    const blob = new Blob([arrayBuffer])
    saveAs(blob, `exported-${unixNow()}.sigit.zip`)

    setIsLoading(false)
  }

  const handleEncryptedExport = async () => {
    const arrayBuffer = await prepareZipExport()
    if (!arrayBuffer) return

    const key = await generateEncryptionKey()

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

    const finalZipFile = await createFinalZipFile(encryptedArrayBuffer, key)

    if (!finalZipFile) return
    saveAs(finalZipFile, `exported-${unixNow()}.sigit.zip`)

    setIsLoading(false)
  }

  const prepareZipExport = async (): Promise<ArrayBuffer | null> => {
    if (Object.entries(files).length === 0 || !meta || !usersPubkey)
      return Promise.resolve(null)

    const usersNpub = hexToNpub(usersPubkey)
    if (
      !signers.includes(usersNpub) &&
      !viewers.includes(usersNpub) &&
      submittedBy !== usersNpub
    ) {
      return Promise.resolve(null)
    }

    setIsLoading(true)
    setLoadingSpinnerDesc('Signing nostr event')

    const prevSig = getLastSignersSig(meta, signers)
    if (!prevSig) return Promise.resolve(null)

    const signedEvent = await signEventForMetaFile(
      JSON.stringify({ prevSig }),
      nostrController,
      setIsLoading
    )

    if (!signedEvent) return Promise.resolve(null)

    const exportSignature = JSON.stringify(signedEvent, null, 2)
    const updatedMeta = { ...meta, exportSignature }
    const stringifiedMeta = JSON.stringify(updatedMeta, null, 2)

    const zip = await getZipWithFiles(updatedMeta, files)
    zip.file('meta.json', stringifiedMeta)

    const arrayBuffer = await zip
      .generateAsync({
        type: ARRAY_BUFFER,
        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 Promise.resolve(null)

    return Promise.resolve(arrayBuffer)
  }

  return (
    <>
      {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
      <Container className={styles.container}>
        {!meta && (
          <>
            <Typography component="label" variant="h6">
              Select exported zip file
            </Typography>

            <MuiFileInput
              placeholder="Select file"
              value={selectedFile}
              onChange={(value) => setSelectedFile(value)}
              InputProps={{
                inputProps: {
                  accept: '.sigit.zip'
                }
              }}
            />

            {selectedFile && (
              <Box sx={{ mt: 2, display: 'flex', justifyContent: 'center' }}>
                <Button onClick={handleVerify} variant="contained">
                  Verify
                </Button>
              </Box>
            )}
          </>
        )}

        {meta && (
          <StickySideColumns
            left={
              currentFile !== null && (
                <FileList
                  files={getCurrentUserFiles(
                    files,
                    currentFileHashes,
                    fileHashes
                  )}
                  currentFile={currentFile}
                  setCurrentFile={setCurrentFile}
                  handleExport={handleExport}
                  handleEncryptedExport={handleEncryptedExport}
                />
              )
            }
            right={<UsersDetails meta={meta} />}
            leftIcon={faFileDownload}
            centerIcon={faFile}
            rightIcon={faCircleInfo}
          >
            <SlimPdfView
              currentFile={currentFile}
              files={getCurrentUserFiles(files, currentFileHashes, fileHashes)}
              parsedSignatureEvents={parsedSignatureEvents}
              noFiles={!zipUrls || zipUrls.length === 0}
            />
            {!zipUrls || zipUrls.length === 0 ? (
              <Typography textAlign="center">
                We were not able to retrieve the files.
              </Typography>
            ) : (
              ''
            )}
          </StickySideColumns>
        )}
      </Container>
    </>
  )
}
