/**
 * 更新发布工具
 * 
 * Copyright © 2025 imengyu.top imengyu-update-server
 */

import { confirm, select, input } from '@inquirer/prompts';
import { writeFile, readFile, access, unlink, stat, constants } from 'node:fs/promises';
import path from 'node:path';
import OSS from 'ali-oss';
import cliProgress from 'cli-progress';
import { selectVersion } from './index.mjs';
import { config } from './postConfig.mjs';
import { readFileRange, compressZip, execAsync } from './utils.mjs';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

//基础配置
//========================================

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const UPLOAD_APP_TYPE_LOCAL = 1;
const UPLOAD_APP_TYPE_ALI_OSS = 2;

async function getConfig() {
  let postConfig = {
    lastVersion: '',
    lastUpdateInfo: '',
    lastSubmitDay: '',
    lastTodaySubVersion: 0,
  };
  try {
    postConfig = JSON.parse(await readFile(path.resolve(__dirname, './_config.json')));
  } catch {
    //
  }
  if (postConfig.lastSubmitDay != new Date().getDate()) 
    postConfig.lastTodaySubVersion = 0;
  return postConfig;
}
async function saveConfig(postConfig) {
  postConfig.lastSubmitDay = new Date().getDate();
  await writeFile(path.resolve(__dirname, './_config.json'), JSON.stringify(postConfig));
}

async function getUpdateInfo(postConfig) {
  let updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
  if (updateInfo === 'git') {
    console.log('开始获取git提交信息');
    updateInfo = await execAsync('git log -1 --pretty=format:"%h %s"');
    console.log('使用git提交信息 "' + updateInfo + '" 作为更新信息');
  }
  return updateInfo;
}
async function uploadMulitPartLarge(axiosInstance, fileInfo, filePath) {

  console.log('开始分片上传');

  const multuploadInfo = (await axiosInstance.post('/update-large-token', {
    fileSize: fileInfo.size,
    fileName: path.basename(filePath)
  })).data;
  const chunkSize = multuploadInfo.splitPartSize;

  const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
  bar1.start(100, 0);

  for (let i = 0; i < multuploadInfo.allChunks; i++) {
    const start = i * chunkSize;
    const len = Math.min(start + chunkSize, fileInfo.size) - start;
    const uploadZipContent = await readFileRange(filePath, start, len);
    const subFormData = new FormData();
    subFormData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
    subFormData.append("key", multuploadInfo.key);
    subFormData.append("info", JSON.stringify({}));
    
    (await axiosInstance.post('/update-large', subFormData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    })).data;
    
    bar1.update(Math.floor(i / multuploadInfo.allChunks * 100));
  }

  bar1.update(100);
  bar1.stop();

  return multuploadInfo.key;
}
async function aliOSSMultipartUpload(client, fileName, uploadFile, progressCallback) {
  
  const fileInfo = await stat(uploadFile); // 获取文件信息
  const partSize = 10 * 1024 * 1024; // 分片大小，这里使用10MB
  const partCount = Math.ceil(fileInfo.size / partSize); // 计算总分片数
  
  console.log(`开始分片上传到阿里OSS，文件大小: ${(fileInfo.size / 1024 / 1024).toFixed(2)}MB，分片数: ${partCount}`);
  
  try {
    // 初始化分片上传
    const multipartUpload = await client.initMultipartUpload(fileName);
    const uploadId = multipartUpload.uploadId;
    
    // 上传分片
    const parts = [];
    for (let i = 0; i < partCount; i++) {
      const start = i * partSize;
      const len = Math.min(partSize, fileInfo.size - start);
      
      // 读取文件分片
      const fileData = await readFileRange(uploadFile, start, len);
      
      // 上传分片
      const partResult = await client.uploadPart(fileName, fileData, uploadId, i + 1);
      parts.push({
        ETag: partResult.res.headers.etag,
        PartNumber: i + 1
      });
      
      // 计算并回调进度
      const progress = (i + 1) / partCount;
      if (progressCallback) {
        progressCallback(progress);
      }
      
      console.log(`已上传分片 ${i + 1}/${partCount} (${(progress * 100).toFixed(2)}%)`);
    }
    
    // 完成分片上传
    await client.completeMultipartUpload(fileName, uploadId, parts);
    
    console.log('阿里OSS分片上传完成');
    
    // 获取上传后的文件URL
    const url = await client.generateObjectUrl(fileName);
    return url;
  } catch (error) {
    console.error('阿里OSS分片上传失败:', error);
    throw error;
  }
}

//App更新与提交
//========================================

export async function postAppUpdate(axiosInstance, param) {
  const postConfig = await getConfig();
  const { versionId, versionName } = await selectVersion(postConfig.lastVersion);
  const updateInfo = await getUpdateInfo(postConfig);

  const serverConfig = await axiosInstance.post('/update-post', { 
    config: { 
      type: 2, 
      test: true, 
      versionId, 
      uploadAppConfig: {}, 
      submitKey: config.submitKey 
    } 
  });

  postConfig.lastVersion = versionId;
  postConfig.lastUpdateInfo = updateInfo;
  postConfig.lastTodaySubVersion++;

  const updateSource = (await select({
    choices: [
      {
        name: '重新构建',
        value: 'rebuild',
      },
      {
        name: '已上传的阿里OSS文件路径',
        value: 'uploaded-alioss',
      },
    ],
    message: '选择上传来源',
    default: 'rebuild',
  }));

  const force = await confirm({ message: '强制更新？', default: false });
  const updateNext = await confirm({ message: '作为下个版本？', default: false });
  const versionCode = await config.buildAppGetVersion(versionName);

  await saveConfig(postConfig);

  if (updateSource === 'rebuild') 
    await config.buildAppCallback(param, versionCode, versionName, postConfig.lastTodaySubVersion);
  else if (updateSource === 'uploaded-alioss') {
    const fileName = await input({ message: '输入已上传的阿里OSS文件路径' });
    try {
      const result = (await axiosInstance.post('/update-post', {
        type: 2,
        ossConfig: {
          ossPath: fileName,
          ossPublic: '',
        },
        uploadAppConfig: {
          updateAsNext: updateNext,
        },
        versionId,
        updateInfo,
        versionCode: versionCode
      })).data;
      console.log('上传成功');
      console.log('新更新ID: ' + result.updateId);
    } catch (e) {
      console.error('上传失败', e);
    }
    return;
  }
  else {
    console.error('错误的选择');
    return;
  }

  const uploadFile = await config.buildAppGetUploadFile(param, versionCode, versionName);
  const fileName = `${await config.buildAppGetOSSFileName(param, versionCode, path.basename(uploadFile), uploadFile)}`;
  try {
    await access(uploadFile, constants.R_OK)
  } catch {
    console.error(`Failed to access ${uploadFile}, did you created it?`);
    return;
  }

  console.log('开始上传');

  //小于8mb则小文件上传，否则使用阿里OSS上传
  const fileInfo = await stat(uploadFile);
  if (fileInfo.size < 8 * 1024 * 1024) {
    const appData = (await readFile(uploadFile));

    const formData = new FormData();
    formData.append("file", new Blob([ appData ], { type : 'application/zip' }));
    formData.append("config", JSON.stringify({
      type: 2,
      versionId,
      versionCode,
      updateInfo,
      updateCustomConfig: await config.getCustomConfig(param, true, versionName),
      uploadAppConfig: { updateAsNext: updateNext },
      force,
      fileName,
    }));

    try {
      const result = (await axiosInstance.post('/update-post', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      })).data;
      console.log('上传成功');
      console.log('新更新ID: ' + result.updateId);
    } catch (e) {
      console.error('上传失败', e);
    }
  } else {

    let ossConfig;
    let multuploadedKey;

    if (serverConfig.uploadAppType === UPLOAD_APP_TYPE_LOCAL) {
      //本地大文件上传
      multuploadedKey = await uploadMulitPartLarge(axiosInstance, fileInfo, uploadFile);
    } else if (serverConfig.uploadAppType === UPLOAD_APP_TYPE_ALI_OSS) {
      //阿里OSS上传
      //请求STS进行临时授权
      const stsToken = (await axiosInstance.post('/update-ali-oss-sts')).data;
      const client = new OSS({
        region: stsToken.Region,
        accessKeyId: stsToken.AccessKeyId,
        accessKeySecret: stsToken.AccessKeySecret,
        stsToken: stsToken.SecurityToken,
        bucket: stsToken.Bucket,
        refreshSTSToken: async () => {
          const refreshToken = (await axiosInstance.get("/update-ali-oss-sts")).data;
          return {
            accessKeyId: refreshToken.AccessKeyId,
            accessKeySecret: refreshToken.AccessKeySecret,
            stsToken: refreshToken.SecurityToken,
          };
        },
      });
      
      //小于96mb则直接上传，否则分片上传

      console.log('Start upload to ali oss');

      let returnUrl = '';
      if (fileInfo.size < 96 * 1024 * 1024) {
        const result = await client.put(fileName, uploadFile);
        returnUrl = result.url;
      } else {
        const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
        bar1.start(100, 0);
        returnUrl = await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
          bar1.update(p * 100);
        });
        bar1.update(100);
        bar1.stop();
      }

      ossConfig = {
        ossPath: fileName,
        ossPublic: returnUrl,
      };

      console.log('Upload to ali oss done');
    } else {
      console.error('错误的 uploadAppType 配置 ' + serverConfig.uploadAppType);
    }
  
    try {
      const result = (await axiosInstance.post('/update-post', {
        config: {
          type: 2,
          ossConfig,
          multuploadedKey,
          uploadAppConfig: {
            updateAsNext: updateNext,
          },
          updateCustomConfig: await config.getCustomConfig(param, true, versionName),
          versionId,
          updateInfo,
          versionCode: versionCode,
          fileName
        },
      })).data;
      console.log('上传成功');
      console.log('新更新ID: ' + result.updateId);
    } catch (e) {
      console.error('上传失败', e);
    }
  }
}

//Web更新与提交
//========================================

export async function postWebUpdate(axiosInstance, param) {
  const skipBuild = param.skip
  const noDelete = param.ndelete;
  
  const postConfig = await getConfig();
  const { versionId, versionName } = await selectVersion(postConfig.lastVersion);
  const updateInfo = await getUpdateInfo(postConfig);

  postConfig.lastVersion = versionId;
  postConfig.lastUpdateInfo = updateInfo;
  postConfig.lastTodaySubVersion++;

  await axiosInstance.post('/update-post', { config: { type: 1, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });

  const now = new Date();
  const versionCode = await config.buildWebVersionGenerateCommand(now, postConfig.lastTodaySubVersion);

  if (config.buildWebOutVersionPath)
    await writeFile(path.resolve(__dirname, config.buildWebOutVersionPath), versionCode);

  await saveConfig(postConfig);
  
  if (!skipBuild && config.buildWebCommand) {
    console.log('正在执行构建...');
    await execAsync(config.buildWebCommand);
    console.log('构建完成');
  }

  const distDir = path.resolve(__dirname, config.buildWebOutDir);

  try {
    await access(distDir, constants.R_OK)
  } catch {
    console.error(`Failed to access ${distDir}`);
    return;
  }

  const outputPath = __dirname + '/upload.zip';
  const skipFiles = config?.buildWebOptions?.skipFiles ?? [];

  if (!skipBuild) {
    console.log('开始压缩zip...');
    await compressZip(distDir, outputPath, skipFiles);
  }

  console.log('开始上传zip');

  let success = false;

  //小于8mb则小文件上传，否则分片上传
  const fileInfo = await stat(outputPath);
  const formData = new FormData();
  const submitConfig = {
    type: 1,
    versionId,
    updateInfo,
    versionCode,
    uploadWebConfig: config.uploadWebConfig,
    updateCustomConfig: await config.getCustomConfig(param, true, versionName),
  }

  if (fileInfo.size < 8 * 1024 * 1024) {
    const uploadZipContent = await readFile(outputPath);
    formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
  } else {
    submitConfig.multuploadedKey = await uploadMulitPartLarge(axiosInstance, fileInfo, outputPath);
  }

  try {
    formData.append("config", JSON.stringify(submitConfig));
    const result = (await axiosInstance.post('/update-post', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    })).data;
    console.log('上传成功');
    console.log('新更新ID: ' + result.updateId);
    success = true;
  } catch (e) {
    console.error('上传失败', e);
  }
  if (!success || noDelete) 
    return;
    
  console.log('删除zip');
  await unlink(outputPath);
}