import axios from 'axios'
import { ipcMain, IpcMainEvent } from 'electron'
import FormData from 'form-data'
import fs from 'fs-extra'
import path from 'path'
import Upyun from 'upyun'

import windowManager from 'apis/app/window/windowManager'

import {
  md5,
  hmacSha1Base64,
  getFileMimeType,
  NewDownloader,
  gotUpload,
  ConcurrencyPromisePool,
  formatError
} from '~/manage/utils/common'
import { ManageLogger } from '~/manage/utils/logger'
import UpDownTaskQueue from '~/manage/datastore/upDownTaskQueue'

import { commonTaskStatus, IWindowList } from '#/types/enum'
import { isImage } from '#/utils/common'
import { cancelDownloadLoadingFileList, refreshDownloadFileTransferList } from '#/utils/static'

class UpyunApi {
  ser: Upyun.Service
  cli: Upyun.Client
  bucket: string
  operator: string
  password: string
  antiLeechToken: string
  expireTime: number
  stopMarker = 'g2gCZAAEbmV4dGQAA2VvZg'
  logger: ManageLogger

  constructor(
    bucket: string,
    operator: string,
    password: string,
    logger: ManageLogger,
    antiLeechToken?: string,
    expireTime?: number
  ) {
    this.ser = new Upyun.Service(bucket, operator, password)
    this.cli = new Upyun.Client(this.ser)
    this.bucket = bucket
    this.operator = operator
    this.password = password
    this.logger = logger
    this.antiLeechToken = antiLeechToken || ''
    this.expireTime = expireTime || 24 * 60 * 60
  }

  getAntiLeechParam(key: string): string {
    const uri = `/${key}`.replace(/%2F/g, '/').replace(/^\/+/g, '/')
    const now = Math.round(new Date().getTime() / 1000)
    const expire = this.expireTime ? now + parseInt(this.expireTime.toString(), 10) : now + 1800
    const sign = md5(`${this.antiLeechToken}&${expire}&${uri}`, 'hex')
    const upt = `${sign.substring(12, 20)}${expire}`
    return `_upt=${upt}`
  }

  formatFolder(item: any, slicedPrefix: string, urlPrefix: string) {
    const key = `${slicedPrefix}${item.name}/`
    let url = `${urlPrefix}/${key}`
    if (this.antiLeechToken) {
      url = `${url}?${this.getAntiLeechParam(key)}`
    }
    return {
      ...item,
      key,
      url,
      fileSize: 0,
      formatedTime: '',
      fileName: item.name,
      isDir: true,
      checked: false,
      isImage: false,
      match: false,
      Key: key
    }
  }

  formatFile(item: any, slicedPrefix: string, urlPrefix: string) {
    const key = `${slicedPrefix}${item.name}`
    let url = `${urlPrefix}/${key}`
    if (this.antiLeechToken) {
      url = `${url}?${this.getAntiLeechParam(key)}`
    }
    return {
      ...item,
      fileName: item.name,
      fileSize: item.size,
      formatedTime: new Date(parseInt(item.time) * 1000).toLocaleString(),
      isDir: false,
      checked: false,
      match: false,
      isImage: isImage(item.name),
      url,
      key
    }
  }

  authorization(method: string, uri: string, contentMd5: string, operator: string, password: string) {
    return `UPYUN ${operator}:${hmacSha1Base64(
      md5(password, 'hex'),
      `${method.toUpperCase()}&${encodeURI(uri)}&${new Date().toUTCString()}${contentMd5 ? `&${contentMd5}` : ''}`
    )}`
  }

  /**
   * 获取空间列表
   */
  async getBucketList(): Promise<any> {
    return this.bucket
  }

  async getBucketListRecursively(configMap: IStringKeyMap): Promise<any> {
    const window = windowManager.get(IWindowList.SETTING_WINDOW)!
    const { bucketName: bucket, prefix, cancelToken } = configMap
    const slicedPrefix = prefix.slice(1)
    const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net`
    const cancelTask = [false]
    ipcMain.on(cancelDownloadLoadingFileList, (_: IpcMainEvent, token: string) => {
      if (token === cancelToken) {
        cancelTask[0] = true
        ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
      }
    })
    let res = {} as any
    const result = {
      fullList: <any>[],
      success: false,
      finished: false
    }
    const folderQueue = [prefix]
    const getFolderFile = async (folder: any) => {
      let marker = ''
      const key = folder
      do {
        res = await this.cli.listDir(key, {
          limit: 10000,
          iter: marker
        })
        if (res) {
          res.files?.forEach((item: any) => {
            item.type === 'F' && folderQueue.push(`${slicedPrefix}${item.name}/`)
            item.type === 'N' && result.fullList.push(this.formatFile(item, folder, urlPrefix))
          })
          window.webContents.send(refreshDownloadFileTransferList, result)
        } else {
          result.finished = true
          window.webContents.send(refreshDownloadFileTransferList, result)
          ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
          return
        }
        marker = res.next
      } while (!cancelTask[0] && res.next !== this.stopMarker)
    }
    while (folderQueue.length) {
      const folder = folderQueue.shift()
      await getFolderFile(folder)
    }
    result.success = !cancelTask[0]
    result.finished = true
    window.webContents.send(refreshDownloadFileTransferList, result)
    ipcMain.removeAllListeners(cancelDownloadLoadingFileList)
  }

  async getBucketListBackstage(configMap: IStringKeyMap): Promise<any> {
    const window = windowManager.get(IWindowList.SETTING_WINDOW)!
    const { bucketName: bucket, prefix, cancelToken } = configMap
    const slicedPrefix = prefix.slice(1)
    const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net`
    let marker = ''
    const cancelTask = [false]
    ipcMain.on('cancelLoadingFileList', (_: IpcMainEvent, token: string) => {
      if (token === cancelToken) {
        cancelTask[0] = true
        ipcMain.removeAllListeners('cancelLoadingFileList')
      }
    })
    let res = {} as any
    const result = {
      fullList: <any>[],
      success: false,
      finished: false
    }
    do {
      res = await this.cli.listDir(prefix, {
        limit: 10000,
        iter: marker
      })
      if (res) {
        res.files?.forEach((item: any) => {
          item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
          item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
        })
        window.webContents.send('refreshFileTransferList', result)
      } else {
        result.finished = true
        window.webContents.send('refreshFileTransferList', result)
        ipcMain.removeAllListeners('cancelLoadingFileList')
        return
      }
      marker = res.next
    } while (!cancelTask[0] && res.next !== this.stopMarker)
    result.success = !cancelTask[0]
    result.finished = true
    window.webContents.send('refreshFileTransferList', result)
    ipcMain.removeAllListeners('cancelLoadingFileList')
  }

  /**
   * 获取文件列表
   * @param {Object} configMap
   * configMap = {
   *  bucketName: string,
   *  bucketConfig: {
   *   Location: string
   * },
   *  paging: boolean,
   *  prefix: string,
   *  marker: string,
   *  itemsPerPage: number,
   *  customUrl: string
   * }
   */
  async getBucketFileList(configMap: IStringKeyMap): Promise<any> {
    const { bucketName: bucket, prefix, marker, itemsPerPage } = configMap
    const slicedPrefix = prefix.slice(1)
    const urlPrefix = configMap.customUrl || `http://${bucket}.test.upcdn.net`
    let res = {} as any
    const result = {
      fullList: <any>[],
      isTruncated: false,
      nextMarker: '',
      success: false
    }
    res = await this.cli.listDir(prefix, {
      limit: itemsPerPage,
      iter: marker || ''
    })
    if (res) {
      res.files?.forEach((item: any) => {
        item.type === 'N' && result.fullList.push(this.formatFile(item, slicedPrefix, urlPrefix))
        item.type === 'F' && result.fullList.push(this.formatFolder(item, slicedPrefix, urlPrefix))
      })
      result.isTruncated = res.next !== this.stopMarker
      result.nextMarker = res.next
      result.success = true
    }
    return result
  }

  /**
   * 重命名文件
   * @param configMap
   * configMap = {
   * bucketName: string,
   * region: string,
   * oldKey: string,
   * newKey: string
   * }
   */
  async renameBucketFile(configMap: IStringKeyMap): Promise<boolean> {
    const oldKey = configMap.oldKey
    let newKey = configMap.newKey
    const method = 'PUT'
    if (newKey.endsWith('/')) {
      newKey = newKey.slice(0, -1)
    }
    const xUpyunMoveSource = `/${this.bucket}/${oldKey}`
    const uri = `/${this.bucket}/${newKey}`
    const authorization = this.authorization(method, uri, '', this.operator, this.password)
    const headers = {
      Authorization: authorization,
      'X-Upyun-Move-Source': xUpyunMoveSource,
      'Content-Length': 0,
      Date: new Date().toUTCString()
    }
    const res = await axios({
      method,
      url: `http://v0.api.upyun.com${uri}`,
      headers
    })
    return res.status === 200
  }

  /**
   * 删除文件
   * @param configMap
   * configMap = {
   * bucketName: string,
   * region: string,
   * key: string
   * }
   */
  async deleteBucketFile(configMap: IStringKeyMap): Promise<boolean> {
    const { key } = configMap
    const res = await this.cli.deleteFile(key)
    return res
  }

  /**
   * delete bucket folder
   * @param configMap
   */
  async deleteBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
    const { key } = configMap
    let marker = ''
    let isTruncated
    const allFileList = {
      CommonPrefixes: [] as any[],
      Contents: [] as any[]
    }
    do {
      const res = await this.cli.listDir(key, {
        limit: 10000,
        iter: marker
      })
      if (res) {
        res.files.forEach((item: any) => {
          item.type === 'N' &&
            allFileList.Contents.push({
              ...item,
              key: `${key}${item.name}`
            })
          item.type === 'F' &&
            allFileList.CommonPrefixes.push({
              ...item,
              key: `${key}${item.name}/`
            })
        })
        marker = res.next
        isTruncated = res.next !== this.stopMarker
      } else {
        return false
      }
    } while (isTruncated)
    if (allFileList.Contents.length > 0) {
      let success = false
      for (let i = 0; i < allFileList.Contents.length; i++) {
        const item = allFileList.Contents[i]
        success = await this.cli.deleteFile(item.key)
        if (!success) {
          return false
        }
      }
    }
    if (allFileList.CommonPrefixes.length > 0) {
      for (const item of allFileList.CommonPrefixes) {
        const res = await this.deleteBucketFolder({
          key: item.key
        })
        if (!res) {
          return false
        }
      }
    }
    const deleteSelf = await this.cli.deleteFile(key)
    if (!deleteSelf) {
      return false
    }
    return true
  }

  /**
   * upload file to bucket
   * axiso:onUploadProgress not work in nodejs , use got instead
   * @param configMap
   */
  async uploadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
    const { fileArray } = configMap
    const instance = UpDownTaskQueue.getInstance()
    fileArray.forEach((item: any) => {
      item.key = item.key.replace(/^\/+/, '')
    })
    for (const item of fileArray) {
      const { bucketName, region, key, filePath, fileName, fileSize } = item
      const id = `${bucketName}-${region}-${key}-${filePath}`
      if (instance.getUploadTask(id)) {
        continue
      }
      instance.addUploadTask({
        id,
        progress: 0,
        status: commonTaskStatus.queuing,
        sourceFileName: fileName,
        sourceFilePath: filePath,
        targetFilePath: key,
        targetFileBucket: bucketName,
        targetFileRegion: region
      })
      const date = new Date().toUTCString()
      const uri = `/${key}`
      const method = 'POST'
      const uplpadPolicy = {
        bucket: bucketName,
        'save-key': uri,
        expiration: Math.floor(Date.now() / 1000) + 2592000,
        date,
        'content-length': fileSize
      }
      const base64Policy = Buffer.from(JSON.stringify(uplpadPolicy)).toString('base64')
      const stringToSign = `${method}&/${bucketName}&${date}&${base64Policy}`
      const signature = hmacSha1Base64(md5(this.password, 'hex'), stringToSign)
      const authorization = `UPYUN ${this.operator}:${signature}`
      const form = new FormData()
      form.append('policy', base64Policy)
      form.append('authorization', authorization)
      form.append('file', fs.createReadStream(filePath), {
        filename: path.basename(key),
        contentType: getFileMimeType(fileName)
      })
      const headers = form.getHeaders()
      headers.Host = 'v0.api.upyun.com'
      headers.Date = date
      headers.Authorization = authorization
      gotUpload(instance, `http://v0.api.upyun.com/${bucketName}`, method, form, headers, id, this.logger)
    }
    return true
  }

  /**
   * 新建文件夹
   * @param configMap
   */
  async createBucketFolder(configMap: IStringKeyMap): Promise<boolean> {
    const { key } = configMap
    const res = await this.cli.makeDir(`/${key}`)
    return res
  }

  /**
   * 下载文件
   * @param configMap
   */
  async downloadBucketFile(configMap: IStringKeyMap): Promise<boolean> {
    const { downloadPath, fileArray, maxDownloadFileCount } = configMap
    const instance = UpDownTaskQueue.getInstance()
    const promises = [] as any
    for (const item of fileArray) {
      const { bucketName, region, key, fileName, customUrl } = item
      const savedFilePath = path.join(downloadPath, fileName)
      const id = `${bucketName}-${region}-${key}`
      if (instance.getDownloadTask(id)) {
        continue
      }
      instance.addDownloadTask({
        id: `${bucketName}-${region}-${key}`,
        progress: 0,
        status: commonTaskStatus.queuing,
        sourceFileName: fileName,
        targetFilePath: savedFilePath
      })
      const preSignedUrl = `${customUrl}/${key}`
      promises.push(
        () =>
          new Promise((resolve, reject) => {
            NewDownloader(instance, preSignedUrl, id, savedFilePath, this.logger).then((res: boolean) => {
              if (res) {
                resolve(res)
              } else {
                reject(res)
              }
            })
          })
      )
    }
    const pool = new ConcurrencyPromisePool(maxDownloadFileCount)
    pool.all(promises).catch(error => {
      this.logger.error(formatError(error, { class: 'UpyunApi', method: 'downloadBucketFile' }))
    })
    return true
  }
}

export default UpyunApi
