import { BatchId, Bee, BZZ, Duration, PostageBatch, RedundancyLevel, Size } from '@ethersphere/bee-js'
import {
  DriveInfo,
  estimateDriveListMetadataSize,
  estimateFileInfoMetadataSize,
  FileInfo,
  FileManagerBase,
} from '@solarpunkltd/file-manager-lib'
import React from 'react'

import { getHumanReadableFileSize } from '../../../utils/file'
import { ActionTag } from '../constants/transfers'

export const getUsableStamps = async (bee: Bee): Promise<PostageBatch[]> => {
  try {
    return (await bee.getPostageBatches())
      .filter(s => s.usable)
      .sort((a, b) => (a.label || '').localeCompare(b.label || ''))
  } catch {
    return []
  }
}

export const validateStampStillExists = async (bee: Bee, batchId: BatchId): Promise<boolean> => {
  try {
    const stamp = await bee.getPostageBatch(batchId.toString())

    return stamp.usable
  } catch (error) {
    // eslint-disable-next-line no-console
    console.warn(`Failed to validate stamp ${batchId.toString().slice(0, 8)}...:`, error)

    return false
  }
}

export const fmGetStorageCost = async (
  capacity: number,
  validityEndDate: Date,
  encryption: boolean,
  erasureCodeLevel: RedundancyLevel,
  beeApi: Bee | null,
): Promise<BZZ | undefined> => {
  try {
    if (Size.fromBytes(capacity).toGigabytes() >= 0 && validityEndDate.getTime() >= new Date().getTime()) {
      const cost = await beeApi?.getStorageCost(
        Size.fromBytes(capacity),
        Duration.fromEndDate(validityEndDate),
        undefined,
        encryption,
        erasureCodeLevel,
      )

      return cost
    }

    return undefined
  } catch {
    return undefined
  }
}

export const fmFetchCost = async (
  capacity: number,
  validityEndDate: Date,
  encryption: boolean,
  erasureCodeLevel: RedundancyLevel,
  beeApi: Bee | null,
  setCost: (cost: BZZ) => void,
  currentFetch: React.MutableRefObject<Promise<void> | null>,
  onError?: (error: unknown) => void,
) => {
  if (currentFetch.current) {
    await currentFetch.current
  }

  let isCurrentFetch = true

  const fetchPromise = (async () => {
    try {
      const cost = await fmGetStorageCost(capacity, validityEndDate, encryption, erasureCodeLevel, beeApi)

      if (isCurrentFetch) {
        if (cost) {
          setCost(cost)
        } else {
          setCost(BZZ.fromDecimalString('0'))
          onError?.(new Error('Storage cost unavailable - node may be syncing'))
        }
      }
    } catch (error) {
      if (isCurrentFetch) {
        setCost(BZZ.fromDecimalString('0'))
        onError?.(error)
      }
    }
  })()

  currentFetch.current = fetchPromise
  await fetchPromise

  isCurrentFetch = false
  currentFetch.current = null
}

export interface CreateDriveOptions {
  beeApi: Bee | null
  fm: FileManagerBase | null
  size: Size
  duration: Duration
  label: string
  encryption: boolean
  redundancyLevel: RedundancyLevel
  adminRedundancy: RedundancyLevel
  isAdmin: boolean
  resetState: boolean
  existingBatch: PostageBatch | null
  onSuccess?: () => void
  onError?: (error: unknown) => void
}

export const handleCreateDrive = async (options: CreateDriveOptions): Promise<void> => {
  const {
    beeApi,
    fm,
    size,
    duration,
    label,
    encryption,
    redundancyLevel,
    adminRedundancy,
    isAdmin,
    resetState,
    existingBatch,
    onSuccess,
    onError,
  } = { ...options }

  if (!beeApi || !fm) {
    // eslint-disable-next-line no-console
    console.error('Error creating drive: Bee API or FM is invalid!')

    onError?.('Error creating drive: Bee API or FM is invalid!')

    return
  }

  try {
    let batchId: BatchId

    if (!existingBatch) {
      if (!isAdmin) {
        if (!fm.adminStamp) {
          // eslint-disable-next-line no-console
          console.error('Error creating drive: admin stamp is not available')

          throw new Error('Error creating drive: admin stamp is not available')
        }

        verifyDriveSpace({
          fm,
          redundancyLevel,
          stamp: fm.adminStamp,
          adminRedundancy,
          cb: err => {
            throw new Error(err)
          },
        })
      }

      batchId = await beeApi.buyStorage(size, duration, { label }, undefined, encryption, redundancyLevel)
    } else {
      const isValid = await validateStampStillExists(beeApi, existingBatch.batchID)

      if (!isValid) {
        throw new Error(
          'The stamp is no longer valid or has been deleted. Please select a different stamp from the list.',
        )
      }

      verifyDriveSpace({
        fm,
        redundancyLevel,
        stamp: existingBatch,
        adminRedundancy,
        cb: err => {
          throw new Error(err)
        },
      })

      batchId = existingBatch.batchID
    }

    await fm.createDrive(batchId, label, isAdmin, redundancyLevel, resetState)

    onSuccess?.()
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error('Error creating drive:', e instanceof Error ? e.message : String(e))
    onError?.(e)
  }
}

export interface DestroyDriveOptions {
  beeApi?: Bee | null
  fm: FileManagerBase | null
  drive: DriveInfo
  adminDrive: DriveInfo | null
  isDestroy: boolean
  onSuccess?: () => void
  onError?: (error: unknown) => void
}

export const handleDestroyAndForgetDrive = async (options: DestroyDriveOptions): Promise<void> => {
  const { beeApi, fm, adminDrive, drive, isDestroy, onSuccess, onError } = { ...options }

  if (!beeApi) {
    onError?.('Bee API is invalid!')

    return
  }

  if (!fm || !fm.adminStamp || !adminDrive) {
    onError?.('FM is invalid!')

    return
  }

  try {
    verifyDriveSpace({
      fm,
      driveId: drive.id.toString(),
      redundancyLevel: drive.redundancyLevel,
      stamp: fm.adminStamp,
      isRemove: true,
      adminRedundancy: adminDrive.redundancyLevel,
      cb: err => {
        throw new Error(err)
      },
    })

    if (!isDestroy) {
      await fm.forgetDrive(drive)
      onSuccess?.()

      return
    }

    const driveStamp = (await getUsableStamps(beeApi)).find(s => s.batchID.toString() === drive.batchId.toString())

    const ttlDays = driveStamp?.duration.toDays() ?? 0

    if (!driveStamp || ttlDays <= 2) {
      // eslint-disable-next-line no-console
      console.warn(`Stamp not found or TTL ${ttlDays} <= 2 days, skipping drive destruction: forgetting the drive.`)
      await fm.forgetDrive(drive)
      onSuccess?.()

      return
    }

    await fm.destroyDrive(drive, driveStamp)
    onSuccess?.()
  } catch (e) {
    onError?.(e)
  }
}

export interface StampCapacityMetrics {
  capacityPct: number
  usedSize: string
  stampSize: string
  usedBytes: number
  stampSizeBytes: number
  remainingBytes: number
}

export const calculateStampCapacityMetrics = (
  stamp: PostageBatch,
  files: FileInfo[],
  redundancyLevel?: RedundancyLevel,
  useReportedOnly?: boolean,
): StampCapacityMetrics => {
  let stampSizeBytes = 0
  let remainingReportedBytes = 0

  if (redundancyLevel !== undefined) {
    stampSizeBytes = stamp.calculateSize(false, redundancyLevel).toBytes()
    remainingReportedBytes = stamp.calculateRemainingSize(false, redundancyLevel).toBytes()
  } else {
    stampSizeBytes = stamp.size.toBytes()
    remainingReportedBytes = stamp.remainingSize.toBytes()
  }

  const usedBytesReported = stampSizeBytes - remainingReportedBytes
  const pctReportedStampUsage = stamp.usage * 100

  let usedSizeMaxBytes = usedBytesReported
  let pctFromDriveUsage = pctReportedStampUsage
  let remainingBytes = remainingReportedBytes

  if (!useReportedOnly) {
    const usedBytesFromFiles = files
      .map(f => {
        let rawSize = 0

        const lifecycle = (f.customMetadata?.lifecycle || '').toString().toLowerCase()
        const isLifecycleOperation =
          lifecycle === ActionTag.Trashed || lifecycle === ActionTag.Recovered || lifecycle === ActionTag.Restored

        if (
          (f.customMetadata?.size && !isLifecycleOperation) ||
          (f.customMetadata?.size && !f.customMetadata?.accumulatedSize)
        ) {
          rawSize = Number(f.customMetadata.size)
        }
        const accumulatedSize = Number(f.customMetadata?.accumulatedSize || rawSize || 0)

        return accumulatedSize
      })
      .reduce((acc, current) => acc + current, 0)

    const remainingBytesFromFiles = stampSizeBytes - usedBytesFromFiles > 0 ? stampSizeBytes - usedBytesFromFiles : 0
    remainingBytes = Math.min(remainingReportedBytes, remainingBytesFromFiles)

    usedSizeMaxBytes = Math.max(usedBytesFromFiles, usedBytesReported)
    pctFromDriveUsage = stampSizeBytes > 0 ? (usedSizeMaxBytes / stampSizeBytes) * 100 : 0
  }

  const usedSizeMax = getHumanReadableFileSize(usedSizeMaxBytes)
  const capacityPct = Math.max(pctFromDriveUsage, pctReportedStampUsage)
  const stampSize = getHumanReadableFileSize(stampSizeBytes)

  return {
    capacityPct,
    usedSize: usedSizeMax,
    stampSize,
    usedBytes: usedSizeMaxBytes,
    stampSizeBytes,
    remainingBytes,
  }
}

export interface DriveSpaceOptions {
  fm: FileManagerBase
  driveId?: string
  redundancyLevel: RedundancyLevel
  stamp: PostageBatch
  adminRedundancy?: RedundancyLevel
  useInfoSize?: boolean
  isRemove?: boolean
  fileSize?: number
  fileCount?: number
  cb?: (msg: string) => void
}

export const verifyDriveSpace = (
  options: DriveSpaceOptions,
): { remainingBytes: number; totalSizeBytes: number; ok: boolean } => {
  const { fm, driveId, redundancyLevel, stamp, adminRedundancy, useInfoSize, isRemove, fileSize, fileCount, cb } = {
    ...options,
  }

  const drives = [...fm.driveList]
  let filesPerDrives: FileInfo[] = []

  // new drivelist state size calc.
  if (isRemove) {
    const driveIx = drives.findIndex(d => d.id.toString() === driveId?.toString())

    if (driveIx === -1) {
      cb?.(`Admin drive not found during stamp verification`)

      return { remainingBytes: 0, totalSizeBytes: 0, ok: false }
    }

    drives.splice(driveIx, 1)
    filesPerDrives = fm.fileInfoList.filter(fi => fi.driveId !== driveId)
  } else {
    filesPerDrives = driveId ? fm.fileInfoList.filter(fi => fi.driveId === driveId) : []
  }

  // admin stamp capacity calcl., needed for forget, destroy, create
  if (adminRedundancy !== undefined && fm.adminStamp) {
    // upper limit estimate on the drivelist metadata state size based on the number of drives and files
    const estimatedDlSizeBytes = estimateDriveListMetadataSize(drives) * drives.length
    const { remainingBytes: remainingAdminBytes } = calculateStampCapacityMetrics(fm.adminStamp, [], adminRedundancy)

    const ok = remainingAdminBytes >= estimatedDlSizeBytes

    if (!ok) {
      cb?.(
        `Insufficient admin drive capacity. Required: ~${getHumanReadableFileSize(
          estimatedDlSizeBytes,
        )} bytes, Available: ${getHumanReadableFileSize(
          remainingAdminBytes,
        )} bytes. Please top up the admin drive/stamp.`,
      )

      return { remainingBytes: remainingAdminBytes, totalSizeBytes: estimatedDlSizeBytes, ok }
    }
  }

  // other fileinfo metadata size calc.
  const estimatedFiSize = estimateFileInfoMetadataSize()
  const count = fileCount ?? 1
  const estimateReqSizeBytes = Number(Boolean(useInfoSize)) * estimatedFiSize * count + (fileSize ? fileSize : 0)
  const { remainingBytes } = calculateStampCapacityMetrics(stamp, filesPerDrives, redundancyLevel)

  const ok = remainingBytes >= estimateReqSizeBytes

  if (!ok) {
    cb?.(
      `Insufficient capacity. Required: ~${getHumanReadableFileSize(
        estimateReqSizeBytes,
      )} bytes, Available: ${getHumanReadableFileSize(remainingBytes)} bytes. Please top up the drive/stamp.`,
    )
  }

  return { remainingBytes, totalSizeBytes: estimateReqSizeBytes, ok }
}
