// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package model

import (
	"bufio"
	"crypto/sha256"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/88250/gulu"
	"github.com/imroc/req/v3"
	"github.com/siyuan-note/logging"
	"github.com/siyuan-note/siyuan/kernel/util"
)

func execNewVerInstallPkg(newVerInstallPkgPath string) {
	logging.LogInfof("installing the new version [%s]", newVerInstallPkgPath)
	var cmd *exec.Cmd
	if gulu.OS.IsWindows() {
		cmd = exec.Command(newVerInstallPkgPath)
	} else if gulu.OS.IsDarwin() {
		exec.Command("chmod", "+x", newVerInstallPkgPath).CombinedOutput()
		cmd = exec.Command("open", newVerInstallPkgPath)
	}
	gulu.CmdAttr(cmd)
	cmdErr := cmd.Run()
	if nil != cmdErr {
		logging.LogErrorf("exec install new version failed: %s", cmdErr)
		return
	}
}

var newVerInstallPkgPath string

func getNewVerInstallPkgPath() string {
	if skipNewVerInstallPkg() {
		newVerInstallPkgPath = ""
		return ""
	}

	downloadPkgURLs, checksum, err := getUpdatePkg()
	if err != nil || 1 > len(downloadPkgURLs) || "" == checksum {
		newVerInstallPkgPath = ""
		return ""
	}

	pkg := path.Base(downloadPkgURLs[0])
	newVerInstallPkgPath = filepath.Join(util.TempDir, "install", pkg)
	localChecksum, _ := sha256Hash(newVerInstallPkgPath)
	if checksum != localChecksum {
		newVerInstallPkgPath = ""
		return ""
	}
	return newVerInstallPkgPath
}

var checkDownloadInstallPkgLock = sync.Mutex{}

func checkDownloadInstallPkg() {
	defer logging.Recover()

	if skipNewVerInstallPkg() {
		return
	}

	if !checkDownloadInstallPkgLock.TryLock() {
		return
	}
	defer checkDownloadInstallPkgLock.Unlock()

	downloadPkgURLs, checksum, err := getUpdatePkg()
	if err != nil || 1 > len(downloadPkgURLs) || "" == checksum {
		return
	}

	msgId := util.PushMsg(Conf.Language(103), 1000*7)
	succ := false
	for _, downloadPkgURL := range downloadPkgURLs {
		err = downloadInstallPkg(downloadPkgURL, checksum)
		if err == nil {
			succ = true
			break

		}
	}
	if !succ {
		util.PushUpdateMsg(msgId, Conf.Language(104), 7000)
	}
}

func getUpdatePkg() (downloadPkgURLs []string, checksum string, err error) {
	defer logging.Recover()
	result, err := util.GetRhyResult(false)
	if err != nil {
		return
	}

	ver := result["ver"].(string)
	if isVersionUpToDate(ver) {
		return
	}

	var suffix string
	if gulu.OS.IsWindows() {
		if "arm64" == runtime.GOARCH {
			suffix = "win-arm64.exe"
		} else {
			suffix = "win.exe"
		}
	} else if gulu.OS.IsDarwin() {
		if "arm64" == runtime.GOARCH {
			suffix = "mac-arm64.dmg"
		} else {
			suffix = "mac.dmg"
		}
	}
	pkg := "siyuan-" + ver + "-" + suffix

	b3logURL := "https://release.b3log.org/siyuan/" + pkg
	liuyunURL := "https://release.liuyun.io/siyuan/" + pkg
	githubURL := "https://github.com/siyuan-note/siyuan/releases/download/v" + ver + "/" + pkg
	ghproxyURL := "https://mirror.ghproxy.com/" + githubURL
	if util.IsChinaCloud() {
		downloadPkgURLs = append(downloadPkgURLs, b3logURL)
		downloadPkgURLs = append(downloadPkgURLs, liuyunURL)
		downloadPkgURLs = append(downloadPkgURLs, ghproxyURL)
		downloadPkgURLs = append(downloadPkgURLs, githubURL)
	} else {
		downloadPkgURLs = append(downloadPkgURLs, githubURL)
		downloadPkgURLs = append(downloadPkgURLs, b3logURL)
		downloadPkgURLs = append(downloadPkgURLs, liuyunURL)
	}

	checksums := result["checksums"].(map[string]interface{})
	checksum = checksums[pkg].(string)
	return
}

func downloadInstallPkg(pkgURL, checksum string) (err error) {
	if "" == pkgURL || "" == checksum {
		return
	}

	pkg := path.Base(pkgURL)
	savePath := filepath.Join(util.TempDir, "install", pkg)
	if gulu.File.IsExist(savePath) {
		localChecksum, _ := sha256Hash(savePath)
		if localChecksum == checksum {
			return
		}
	}

	err = os.MkdirAll(filepath.Join(util.TempDir, "install"), 0755)
	if err != nil {
		logging.LogErrorf("create temp install dir failed: %s", err)
		return
	}

	logging.LogInfof("downloading install package [%s]", pkgURL)
	client := req.C().SetTLSHandshakeTimeout(7 * time.Second).SetTimeout(10 * time.Minute).DisableInsecureSkipVerify()
	callback := func(info req.DownloadInfo) {
		progress := fmt.Sprintf("%.2f%%", float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
		// logging.LogDebugf("downloading install package [%s %s]", pkgURL, progress)
		util.PushStatusBar(fmt.Sprintf(Conf.Language(133), progress))
	}
	_, err = client.R().SetOutputFile(savePath).SetDownloadCallbackWithInterval(callback, 1*time.Second).Get(pkgURL)
	if err != nil {
		logging.LogErrorf("download install package [%s] failed: %s", pkgURL, err)
		return
	}

	localChecksum, _ := sha256Hash(savePath)
	if checksum != localChecksum {
		logging.LogErrorf("verify checksum failed, download install package [%s] checksum [%s] not equal to downloaded [%s] checksum [%s]", pkgURL, checksum, savePath, localChecksum)
		return
	}
	logging.LogInfof("downloaded install package [%s] to [%s]", pkgURL, savePath)
	util.PushStatusBar(Conf.Language(62))
	return
}

func sha256Hash(filename string) (ret string, err error) {
	file, err := os.Open(filename)
	if err != nil {
		return
	}
	defer file.Close()

	hash := sha256.New()
	reader := bufio.NewReader(file)
	buf := make([]byte, 1024*1024*4)
	for {
		switch n, readErr := reader.Read(buf); readErr {
		case nil:
			hash.Write(buf[:n])
		case io.EOF:
			return fmt.Sprintf("%x", hash.Sum(nil)), nil
		default:
			return "", err
		}
	}
}

type Announcement struct {
	Id     string `json:"id"`
	Title  string `json:"title"`
	URL    string `json:"url"`
	Region int    `json:"region"`
}

func GetAnnouncements() (ret []*Announcement) {
	result, err := util.GetRhyResult(false)
	if err != nil {
		logging.LogErrorf("get announcement failed: %s", err)
		return
	}

	if nil == result["announcement"] {
		return
	}

	announcements := result["announcement"].([]interface{})
	for _, announcement := range announcements {
		ann := announcement.(map[string]interface{})
		ret = append(ret, &Announcement{
			Id:     ann["id"].(string),
			Title:  ann["title"].(string),
			URL:    ann["url"].(string),
			Region: int(ann["region"].(float64)),
		})
	}
	return
}

func CheckUpdate(showMsg bool) {
	if !showMsg {
		return
	}

	if Conf.System.IsMicrosoftStore {
		return
	}

	result, err := util.GetRhyResult(showMsg)
	if err != nil {
		return
	}

	ver := result["ver"].(string)
	releaseLang := result["release"].(string)
	if releaseLangArg := result["release_"+Conf.Lang]; nil != releaseLangArg {
		releaseLang = releaseLangArg.(string)
	}

	var msg string
	var timeout int
	if isVersionUpToDate(ver) {
		msg = Conf.Language(10)
		timeout = 3000
	} else {
		msg = fmt.Sprintf(Conf.Language(9), "<a href=\""+releaseLang+"\">"+releaseLang+"</a>")
		showMsg = true
		timeout = 15000
	}
	if showMsg {
		util.PushMsg(msg, timeout)
		go func() {
			defer logging.Recover()
			checkDownloadInstallPkg()
			if "" != getNewVerInstallPkgPath() {
				util.PushMsg(Conf.Language(62), 15*1000)
			}
		}()
	}
}

func isVersionUpToDate(releaseVer string) bool {
	return ver2num(releaseVer) <= ver2num(util.Ver)
}

func skipNewVerInstallPkg() bool {
	if !gulu.OS.IsWindows() && !gulu.OS.IsDarwin() {
		return true
	}
	if util.ISMicrosoftStore || util.ContainerStd != util.Container {
		return true
	}
	if !Conf.System.DownloadInstallPkg {
		return true
	}
	if gulu.OS.IsWindows() {
		plat := strings.ToLower(Conf.System.OSPlatform)
		// Windows 7, 8 and Server 2012 are no longer supported https://github.com/siyuan-note/siyuan/issues/7347
		if strings.Contains(plat, " 7 ") || strings.Contains(plat, " 8 ") || strings.Contains(plat, "2012") {
			return true
		}
	}
	return false
}

func ver2num(a string) int {
	var version string
	var suffixpos int
	var suffixStr string
	var suffix string
	a = strings.Trim(a, " ")
	if strings.Contains(a, "alpha") {
		suffixpos = strings.Index(a, "-alpha")
		version = a[0:suffixpos]
		suffixStr = a[suffixpos+6 : len(a)]
		suffix = "0" + fmt.Sprintf("%03s", suffixStr)
	} else if strings.Contains(a, "beta") {
		suffixpos = strings.Index(a, "-beta")
		version = a[0:suffixpos]
		suffixStr = a[suffixpos+5 : len(a)]
		suffix = "1" + fmt.Sprintf("%03s", suffixStr)
	} else {
		version = a
		suffix = "5000"
	}
	split := strings.Split(version, ".")
	var verArr []string

	verArr = append(verArr, "1")
	var tmp string
	for i := 0; i < 3; i++ {
		if i < len(split) {
			tmp = split[i]
		} else {
			tmp = "0"
		}
		verArr = append(verArr, fmt.Sprintf("%04s", tmp))
	}
	verArr = append(verArr, suffix)

	ver := strings.Join(verArr, "")
	verNum, _ := strconv.Atoi(ver)
	return verNum
}
