import axios, { type AxiosRequestConfig } from 'axios'
import CryptoJS from 'crypto-js'

type UploadFileResponseItem = {
  createTime: string
  createUser: number
  fileBucket: string
  fileId: string
  fileName: string
  filePath: string
  fileSizeKb: number
  fileSuffix: string
  finalName: string
  realPath: string
  updateTime: string
  updateUser: string
}

type ChunkCheckResponse = {
  status: number
  fileUrl?: string
  chunkNumbers?: number[]
}

const OA_UPLOAD_BASE_URL = 'http://47.112.96.71:8082'
const CHUNK_UPLOAD_THRESHOLD = 10 * 1024 * 1024
const CHUNK_SIZE = 5 * 1024 * 1024
const MAX_CONCURRENT_UPLOADS = 3

function hashBufferMD5(buffer: ArrayBuffer): string {
  const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(buffer))
  return CryptoJS.MD5(wordArray).toString(CryptoJS.enc.Hex)
}

function readSlice(file: File, start: number, end: number): Promise<ArrayBuffer> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = (event) => resolve(event.target?.result as ArrayBuffer)
    reader.onerror = () => reject(reader.error ?? new Error('读取文件分片失败'))
    reader.readAsArrayBuffer(file.slice(start, end))
  })
}

// 返回一个由文件名 + 头部 + 尾部 + 中间抽样共同生成的综合指纹 用于检查文件是否存在 hash不耗事件 主要等待异步io耗时间
// 4GB的视频耗时30s大概
async function fileHashHeadTailMiddle(file: File, chunkSize = 1024 * 1024) {
  const fileSize = file.size
  const finalHashes: string[] = []

  // 对文件名做md5加密
  const fileName = file.name.split('_').slice(0, -1).join('_') || file.name
  finalHashes.push(CryptoJS.MD5(fileName).toString(CryptoJS.enc.Hex))

  // 读取文件头做md5加密
  const headBuffer = await readSlice(file, 0, Math.min(chunkSize, fileSize))
  finalHashes.push(hashBufferMD5(headBuffer))

  // 读取文件尾做md5加密
  const tailSize = Math.min(chunkSize, fileSize)
  const tailBuffer = await readSlice(file, fileSize - tailSize, fileSize)
  finalHashes.push(hashBufferMD5(tailBuffer))

  // 循环读取文件中间的部分 然后部分做md5加密
  if (fileSize > 2 * chunkSize) {
    let offset = chunkSize

    while (offset < fileSize - chunkSize) {
      const size = Math.min(chunkSize, fileSize - chunkSize - offset)
      const buffer = await readSlice(file, offset, offset + size)
      const u8 = new Uint8Array(buffer)
      const piece = new Uint8Array(10)

      // 取前5个字节和后5个字节做md5加密 计算时间比较短
      piece.set(u8.slice(0, Math.min(5, u8.length)), 0)
      piece.set(u8.slice(Math.max(u8.length - 5, 0)), 5)
      finalHashes.push(hashBufferMD5(piece.buffer))
      offset += size
    }
  }

  // 将所有md5加密后的结果拼接起来 然后做md5加密
  return CryptoJS.MD5(finalHashes.join('')).toString(CryptoJS.enc.Hex)
}

// 将文件进行分片 返回一个Blob数组
function createChunks(file: File, chunkSize: number) {
  const chunks: Blob[] = []
  let start = 0

  while (start < file.size) {
    const end = Math.min(start + chunkSize, file.size)
    chunks.push(file.slice(start, end))
    start = end
  }

  return chunks
}

// 分片上传文件
/**
 * 第 1 步：给文件生成一个 hash
 * 第 2 步：调用 check 询问服务端当前状态
 * 第 3 步：根据 check 结果决定是否秒传 或者 断点续传 或者 正常分片上传
 * 第 4 步：把大文件切成多个分片
 * 第 5 步：创建多个并发 worker，每个 worker 循环领取分片任务 进行分片上传
 * 第 6 步：所有分片都上传完成后，通知服务端合并
 * 第 7 步：如果文件存在，则合并文件
 */
async function uploadFileByChunks(
  file: File,
  options: {
    onProgress?: (progress: number) => void
  },
  signal: AbortSignal,
): Promise<UploadFileResponseItem> {
  const { onProgress } = options
  const fileHash = await fileHashHeadTailMiddle(file)
  const fileSuffix = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.')) : ''

  const checkResponse = await axios.post<{ data: ChunkCheckResponse }>(
    `${OA_UPLOAD_BASE_URL}/mobiles/file-upload/check`,
    {
      hash: fileHash,
      fileName: file.name,
      fileSize: file.size,
      fileSuffx: fileSuffix,
    },
    { signal },
  )

  // 秒传服务检查文件是否存在
  const checkData = checkResponse.data.data
  if (checkData.status === 200 && checkData.fileUrl) {
    onProgress?.(100)
    return {
      fileId: fileHash,
      fileName: file.name,
      filePath: checkData.fileUrl,
      finalName: file.name,
    } as UploadFileResponseItem
  }

  const chunks = createChunks(file, CHUNK_SIZE)
  const totalChunks = chunks.length

  // 断点续传 记录已经上传的片数
  const uploadedSet = new Set<number>(
    Array.isArray(checkData.chunkNumbers)
      ? checkData.chunkNumbers
          .map((item) => Number(item))
          .filter((item) => Number.isInteger(item) && item >= 0)
      : [],
  )

  const updateProgress = () => {
    const progress = totalChunks > 0 ? Math.floor((uploadedSet.size / totalChunks) * 100) : 0
    onProgress?.(progress)
  }

  // 有可能部分已经上传过了直接更新一次上传进度
  updateProgress()

  // 上传单个分片
  const uploadChunk = async (chunk: Blob, index: number) => {
    const formData = new FormData()
    formData.append('fileId', fileHash)
    formData.append('filePart', chunk, file.name)
    formData.append('chunkNumber', String(index))

    await axios.post(`${OA_UPLOAD_BASE_URL}/mobiles/file-upload/chunk`, formData, {
      signal,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    })

    uploadedSet.add(index)
    updateProgress()
  }

  let nextIndex = 0
  const worker = async () => {
    // 遍历所有分片 如果已经上传过了就跳过 否则上传
    // 相当于开了 3个while死循环 同时上传文件  只有等3个线程都上传完了 才结束
    // 总有某个 worker 在自己的异步任务完成后恢复执行，然后继续进入下一轮 while，领取新的分片任务。

    while (nextIndex < totalChunks) {
      const currentIndex = nextIndex
      nextIndex += 1

      if (uploadedSet.has(currentIndex)) continue

      await uploadChunk(chunks[currentIndex]!, currentIndex)
    }
  }

  //  同时开了 3 个异步 worker，每个 worker 里面都有一个 while 循环，不断去“领取下一个还没处理的分片”。
  await Promise.all(
    Array.from({ length: Math.min(MAX_CONCURRENT_UPLOADS, totalChunks) }, () => worker()),
  )

  const finishFormData = new FormData()
  finishFormData.set('fileName', file.name)
  finishFormData.set('fileId', fileHash)
  finishFormData.set('chunkTotalCount', String(totalChunks))

  // 上传完成 通知后端文件上传完成
  const finishResponse = await axios.post<{ data: { fileUrl: string } }>(
    `${OA_UPLOAD_BASE_URL}/mobiles/file-upload/finish`,
    finishFormData,
    {
      signal,
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    },
  )

  onProgress?.(100)

  return {
    fileId: fileHash,
    fileName: file.name,
    filePath: finishResponse.data.data.fileUrl,
    finalName: file.name,
  } as UploadFileResponseItem
}

// 上传文件 10mb以上用nas分片 10mb以下正常上传
export const uploadFile = (
  file: File,
  options: {
    onProgress?: (progress: number) => void
  } = {},
): {
  cancel: () => void
  promise: Promise<UploadFileResponseItem>
} => {
  const { onProgress } = options
  const controller = new AbortController()
  const isChunkUpload = file.size > CHUNK_UPLOAD_THRESHOLD

  const promise = isChunkUpload
    ? uploadFileByChunks(file, options, controller.signal)
    : (() => {
        // 用IIFE包裹直接返回promise
        const formData = new FormData()
        formData.append('fileList', file)

        const axiosOptions: AxiosRequestConfig<FormData> = {
          signal: controller.signal,
        }

        if (onProgress) {
          axiosOptions.onUploadProgress = (progressEvent) => {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / (progressEvent.total || 1),
            )
            onProgress(percentCompleted)
          }
        }

        return axios
          .post(`${OA_UPLOAD_BASE_URL}/mobiles/uploadFile`, formData, axiosOptions)
          .then((data) => data.data.data[0] as UploadFileResponseItem)
      })()

  return {
    cancel: () => {
      controller.abort()
    },
    promise,
  }
}
