import { useEffect, useState } from 'react'
import {
  CreateSignatureEventContent,
  DocSignatureEvent,
  Meta,
  SignedEventContent,
  Timestamp
} from '../types'
import { Mark } from '../types/mark'
import {
  fromUnixTimestamp,
  hexToNpub,
  parseNostrEvent,
  parseCreateSignatureEventContent,
  SigitStatus,
  SignStatus
} from '../utils'
import { toast } from 'react-toastify'
import { verifyEvent } from 'nostr-tools'
import { Event } from 'nostr-tools'
import store from '../store/store'
import { AuthState } from '../store/auth/types'
import { NostrController } from '../controllers'
import { MetaParseError } from '../types/errors/MetaParseError'

/**
 * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`,
 * and `Event` (event's fields are made optional and pubkey and created_at replaced with our versions)
 */
export interface FlatMeta
  extends Meta,
    CreateSignatureEventContent,
    Partial<Omit<Event, 'pubkey' | 'created_at'>> {
  // Remove pubkey and use submittedBy as `npub1${string}`
  submittedBy?: `npub1${string}`

  // Optional field only present on exported sigits
  // Exporting adds user's pubkey
  exportedBy?: `npub1${string}`

  // Remove created_at and replace with createdAt
  createdAt?: number

  // Validated create signature event
  isValid: boolean

  // Decryption
  encryptionKey: string | null

  // Parsed Document Signatures
  parsedSignatureEvents: {
    [signer: `npub1${string}`]: DocSignatureEvent
  }

  // Calculated completion time
  completedAt?: number

  // Calculated status fields
  signedStatus: SigitStatus
  signersStatus: {
    [signer: `npub1${string}`]: SignStatus
  }

  timestamps?: Timestamp[]
}

/**
 * Custom use hook for parsing the Sigit Meta
 * @param meta Sigit Meta
 * @returns flattened Meta object with calculated signed status
 */
export const useSigitMeta = (meta: Meta): FlatMeta => {
  const [isValid, setIsValid] = useState(false)
  const [kind, setKind] = useState<number>()
  const [tags, setTags] = useState<string[][]>()
  const [createdAt, setCreatedAt] = useState<number>()
  const [submittedBy, setSubmittedBy] = useState<`npub1${string}`>() // submittedBy, pubkey from nostr event
  const [exportedBy, setExportedBy] = useState<`npub1${string}`>() // pubkey from export signature nostr event
  const [id, setId] = useState<string>()
  const [sig, setSig] = useState<string>()

  const [signers, setSigners] = useState<`npub1${string}`[]>([])
  const [viewers, setViewers] = useState<`npub1${string}`[]>([])
  const [fileHashes, setFileHashes] = useState<{
    [user: `npub1${string}`]: string
  }>({})
  const [markConfig, setMarkConfig] = useState<Mark[]>([])
  const [title, setTitle] = useState<string>('')
  const [zipUrl, setZipUrl] = useState<string>('')

  const [parsedSignatureEvents, setParsedSignatureEvents] = useState<{
    [signer: `npub1${string}`]: DocSignatureEvent
  }>({})

  const [completedAt, setCompletedAt] = useState<number>()

  const [signedStatus, setSignedStatus] = useState<SigitStatus>(
    SigitStatus.Partial
  )
  const [signersStatus, setSignersStatus] = useState<{
    [signer: `npub1${string}`]: SignStatus
  }>({})

  const [encryptionKey, setEncryptionKey] = useState<string | null>(null)

  useEffect(() => {
    if (!meta) return
    ;(async function () {
      try {
        if (meta.exportSignature) {
          const exportSignatureEvent = await parseNostrEvent(
            meta.exportSignature
          )
          if (
            verifyEvent(exportSignatureEvent) &&
            exportSignatureEvent.pubkey
          ) {
            setExportedBy(exportSignatureEvent.pubkey as `npub1${string}`)
          }
        }

        const createSignatureEvent = await parseNostrEvent(meta.createSignature)

        const { kind, tags, created_at, pubkey, id, sig, content } =
          createSignatureEvent

        setIsValid(verifyEvent(createSignatureEvent))
        setKind(kind)
        setTags(tags)
        // created_at in nostr events are stored in seconds
        setCreatedAt(fromUnixTimestamp(created_at))
        setSubmittedBy(pubkey as `npub1${string}`)
        setId(id)
        setSig(sig)

        const { title, signers, viewers, fileHashes, markConfig, zipUrl } =
          await parseCreateSignatureEventContent(content)

        setTitle(title)
        setSigners(signers)
        setViewers(viewers)
        setFileHashes(fileHashes)
        setMarkConfig(markConfig)
        setZipUrl(zipUrl)

        if (meta.keys) {
          const { sender, keys } = meta.keys
          // Retrieve the user's public key from the state
          const usersPubkey = (store.getState().auth as AuthState).usersPubkey!
          const usersNpub = hexToNpub(usersPubkey)

          // Check if the user's public key is in the keys object
          if (usersNpub in keys) {
            // Instantiate the NostrController to decrypt the encryption key
            const nostrController = NostrController.getInstance()
            const decrypted = await nostrController
              .nip04Decrypt(sender, keys[usersNpub])
              .catch((err) => {
                console.log(
                  'An error occurred in decrypting encryption key',
                  err
                )
                return null
              })

            setEncryptionKey(decrypted)
          }
        }
        // Temp. map to hold events and signers
        const parsedSignatureEventsMap = new Map<
          `npub1${string}`,
          DocSignatureEvent
        >()
        const signerStatusMap = new Map<`npub1${string}`, SignStatus>()

        const getPrevSignerSig = (npub: `npub1${string}`) => {
          if (signers[0] === npub) {
            return sig
          }

          // find the index of signer
          const currentSignerIndex = signers.findIndex(
            (signer) => signer === npub
          )
          // return if could not found user in signer's list
          if (currentSignerIndex === -1) return
          // find prev signer
          const prevSigner = signers[currentSignerIndex - 1]

          // get the signature of prev signer
          return parsedSignatureEventsMap.get(prevSigner)?.sig
        }

        for (const npub in meta.docSignatures) {
          try {
            // Parse each signature event
            const event = await parseNostrEvent(
              meta.docSignatures[npub as `npub1${string}`]
            )

            // Save events to a map, to save all at once outside loop
            // We need the object to find completedAt
            // Avoided using parsedSignatureEvents due to useEffect deps
            parsedSignatureEventsMap.set(npub as `npub1${string}`, event)
          } catch (error) {
            signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
          }
        }

        parsedSignatureEventsMap.forEach((event, npub) => {
          const isValidSignature = verifyEvent(event)
          if (isValidSignature) {
            // get the signature of prev signer from the content of current signers signedEvent
            const prevSignersSig = getPrevSignerSig(npub)
            try {
              const obj: SignedEventContent = JSON.parse(event.content)
              parsedSignatureEventsMap.set(npub, {
                ...event,
                parsedContent: obj
              })
              if (
                obj.prevSig &&
                prevSignersSig &&
                obj.prevSig === prevSignersSig
              ) {
                signerStatusMap.set(npub as `npub1${string}`, SignStatus.Signed)
              }
            } catch (error) {
              signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid)
            }
          }
        })

        signers
          .filter((s) => !parsedSignatureEventsMap.has(s))
          .forEach((s) => signerStatusMap.set(s, SignStatus.Pending))

        // Get the first signer that hasn't signed
        const nextSigner = signers.find((s) => !parsedSignatureEventsMap.has(s))
        if (nextSigner) {
          signerStatusMap.set(nextSigner, SignStatus.Awaiting)
        }

        setSignersStatus(Object.fromEntries(signerStatusMap))
        setParsedSignatureEvents(Object.fromEntries(parsedSignatureEventsMap))

        const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
        const isCompletelySigned = signers.every((signer) =>
          signedBy.includes(signer)
        )
        setSignedStatus(
          isCompletelySigned ? SigitStatus.Complete : SigitStatus.Partial
        )

        // Check if all signers signed
        if (isCompletelySigned) {
          setCompletedAt(
            fromUnixTimestamp(
              signedBy.reduce((p, c) => {
                return Math.max(
                  p,
                  parsedSignatureEventsMap.get(c)?.created_at || 0
                )
              }, 0)
            )
          )
        }
      } catch (error) {
        if (error instanceof MetaParseError) {
          toast.error(error.message)
        }
        console.error(error)
      }
    })()
  }, [meta])

  return {
    modifiedAt: meta?.modifiedAt,
    createSignature: meta?.createSignature,
    docSignatures: meta?.docSignatures,
    keys: meta?.keys,
    timestamps: meta?.timestamps,
    isValid,
    kind,
    tags,
    createdAt,
    submittedBy,
    exportedBy,
    id,
    sig,
    signers,
    viewers,
    fileHashes,
    markConfig,
    title,
    zipUrl,
    parsedSignatureEvents,
    completedAt,
    signedStatus,
    signersStatus,
    encryptionKey
  }
}
