import { AppUtil, FileUtil, JSONUtil, StrUtil } from '@pura/harmony-utils';
import { MediaEntity, VideoMaterial } from '../entity/MaterialInfoEntity';
import url from '@ohos.url';
import { Constants } from '../common/Constants';
import axios, { AxiosHeaders, AxiosResponse, AxiosResponseHeaders } from '@ohos/axios';
import { BusinessError, request } from '@kit.BasicServicesKit';
import { MP4Parser } from '@ohos/mp4parser';
import { JSON } from '@kit.ArkTS';

export interface DownloadCallback {
  onGetTotal?:(total: number) => void;
  onProgress?:(progress: number) => void;
  onPause?:() => void;
  onMerge?:(step: number) => void;
  onSuccess?:(filePath: string) => void;
  onCancel?:() => void;
  onFailed?:(err: string) => void;
}

export class MediaDownloader {
  static getInstance(): MediaDownloader {
    return new MediaDownloader();
  }

  /**
   * 缓存文件夹
   */
  private cacheDir = FileUtil.getCacheDirPath() + FileUtil.separator;

  /**
   * 任务对象
   */
  private task?: request.agent.Task | null

  /**
   * 下载回调
   */
  private mCallback?: DownloadCallback;

  /**
   * 文件名
   */
  private fileName = '';

  /**
   * 代理池
   */
  private proxyList = new Array<string>();

  callback(callback: DownloadCallback): MediaDownloader {
    this.mCallback = callback;
    return this;
  }

  createHeaders(map?: Map<string, Object>): Record<string, string> {
    const headers: Record<string, string> = {};
    map?.forEach((value, key) => {
      headers[key] = value.toString();
    })
    return headers;
  }

  setProxyList(list?: Array<string>): MediaDownloader {
    if (list && list.length > 0) {
      this.proxyList = list
    }
    return this
  }

  async down(media: MediaEntity) {
    this.fileName = media.name;
    if (media.isM3u8) {
      // this.doM3u8Down();
      this.doException('暂不支持')
    } else if (media.isThreading) {
      // this.doThreadingDown();
      this.doMediaDown(media, media.isMerge ? 1 : 0);
    } else {
      this.doMediaDown(media, media.isMerge ? 1 : 0);
    }
  }

  async doMediaDown(media: MediaEntity, step: number) {
    if (media instanceof VideoMaterial && step === 0) {
      await this.decodeWxVideo(media)
    }

    let url = media.origin;
    if (step === 2 && media instanceof VideoMaterial) {
      url = media.speed_up === 1 || media.isThreading ? media.audio!!.url : media.audio!!.origin_url;
    } else {
      url = media.speed_up === 1 || media.isThreading ? media.url : media.origin_url;
    }

    let headers: Record<string, string> = {}
    if (media.headers) {
      headers = this.createHeaders(JSONUtil.jsonToMap(JSON.stringify(media.headers)));
    }
    headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0'
    headers['Accept'] = '*/*'

    let isCanContinue = false;
    /*if (step === 0 || step === 1) {
      isCanContinue = await this.isCanContinue(url, media.currentLen)
      media.continueDownload = true;
    } else if (step === 2 && media instanceof VideoMaterial && media.audio) {
      isCanContinue = await this.isCanContinue(url, media.audio.currentLen)
      media.continueDownload = true;
    }*/

    if (step === 0 && FileUtil.accessSync(this.cacheDir + this.fileName) && isCanContinue) {
      headers['Range'] = `bytes=${media.currentLen}-`;
    } else if (step == 1 && FileUtil.accessSync(this.cacheDir + media.cacheName) &&
      (isCanContinue || media.totalSize !== 0 && media.currentLen === media.totalSize)) {
      if (media instanceof VideoMaterial && media.audio && media.totalSize !== 0 && media.currentLen === media.totalSize) {
        this.doMerge(4) // 音频提取中
        this.doMediaDown(media, 2)
      } else {
        headers['Range'] = `bytes=${media.currentLen}-`;
      }
    } else if (step === 2 && media instanceof VideoMaterial && media.audio && FileUtil.accessSync(this.cacheDir + media.audio.cacheName) &&
      (isCanContinue || media.audio.totalSize !== 0 && media.audio.currentLen === media.audio.totalSize)) {
      if (media.audio.totalSize !== 0 && media.audio.currentLen == media.audio.totalSize) {
        this.doMerge(5) // 音视频合并中
        let videoPath = this.cacheDir + media.cacheName
        let audioPath = this.cacheDir + media.audio.cacheName
        if (FileUtil.accessSync(videoPath) && FileUtil.accessSync(audioPath)) {
          this.mergeVideoAndAudio(videoPath, audioPath, url)
        } else {
          throw Error('file not exists')
        }
      } else {
        headers['Range'] = `bytes=${media.audio.currentLen}-`;
      }
    } else {
      if (step === 0 || step === 1) {
        media.currentLen = 0
      } else if (step === 2 && media instanceof VideoMaterial && media.audio) {
        media.audio.currentLen = 0
      }
    }

    if (this.canceled) return
    try {
      let filePath = ''
      if (step == 1 && media instanceof VideoMaterial) {
        filePath = this.cacheDir + media.cacheName
      } else if (step == 2 && media instanceof VideoMaterial) {
        filePath = this.cacheDir + media.audio!!.cacheName
      } else {
        filePath = this.cacheDir + this.fileName
      }
      let config: request.agent.Config  =  {
        action: request.agent.Action.DOWNLOAD,
        url: url,
        overwrite: true,
        saveas: filePath,
        headers: headers,
        gauge: true,
        priority:0
      }
      let total = 0;
      request.agent.create(AppUtil.getApplicationContext(), config)
        .then((task: request.agent.Task) => {
          this.task = task
          task.start((err: BusinessError) => {
            if (err) {
              console.error(err.message);
              this.doException(err.message);
              return;
            }
          });
          task.on('progress', (progress) => {
            if (total === 0) {
              total = progress.sizes[0] as number;
              if (step === 0 || step === 1) {
                total += media.currentLen
                media.totalSize = total
              } else if (step === 2 && media instanceof VideoMaterial && media.audio) {
                total += media.audio.currentLen
                media.audio.totalSize = total
              }
              this.doGetTotal(total);
              console.debug(`total: ${progress.sizes[0].toString()}`)
            }
            if (step === 0 || step === 1) {
              media.currentLen = progress.processed
            } else if (step === 2 && media instanceof VideoMaterial && media.audio) {
              media.audio.currentLen = progress.processed
            }
            this.doProgress(progress.processed);
            console.debug(`progress: ${progress.processed.toString()}`);
          })
          task.on('completed', () => {
            console.debug('download completed');
            if (step == 1 && media instanceof VideoMaterial && media.audio) {
              this.doMerge(4) // 音频提取中
              this.doMediaDown(media, 2)
            } else if (step == 2 && media instanceof VideoMaterial) {
              this.doMerge(5) // 音视频合并中
              let videoPath = this.cacheDir + media.cacheName
              let audioPath = this.cacheDir + media.audio!!.cacheName
              if (FileUtil.accessSync(videoPath) && FileUtil.accessSync(audioPath)) {
                this.mergeVideoAndAudio(videoPath, audioPath, url)
              } else {
                this.doException('文件不存在')
              }
            } else {
              this.doSuccess(this.cacheDir + this.fileName);
            }
            request.agent.remove(task.tid);
          })
          task.on('failed', (progress) => {
            console.log('download failed');
            request.agent.show(task.tid).then((taskInfo: request.agent.TaskInfo) => {
              request.agent.remove(task.tid);
              if (this.proxyList.length > 0 && this.replaceUrlWithProxy(media)) {
                this.doMediaDown(media, step)
              }  else {
                this.doException(taskInfo.reason);
              }
            }).catch((err: BusinessError) => {
              this.doException(err.message);
              request.agent.remove(task.tid);
            });
          });
        })
        .catch((err: BusinessError) => {
          console.error(err.message);
          this.doException(err.message);
      })
    } catch (e) {
      console.error(e);
      this.doException(e);
    }
  }

  async mergeVideoAndAudio(videoPath: string, audioPath: string, url: string) {
    let outputPath = this.cacheDir + this.fileName
    if (FileUtil.accessSync(outputPath)) {
      FileUtil.unlinkSync(outputPath)
    }
    let cmd = `ffmpeg -i ${videoPath} -i ${audioPath} -c copy ${outputPath}`
    MP4Parser.ffmpegCmd(cmd, {
      callBackResult: (code: number) => {
        if (code === 0) {
          this.doSuccess(outputPath)
        } else {
          this.doException('合并失败')
        }
      }
    })
  }

  async fixM4s(filePath: string) {
    const file = FileUtil.openSync(filePath, 0o2);
    const stat = FileUtil.lstatSync(filePath);
    const buffer = new ArrayBuffer(stat.size);
    FileUtil.readSync(file.fd, buffer);
    FileUtil.fsyncSync(file.fd);
    FileUtil.closeSync(file.fd);

    let bytes = new Uint8Array(buffer)
    if (bytes.length > 9) {
      bytes = bytes.subarray(9)
      FileUtil.writeEasy(filePath, bytes.buffer, false)
    }
  }

  async decodeWxVideo(video: VideoMaterial) {
    if (!video.url.includes("wechatDecode=batiao")) {
      return
    }
    try {
      const parseUrl = url.URL.parseURL(video.url);
      const realUrl = parseUrl.params.get('url') as string;
      const decodeKey = parseUrl.params.get("decodeKey");
      const keyUrl = Constants.BASE_URL + "/api/weixin/video/key?decodeKey=" + decodeKey;
      const response: AxiosResponse = await axios({
        url: keyUrl,
        method: 'get',
        responseType: 'array_buffer',
      })
      if (response.status === 200) {
        video.url = realUrl;
        video.origin_url = realUrl;
        video.decodeKey = new Int8Array(response.data)
      }
    } catch (e) {
      console.error(e);
    }
  }

  async isCanContinue(url: string, start: number): Promise<boolean> {
    try {
      const headers = new AxiosHeaders();
      headers.set('RANGE', `bytes=${start}-`);
      headers.set('Connection', 'close');
      const response: AxiosResponse = await axios({
        url: url,
        method: 'get',
        headers: headers,
        responseType: 'array_buffer'
      })
      if (response.status === 206) { //支持
        return Promise.resolve(true);
      } else {
        const acceptRangesHeaderValue = (response.headers as AxiosResponseHeaders).get('Accept-Ranges') as string
        if ("bytes" === acceptRangesHeaderValue?.toLowerCase()) { //支持
          return Promise.resolve(true);
        } else { //不支持
          return Promise.resolve(false);
        }
      }
    } catch (e) {
      console.log(e);
      return Promise.resolve(false);
    }
  }

  replaceUrlWithProxy(media: MediaEntity): boolean {
    try {
      let vUrl = url.URL.parseURL(media.url)

      let domainAndPort = vUrl?.host ? vUrl?.host : ''
      for (let i = 0; i < this.proxyList.length; i++) {
        const proxy = this.proxyList[i];
        if (i < this.proxyList.length - 1) {
          if (proxy.includes(domainAndPort)) {
            domainAndPort = this.proxyList[i + 1]
            break
          }
        } else return false
      }

      // 获取原始 URL 的路径和查询参数
      media.url = domainAndPort + vUrl?.pathname + vUrl?.search
      if (media instanceof VideoMaterial && media.audio) {
        let aUrl = url.URL.parseURL(media.audio.url)
        media.audio!!.url = domainAndPort + aUrl?.pathname + aUrl?.search
      }
      return true
    } catch (e) {
      console.error(e)
      return false
    }
  }

  getDomainFromUrl(urlString: string): string {
    try {
      let parseURL = url.URL.parseURL(urlString)
      return parseURL.protocol + "//" + parseURL.host
    } catch (e) {
      return ''
    }
  }

  getUrlFileExt(srcUrl: string): string {
    try {
      let url = srcUrl.substring(0, srcUrl.indexOf('?'))
      if (url) {
        return url.substring(url.lastIndexOf('.') + 1)
      }
    } catch (e) {
      console.error(e)
    }
    return ''
  }

  private canceled = false
  async cancel() {
    this.canceled = true
    if (this.task) {
      this.task.stop()
        .then(() => {
          this.task = null
          this.doCancel()
        })
        .catch((e: Error) => {
          this.doException(e.message)
        })
    }
  }

  doGetTotal(total: number) {
    if (this.mCallback?.onGetTotal) {
      this.mCallback.onGetTotal(total);
    }
  }

  doProgress(progress: number) {
    if (this.mCallback?.onProgress) {
      this.mCallback.onProgress(progress);
    }
  }

  doPause() {
    if (this.mCallback?.onPause) {
      this.mCallback.onPause();
    }
  }

  doMerge(step: number) {
    if (this.mCallback?.onMerge) {
      this.mCallback.onMerge(step);
    }
  }

  doSuccess(filePath: string) {
    if (this.mCallback?.onSuccess) {
      this.mCallback.onSuccess(filePath)
    }
  }

  doCancel() {
    if (this.mCallback?.onCancel) {
      this.mCallback.onCancel();
    }
  }

  doException(err: string) {
    if (this.mCallback?.onFailed) {
      this.mCallback.onFailed(err);
    }
  }
}