import { Box, Button, Divider, Tooltip, Typography } from '@mui/material'
import JSZip from 'jszip'
import { MuiFileInput } from 'mui-file-input'
import { Event, verifyEvent } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { NostrController } from '../../controllers'
import {
  CreateSignatureEventContent,
  DocSignatureEvent,
  Meta
} from '../../types'
import {
  decryptArrayBuffer,
  extractMarksFromSignedMeta,
  getHash,
  hexToNpub,
  unixNow,
  parseJson,
  readContentOfZipEntry,
  signEventForMetaFile,
  shorten,
  getCurrentUserFiles
} from '../../utils'
import styles from './style.module.scss'
import { useLocation } from 'react-router-dom'
import axios from 'axios'
import { PdfFile } from '../../types/drawing.ts'
import {
  addMarks,
  convertToPdfBlob,
  convertToPdfFile,
  groupMarksByPage,
  inPx
} from '../../utils/pdf.ts'
import { State } from '../../store/rootReducer.ts'
import { useSelector } from 'react-redux'
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/index.tsx'
import { UserAvatar } from '../../components/UserAvatar/index.tsx'
import { useSigitProfiles } from '../../hooks/useSigitProfiles.tsx'
import { TooltipChild } from '../../components/TooltipChild.tsx'
import FileList from '../../components/FileList'
import { CurrentUserFile } from '../../types/file.ts'
import { Mark } from '../../types/mark.ts'

interface PdfViewProps {
  files: CurrentUserFile[]
  currentFile: CurrentUserFile | null
  parsedSignatureEvents: {
    [signer: `npub1${string}`]: DocSignatureEvent
  }
}

const SlimPdfView = ({
  files,
  currentFile,
  parsedSignatureEvents
}: PdfViewProps) => {
  const pdfRefs = useRef<(HTMLDivElement | null)[]>([])
  useEffect(() => {
    if (currentFile !== null && !!pdfRefs.current[currentFile.id]) {
      pdfRefs.current[currentFile.id]?.scrollIntoView({
        behavior: 'smooth',
        block: 'end'
      })
    }
  }, [currentFile])
  return (
    <div className={styles.view}>
      {files.map((currentUserFile, i) => {
        const { hash, filename, pdfFile, id } = currentUserFile
        const signatureEvents = Object.keys(parsedSignatureEvents)
        if (!hash) return
        return (
          <>
            <div
              id={filename}
              ref={(el) => (pdfRefs.current[id] = el)}
              key={filename}
              className={styles.fileWrapper}
            >
              {pdfFile.pages.map((page, i) => {
                const marks: Mark[] = []

                signatureEvents.forEach((e) => {
                  const m = parsedSignatureEvents[
                    e as `npub1${string}`
                  ].parsedContent?.marks.filter(
                    (m) => m.pdfFileHash == hash && m.location.page == i
                  )
                  if (m) {
                    marks.push(...m)
                  }
                })
                return (
                  <div className={styles.imageWrapper} key={i}>
                    <img draggable="false" src={page.image} />
                    {marks.map((m) => {
                      return (
                        <div
                          className={styles.mark}
                          key={m.id}
                          style={{
                            left: inPx(m.location.left),
                            top: inPx(m.location.top),
                            width: inPx(m.location.width),
                            height: inPx(m.location.height)
                          }}
                        >
                          {m.value}
                        </div>
                      )
                    })}
                  </div>
                )
              })}
            </div>

            {i < files.length - 1 && (
              <Divider
                sx={{
                  fontSize: '12px',
                  color: 'rgba(0,0,0,0.15)'
                }}
              >
                File Separator
              </Divider>
            )}
          </>
        )
      })}
    </div>
  )
}

export const VerifyPage = () => {
  const location = useLocation()
  /**
   * 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
   */
  const { uploadedZip, meta } = location.state || {}

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

  const profiles = useSigitProfiles([
    ...(submittedBy ? [submittedBy] : []),
    ...signers,
    ...viewers
  ])

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

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

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

  useEffect(() => {
    if (Object.entries(files).length > 0) {
      const tmp = getCurrentUserFiles(files, fileHashes)

      setCurrentFile(tmp[0])
    }
  }, [fileHashes, files])

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

  useEffect(() => {
    if (uploadedZip) {
      setSelectedFile(uploadedZip)
    } else if (meta && encryptionKey) {
      const processSigit = async () => {
        setIsLoading(true)

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

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

            if (arrayBuffer) {
              const zip = await JSZip.loadAsync(arrayBuffer).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]: PdfFile } = {}
              const fileHashes: { [key: string]: string | null } = {}
              const fileNames = Object.values(zip.files).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 fileName of fileNames) {
                const arrayBuffer = await readContentOfZipEntry(
                  zip,
                  fileName,
                  'arraybuffer'
                )

                if (arrayBuffer) {
                  files[fileName] = await convertToPdfFile(
                    arrayBuffer,
                    fileName!
                  )
                  const hash = await getHash(arrayBuffer)

                  if (hash) {
                    fileHashes[fileName.replace(/^files\//, '')] = hash
                  }
                } else {
                  fileHashes[fileName.replace(/^files\//, '')] = null
                }
              }

              setCurrentFileHashes(fileHashes)
              setFiles(files)

              setIsLoading(false)
            }
          })
          .catch((err) => {
            console.error(`error occurred in getting file from ${zipUrl}`, err)
            toast.error(
              err.message || `error occurred in getting file from ${zipUrl}`
            )
          })
          .finally(() => {
            setIsLoading(false)
          })
      }

      processSigit()
    }
  }, [encryptionKey, meta, uploadedZip, zipUrl])

  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 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 fileName of fileNames) {
      const arrayBuffer = await readContentOfZipEntry(
        zip,
        fileName,
        'arraybuffer'
      )

      if (arrayBuffer) {
        const hash = await getHash(arrayBuffer)

        if (hash) {
          fileHashes[fileName.replace(/^files\//, '')] = hash
        }
      } else {
        fileHashes[fileName.replace(/^files\//, '')] = null
      }
    }

    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

    const createSignatureEvent = await parseJson<Event>(
      parsedMetaJson.createSignature
    ).catch((err) => {
      console.log('err in parsing the createSignature event:>> ', err)
      toast.error(
        err.message || 'error occurred in parsing the create signature event'
      )
      setIsLoading(false)
      return null
    })

    if (!createSignatureEvent) return

    const isValidCreateSignature = verifyEvent(createSignatureEvent)

    if (!isValidCreateSignature) {
      toast.error('Create signature is invalid')
      setIsLoading(false)
      return
    }

    const createSignatureContent = await parseJson<CreateSignatureEventContent>(
      createSignatureEvent.content
    ).catch((err) => {
      console.log(
        `err in parsing the createSignature event's content :>> `,
        err
      )
      toast.error(
        err.message ||
          `error occurred in parsing the create signature event's content`
      )
      setIsLoading(false)
      return null
    })

    if (!createSignatureContent) return

    setIsLoading(false)
  }

  const handleExport = async () => {
    if (Object.entries(files).length === 0 || !meta || !usersPubkey) return

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

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

    const prevSig = getLastSignersSig(meta, signers)
    if (!prevSig) return

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

    if (!signedEvent) return

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

    const zip = new JSZip()
    zip.file('meta.json', stringifiedMeta)

    const marks = extractMarksFromSignedMeta(updatedMeta)
    const marksByPage = groupMarksByPage(marks)

    for (const [fileName, pdf] of Object.entries(files)) {
      const pages = await addMarks(pdf.file, marksByPage)
      const blob = await convertToPdfBlob(pages)
      zip.file(`files/${fileName}`, blob)
    }

    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-${unixNow()}.sigit.zip`)

    setIsLoading(false)
  }

  const displayExportedBy = () => {
    if (!meta || !meta.exportSignature) return null

    const exportSignatureString = meta.exportSignature

    try {
      const exportSignatureEvent = JSON.parse(exportSignatureString) as Event

      if (verifyEvent(exportSignatureEvent)) {
        const exportedBy = exportSignatureEvent.pubkey
        const profile = profiles[exportedBy]
        return (
          <Tooltip
            title={
              profile?.display_name ||
              profile?.name ||
              shorten(hexToNpub(exportedBy))
            }
            placement="top"
            arrow
            disableInteractive
          >
            <TooltipChild>
              <UserAvatar pubkey={exportedBy} image={profile?.picture} />
            </TooltipChild>
          </Tooltip>
        )
      } else {
        toast.error(`Invalid export signature!`)
        return (
          <Typography component="label" sx={{ color: 'red' }}>
            Invalid export signature
          </Typography>
        )
      }
    } catch (error) {
      console.error(`An error occurred wile parsing exportSignature`, error)
      return null
    }
  }

  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)}
                    currentFile={currentFile}
                    setCurrentFile={setCurrentFile}
                    handleDownload={handleExport}
                  />
                )}
                {displayExportedBy()}
              </>
            }
            right={<UsersDetails meta={meta} />}
          >
            <SlimPdfView
              currentFile={currentFile}
              files={getCurrentUserFiles(files, currentFileHashes)}
              parsedSignatureEvents={parsedSignatureEvents}
            />
          </StickySideColumns>
        )}
      </Container>
    </>
  )
}
