import COS from 'cos-js-sdk-v5'
import { post } from '@/api/request'

export interface CosStsData {
  bucket: string
  durationSeconds: number
  expireAt: number
  region: string
  sessionToken: string
  tmpSecretId: string
  tmpSecretKey: string
  uploadPathPrefix: string
}

export interface CosUploadResult {
  bucket: string
  filePath: string
  key: string
  region: string
  url: string
}

type UploadProgress = (event: { percent: number }) => void
type NativeUpload = (config: anyObj) => Promise<any>
export type CosFileType = 1 | 2

const stsCache: Partial<Record<CosFileType, CosStsData>> = {}
const stsPromise: Partial<Record<CosFileType, Promise<CosStsData>>> = {}

const normalizePrefix = (prefix = '') => prefix.replace(/^\/+/, '').replace(/\/?$/, '/')

const getFileExt = (fileName = '', type = '') => {
  const ext = fileName.match(/\.([a-zA-Z0-9]+)$/)?.[1]
  if (ext) return ext.toLowerCase()
  if (type.includes('/')) return type.split('/').pop() || 'png'
  return 'png'
}

const getFileName = (file: File | Blob, fileName?: string) => {
  const rawName = fileName || (file instanceof File ? file.name : '')
  const ext = getFileExt(rawName, file.type)
  return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`
}

const buildKey = (sts: CosStsData, file: File | Blob, fileName?: string) => {
  const date = new Date()
  const yyyy = date.getFullYear()
  const mm = String(date.getMonth() + 1).padStart(2, '0')
  const dd = String(date.getDate()).padStart(2, '0')
  return `${normalizePrefix(sts.uploadPathPrefix)}${yyyy}${mm}${dd}/${getFileName(file, fileName)}`
}

const getCosHost = (sts: CosStsData) => `${sts.bucket}.cos.${sts.region}.myqcloud.com`

const normalizeCosUrl = (location: string, sts: CosStsData, key: string) => {
  const url = location || `${getCosHost(sts)}/${key}`
  return /^https?:\/\//i.test(url) ? url.replace(/^http:\/\//i, 'https://') : `https://${url}`
}

const normalizeStsTime = (value: number) => (value > 9999999999 ? Math.floor(value / 1000) : value)

const normalizeSts = (sts: CosStsData): CosStsData => ({
  ...sts,
  expireAt: normalizeStsTime(Number(sts.expireAt) || 0),
})

const isValidSts = (sts: CosStsData | undefined) => {
  if (!sts) return false
  const now = Math.floor(Date.now() / 1000)
  return (
    !!sts.bucket && !!sts.region && !!sts.tmpSecretId && !!sts.tmpSecretKey && sts.expireAt > now
  )
}

export const getCosSts = async (fileType: CosFileType = 2) => {
  if (isValidSts(stsCache[fileType])) return stsCache[fileType] as CosStsData
  if (!stsPromise[fileType]) {
    stsPromise[fileType] = post<CosStsData>('/common/cos/sts', { fileType }).then(res => {
      const normalized = normalizeSts(res)
      stsCache[fileType] = normalized
      return normalized
    })
    stsPromise[fileType]?.finally(() => {
      stsPromise[fileType] = undefined
    })
  }
  return stsPromise[fileType] as Promise<CosStsData>
}

const createCos = (fileType: CosFileType) =>
  new COS({
    Protocol: 'https:',
    getAuthorization: async (_options, callback) => {
      const sts = await getCosSts(fileType)
      const now = Math.floor(Date.now() / 1000)
      callback({
        TmpSecretId: sts.tmpSecretId,
        TmpSecretKey: sts.tmpSecretKey,
        SecurityToken: sts.sessionToken,
        StartTime: Math.max(now - 60, sts.expireAt - sts.durationSeconds),
        ExpiredTime: sts.expireAt,
      })
    },
  })

export const uploadFileToCos = async (
  file: File | Blob,
  options: { fileName?: string; fileType?: CosFileType; onProgress?: UploadProgress } = {},
): Promise<CosUploadResult> => {
  const fileType = options.fileType || 2
  const sts = await getCosSts(fileType)
  const key = buildKey(sts, file, options.fileName)
  const result = await createCos(fileType).putObject({
    Bucket: sts.bucket,
    Region: sts.region,
    Key: key,
    Body: file,
    ContentType: file.type || undefined,
    onProgress: progress => {
      options.onProgress?.({ percent: Math.round((progress.percent || 0) * 100) })
    },
  })
  const url = normalizeCosUrl(result.Location, sts, key)
  return {
    bucket: sts.bucket,
    filePath: url,
    key,
    region: sts.region,
    url,
  }
}

const toHex = (buffer: ArrayBuffer) =>
  Array.from(new Uint8Array(buffer))
    .map(byte => byte.toString(16).padStart(2, '0'))
    .join('')

const sha1 = async (value: string) => {
  const data = new TextEncoder().encode(value)
  return toHex(await crypto.subtle.digest('SHA-1', data))
}

const hmacSha1 = async (key: string, value: string) => {
  const cryptoKey = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(key),
    { name: 'HMAC', hash: 'SHA-1' },
    false,
    ['sign'],
  )
  return toHex(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(value)))
}

const encodeBase64 = (value: string) => {
  const bytes = new TextEncoder().encode(value)
  let binary = ''
  bytes.forEach(byte => {
    binary += String.fromCharCode(byte)
  })
  return btoa(binary)
}

const createPostObjectFields = async (sts: CosStsData, key: string) => {
  const now = Math.floor(Date.now() / 1000)
  const start = Math.max(now - 60, sts.expireAt - sts.durationSeconds)
  const keyTime = `${start};${sts.expireAt}`
  const policy = encodeBase64(
    JSON.stringify({
      expiration: new Date(sts.expireAt * 1000).toISOString(),
      conditions: [
        { bucket: sts.bucket },
        { key },
        { 'q-sign-algorithm': 'sha1' },
        { 'q-ak': sts.tmpSecretId },
        { 'q-sign-time': keyTime },
        { 'x-cos-security-token': sts.sessionToken },
      ],
    }),
  )
  const signKey = await hmacSha1(sts.tmpSecretKey, keyTime)
  const signature = await hmacSha1(signKey, await sha1(policy))
  return {
    key,
    policy,
    'q-sign-algorithm': 'sha1',
    'q-ak': sts.tmpSecretId,
    'q-key-time': keyTime,
    'q-signature': signature,
    'x-cos-security-token': sts.sessionToken,
  }
}

export const uploadNativeFilePathToCos = async (
  filePath: string,
  nativeUpload: NativeUpload,
  options: { fileName?: string; fileType?: CosFileType; mimeType?: string } = {},
): Promise<CosUploadResult> => {
  const sts = await getCosSts(options.fileType || 2)
  const fileName = options.fileName || filePath.split(/[\\/]/).pop() || ''
  const key = buildKey(sts, new Blob([], { type: options.mimeType || 'image/png' }), fileName)
  const formData = await createPostObjectFields(sts, key)
  await nativeUpload({
    url: `https://${getCosHost(sts)}`,
    header: {},
    data: formData,
    filePath,
    fileKey: 'file',
    formData,
  })
  const url = normalizeCosUrl('', sts, key)
  return {
    bucket: sts.bucket,
    filePath: url,
    key,
    region: sts.region,
    url,
  }
}

export const toCosUploadResponse = (result: CosUploadResult) => ({
  data: {
    code: 200,
    data: result,
    message: '',
  },
})

export const cosUploadRequest = async (options: anyObj) => {
  try {
    const result = await uploadFileToCos(options.file, {
      fileType: options.data?.fileType || 2,
      fileName: options.file?.name,
      onProgress: event => options.onProgress?.(event),
    })
    const response = {
      code: 200,
      data: result,
      message: '',
    }
    options.onSuccess?.(response)
    return response
  } catch (error) {
    options.onError?.(error)
    throw error
  }
}
