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

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

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

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

function readFileRange(file, start, length) {
  return new Promise((resolve, reject) => {
    fs.open(file, 'r', (err, fd) => {
      if (err) {
        reject('Error opening file:', er);
        return;
      }
      const buffer = Buffer.alloc(length);
      fs.read(fd, buffer, 0, length, start, (err, bytesRead, buffer) => {
        if (err) {
          reject('Error reading file:', err)
          return;
        }
        fs.close(fd, (err) => {
          if (err) {
            reject('Error closing file:', err)
            return ;
          }
          resolve(buffer);
        });
      });
    });
  })
  
}
export function pad(num, n) {
  var strNum = num.toString();
  var len = strNum.length;
  while (len < n) {
    strNum = "0" + strNum;
    len++;
  }
  return strNum;
}
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 new Promise((resolve, reject) => {
      exec('git log -1 --pretty=format:"%h %s"', (error, stdout, stderr) => {
        if (error) {
          reject(error);
          return;
        }
        resolve(stdout);
      });
    });
    console.log('使用git提交信息作为更新信息');
  }
  return updateInfo;
}

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

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

  await axiosInstance.post('/update-post', { config: { type: 2, test: true, versionId, uploadWebConfig: config.uploadWebConfig, 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 = hotfix ? false : await confirm({ message: '作为下个版本？', default: false });
  const versionCode = config.buildAppGetVersion();

  await saveConfig(postConfig);

  if (updateSource === 'rebuild') 
    await config.buildAppCallback(param, versionCode, 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);
  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("type", 2);
    formData.append("versionId", versionId);
    formData.append("updateInfo", updateInfo);
    formData.append("uploadAppConfig", { updateAsNext: updateNext });
    formData.append("force", force);
    formData.append("versionCode", versionCode);

    try {
      const result = (await axiosInstance.post('/update-post', formData, {
        headers: { 'Content-Type': 'multipart/form-data' }
      })).data;
      console.log('上传成功');
      console.log('新更新ID: ' + result.updateId);
      console.log('删除构建文件');

      await unlink(uploadFile);
    } catch (e) {
      console.error('上传失败', e);
    }

  } else {
    //阿里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则直接上传，否则分片上传
    const fileName = `/${await config.buildAppGetOSSFileName(param)}`;

    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);
      await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
        bar1.update(p * 100);
      });
      bar1.stop();
    }

    console.log('Upload to ali oss done');
  
    try {
      const result = (await axiosInstance.post('/update-post', {
        type: 2,
        ossConfig: {
          ossPath: fileName,
          ossPublic: returnUrl,
        },
        uploadAppConfig: {
          updateAsNext: updateNext,
        },
        versionId,
        updateInfo,
        versionCode: versionCode
      })).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 = await selectVersion(false, 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 new Promise((resolve, reject) => {
      exec(config.buildWebCommand, function(err, stdout) {
        if (err)
          reject(err);
        else 
          resolve();
      });
    });

    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';

  if (!skipBuild) {
    console.log('开始压缩zip...');

    const output = fs.createWriteStream(outputPath);
    const archive = archiver('zip', {
      zlib: { level: 9 } // Sets the compression level.
    });
    archive.pipe(output);

    const files = await readdir(distDir);
    for (const file of files) {
      const filestat = await stat(distDir + '/' + file);
      if (filestat.isDirectory()) {
        archive.directory(distDir + '/' + file, file);
      } else {
        archive.file(distDir + '/' + file, { name: file });
      }
    }

    console.log('等待压缩zip...');

    const waitArchive = new Promise((resolve, reject) => {
      output.on('close', function() {
        console.log(archive.pointer() + ' total bytes');
        console.log('archiver has been finalized and the output file descriptor has closed.');
        resolve();
      });
      archive.on('error', function(err) {
        reject(err);
      });
    })

    await archive.finalize();
    await waitArchive;
  }

  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,
  }

  if (fileInfo.size < 8 * 1024 * 1024) {
    const uploadZipContent = await readFile(outputPath);
    formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
  } else {
    const multuploadInfo = (await axiosInstance.post('/update-large-token', {
      fileSize: fileInfo.size,
      fileName: path.basename(outputPath)
    })).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(outputPath, 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.stop();

    submitConfig.multuploadedKey = multuploadInfo.key;
  }

  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);
}