import { CreateSignatureEventContent, Meta } from '../types'
import {
  decryptArrayBuffer,
  encryptArrayBuffer,
  fromUnixTimestamp,
  getHash,
  parseJson,
  uploadToFileStorage
} from '.'
import { Event, verifyEvent } from 'nostr-tools'
import { toast } from 'react-toastify'
import { extractFileExtensions } from './file'
import { handleError } from '../types/errors'
import {
  MetaParseError,
  MetaParseErrorType
} from '../types/errors/MetaParseError'
import axios from 'axios'
import {
  MetaStorageError,
  MetaStorageErrorType
} from '../types/errors/MetaStorageError'

export enum SignStatus {
  Signed = 'Signed',
  Awaiting = 'Awaiting',
  Pending = 'Pending',
  Invalid = 'Invalid',
  Viewer = 'Viewer'
}

export enum SigitStatus {
  Partial = 'In-Progress',
  Complete = 'Completed'
}

export interface SigitCardDisplayInfo {
  createdAt?: number
  title?: string
  submittedBy?: `npub1${string}`
  signers: `npub1${string}`[]
  fileExtensions: string[]
  signedStatus: SigitStatus
  isValid: boolean
}

/**
 * Wrapper for event parser that throws custom SigitMetaParseError with cause and context
 * @param raw Raw string for parsing
 * @returns parsed Event
 */
export const parseNostrEvent = async (raw: string): Promise<Event> => {
  try {
    const event = await parseJson<Event>(raw)
    return event
  } catch (error) {
    throw new MetaParseError(MetaParseErrorType.PARSE_ERROR_EVENT, {
      cause: handleError(error),
      context: raw
    })
  }
}

/**
 * Wrapper for event content parser that throws custom SigitMetaParseError with cause and context
 * @param raw Raw string for parsing
 * @returns parsed CreateSignatureEventContent
 */
export const parseCreateSignatureEventContent = async (
  raw: string
): Promise<CreateSignatureEventContent> => {
  try {
    const createSignatureEventContent =
      await parseJson<CreateSignatureEventContent>(raw)
    return createSignatureEventContent
  } catch (error) {
    throw new MetaParseError(
      MetaParseErrorType.PARSE_ERROR_SIGNATURE_EVENT_CONTENT,
      {
        cause: handleError(error),
        context: raw
      }
    )
  }
}

/**
 * Extracts only necessary metadata for the card display
 * @param meta Sigit metadata
 * @returns SigitCardDisplayInfo
 */
export const extractSigitCardDisplayInfo = async (meta: Meta) => {
  if (!meta?.createSignature) return

  const sigitInfo: SigitCardDisplayInfo = {
    signers: [],
    fileExtensions: [],
    signedStatus: SigitStatus.Partial,
    isValid: false
  }

  try {
    const createSignatureEvent = await parseNostrEvent(meta.createSignature)

    sigitInfo.isValid = verifyEvent(createSignatureEvent)

    // created_at in nostr events are stored in seconds
    sigitInfo.createdAt = fromUnixTimestamp(createSignatureEvent.created_at)

    const createSignatureContent = await parseCreateSignatureEventContent(
      createSignatureEvent.content
    )

    const files = Object.keys(createSignatureContent.fileHashes)
    const { extensions } = extractFileExtensions(files)

    const signedBy = Object.keys(meta.docSignatures) as `npub1${string}`[]
    const isCompletelySigned = createSignatureContent.signers.every((signer) =>
      signedBy.includes(signer)
    )

    sigitInfo.title = createSignatureContent.title
    sigitInfo.submittedBy = createSignatureEvent.pubkey as `npub1${string}`
    sigitInfo.signers = createSignatureContent.signers
    sigitInfo.fileExtensions = extensions

    if (isCompletelySigned) {
      sigitInfo.signedStatus = SigitStatus.Complete
    }

    return sigitInfo
  } catch (error) {
    if (error instanceof MetaParseError) {
      toast.error(error.message)
      console.error(error.name, error.message, error.cause, error.context)
    } else {
      console.error('Unexpected error', error)
    }
  }
}

export const uploadMetaToFileStorage = async (
  meta: Meta,
  encryptionKey: string | undefined
) => {
  // Value is the stringified meta object
  const value = JSON.stringify(meta)
  const encoder = new TextEncoder()

  // Encode it to the arrayBuffer
  const uint8Array = encoder.encode(value)

  if (!encryptionKey) {
    throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
  }

  // Encrypt the file contents with the same encryption key from the create signature
  const encryptedArrayBuffer = await encryptArrayBuffer(
    uint8Array,
    encryptionKey
  )

  const hash = await getHash(encryptedArrayBuffer)
  if (!hash) {
    throw new MetaStorageError(MetaStorageErrorType.HASHING_FAILED)
  }

  // Create the encrypted json file from array buffer and hash
  const file = new File([encryptedArrayBuffer], `${hash}.json`)
  const urls = await uploadToFileStorage(file)

  return urls
}

/**
 * Fetches the meta from one or more file storages, one by one, and it will take the first one, which has matching hash
 * @param urls urls of meta files
 * @param encryptionKey
 */
export const fetchMetaFromFileStorage = async (
  urls: string[],
  encryptionKey: string | undefined
): Promise<Meta> => {
  if (!encryptionKey) {
    throw new MetaStorageError(MetaStorageErrorType.ENCRYPTION_KEY_REQUIRED)
  }

  for (let i = 0; i < urls.length; i++) {
    const url = urls[i]
    const isLastUrl = i === urls.length - 1
    const encryptedArrayBuffer = await axios.get(url, {
      responseType: 'arraybuffer'
    })

    // Verify hash
    const parts = url.split('/')
    const urlHash = parts[parts.length - 1]
    const hash = await getHash(encryptedArrayBuffer.data)
    if (hash !== urlHash) {
      // If no more urls left to try and hash check failed, throw an error
      if (isLastUrl)
        throw new MetaStorageError(
          MetaStorageErrorType.HASH_VERIFICATION_FAILED
        )
      // Otherwise, skip to the next url to fetch
      continue
    }

    const arrayBuffer = await decryptArrayBuffer(
      encryptedArrayBuffer.data,
      encryptionKey
    ).catch((err) => {
      if (isLastUrl) {
        throw new MetaStorageError(MetaStorageErrorType.DECRYPTION_FAILED, {
          cause: err
        })
      } else {
        return null
      }
    })

    if (arrayBuffer) {
      // Decode meta.json and parse
      const decoder = new TextDecoder()
      const json = decoder.decode(arrayBuffer)
      const meta = await parseJson<Meta>(json)
      return meta
    } else if (!isLastUrl) {
      continue
    }

    throw new MetaStorageError(MetaStorageErrorType.UNHANDLED_FETCH_ERROR)
  }

  throw new MetaStorageError(
    MetaStorageErrorType.NO_URLS_PROCESSED_SUCCESSFULLY
  )
}
