package update

import (
	"bufio"
	"encoding/hex"
	"fmt"
	"hash"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/dustin/go-humanize"
	"github.com/kardianos/osext"
	log "github.com/sirupsen/logrus"
	"github.com/tidwall/gjson"

	"github.com/sjkhsl/study_xxqg/conf"
)

// CheckUpdate 检查更新
func CheckUpdate(version string) string {
	log.Infof("正在检查更新.")
	if version == "(devel)" {
		log.Warnf("检查更新失败: 使用的 Actions 测试版或自编译版本.")
		return ""
	}
	if version == "unknown" {
		log.Warnf("检查更新失败: 使用的未知版本.")
		return ""
	}

	if !strings.HasPrefix(version, "v") {
		log.Warnf("版本格式错误")
		return ""
	}
	latest, err := lastVersion()
	if err != nil {
		log.Warnf("检查更新失败: %v", err)
		return ""
	}
	if strings.Contains(latest, "must") {
		log.Infoln("检测到强制更新，开始强制更新")
		SelfUpdate("", version)
		return ""
	}
	if versionCompare(version, latest) {
		log.Infof("当前有更新的 study_xxqg 可供更新, 请前往 https://github.com/sjkhsl/study_xxqg/releases 下载.")
		log.Infof("当前版本: %v 最新版本: %v", version, latest)
		return "检测到可用更新，版本号：" + latest
	}
	log.Infof("检查更新完成. 当前已运行最新版本.")
	return ""
}

func readLine() (str string) {
	console := bufio.NewReader(os.Stdin)
	str, _ = console.ReadString('\n')
	str = strings.TrimSpace(str)
	return
}

func lastVersion() (string, error) {
	response, err := http.Get("https://api.github.com/repos/sjkhsl/study_xxqg/releases/latest")
	if err != nil {
		return "", err
	}
	data, _ := io.ReadAll(response.Body)
	defer response.Body.Close()
	return gjson.GetBytes(data, "tag_name").Str, nil
}

//
//  versionCompare
//  @Description: 检测是否有更新，有返回true
//  @param nowVersion
//  @param lastVersion
//  @return bool
//
func versionCompare(nowVersion, lastVersion string) bool {
	nowVersion = strings.ReplaceAll(nowVersion, "must", "beta")
	lastVersion = strings.ReplaceAll(lastVersion, "must", "beta")

	NowBeta := strings.Contains(nowVersion, "beta")
	LastBeta := strings.Contains(lastVersion, "beta")

	// 获取主要版本号
	nowMainVersion := strings.Split(nowVersion, "-")
	lastMainVersion := strings.Split(lastVersion, "-")

	nowMainIntVersion, _ := strconv.Atoi(strings.ReplaceAll(strings.TrimLeft(nowMainVersion[0], "v"), ".", ""))
	lastMainIntVersion, _ := strconv.Atoi(strings.ReplaceAll(strings.TrimLeft(lastMainVersion[0], "v"), ".", ""))

	if nowMainIntVersion < lastMainIntVersion {
		return true
	}
	if strings.Contains(nowVersion, "SNAPSHOT") {
		if nowMainIntVersion == lastMainIntVersion {
			return false
		} else {
			return true
		}
	}
	// 如果最新版本是beta
	if LastBeta {
		// 如果当前版本也是beta
		if NowBeta {
			// 对beta后面的数字进行比较
			nowBetaVersion, _ := strconv.Atoi(strings.TrimLeft(nowMainVersion[1], "beta"))
			lastBetaVersion, _ := strconv.Atoi(strings.TrimLeft(lastMainVersion[1], "beta"))
			if nowBetaVersion < lastBetaVersion {
				return true
			}
			return false
			// 如果当前版本部署beta,需要更新
		} else {
			return true
		}
		// 最新版本不是beta,需要更新
	} else {
		return false
	}
}

func binaryName() string {
	goarch := runtime.GOARCH
	if goarch == "arm" {
		goarch += "v7"
	}
	ext := "tar.gz"
	if runtime.GOOS == "windows" {
		ext = "zip"
	}
	return fmt.Sprintf("study_xxqg_%v_%v.%v", runtime.GOOS, goarch, ext)
}

func checksum(github, version string) []byte {
	sumURL := fmt.Sprintf("%v/sjkhsl/study_xxqg/releases/download/%v/study_xxqg_checksums.txt", github, version)
	response, err := http.Get(sumURL)
	if err != nil {
		return nil
	}
	rd := bufio.NewReader(response.Body)
	for {
		str, err := rd.ReadString('\n')
		if err != nil {
			break
		}
		str = strings.TrimSpace(str)
		if strings.HasSuffix(str, binaryName()) {
			sum, _ := hex.DecodeString(strings.TrimSuffix(str, "  "+binaryName()))
			return sum
		}
	}
	return nil
}

func wait() {
	log.Info("按 Enter 继续....")
	readLine()
	os.Exit(0)
}

// SelfUpdate 自更新
func SelfUpdate(github string, version string) {
	github = conf.GetConfig().GithubProxy
	if github == "" {
		github = "https://github.com"
	}

	if version == "unknown" {
		log.Warningln("测试版本，不更新！")
		return
	}

	log.Infof("正在检查更新.")
	latest, err := lastVersion()
	if err != nil {
		log.Warnf("获取最新版本失败: %v", err)
		wait()
	}
	url := fmt.Sprintf("%v/sjkhsl/study_xxqg/releases/download/%v/%v", github, latest, binaryName())
	if version == latest {
		log.Info("当前版本已经是最新版本!")
		wait()
	}
	log.Info("当前最新版本为 ", latest)
	log.Info("正在更新,请稍等...")
	sum := checksum(github, latest)
	if sum != nil {
		err = update(url, sum)
		if err != nil {
			log.Error("更新失败: ", err)
		} else {
			log.Info("更新成功!")
		}
	} else {
		log.Error("checksum 失败!")
	}
}

// writeSumCounter 写入量计算实例
type writeSumCounter struct {
	total uint64
	hash  hash.Hash
}

// Write 方法将写入的byte长度追加至写入的总长度Total中
func (wc *writeSumCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.total += uint64(n)
	wc.hash.Write(p)
	fmt.Printf("\r                                    ")
	fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.total))
	return n, nil
}

// FromStream copy form getlantern/go-update
func fromStream(updateWith io.Reader) (err error, errRecover error) {
	updatePath, err := osext.Executable()
	if err != nil {
		return
	}

	// get the directory the executable exists in
	updateDir := filepath.Dir(updatePath)
	filename := filepath.Base(updatePath)
	// Copy the contents of of newbinary to a the new executable file
	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
	fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
	if err != nil {
		return
	}
	// We won't log this error, because it's always going to happen.
	defer func() { _ = fp.Close() }()
	if _, err = io.Copy(fp, bufio.NewReader(updateWith)); err != nil {
		log.Errorf("Unable to copy data: %v\n", err)
	}

	// if we don't call fp.Close(), windows won't let us move the new executable
	// because the file will still be "in use"
	if err := fp.Close(); err != nil {
		log.Errorf("Unable to close file: %v\n", err)
	}
	// this is where we'll move the executable to so that we can swap in the updated replacement
	oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))

	// delete any existing old exec file - this is necessary on Windows for two reasons:
	// 1. after a successful update, Windows can't remove the .old file because the process is still running
	// 2. windows rename operations fail if the destination file already exists
	_ = os.Remove(oldPath)

	// move the existing executable to a new file in the same directory
	err = os.Rename(updatePath, oldPath)
	if err != nil {
		return
	}

	// move the new executable in to become the new program
	err = os.Rename(newPath, updatePath)

	if err != nil {
		// copy unsuccessful
		errRecover = os.Rename(oldPath, updatePath)
	} else {
		// copy successful, remove the old binary
		_ = os.Remove(oldPath)
	}
	return
}
