package upgrader

import (
	"encoding/json"
	"fmt"
	_github "github.com/0xJacky/Nginx-UI/.github"
	"github.com/0xJacky/Nginx-UI/internal/helper"
	"github.com/0xJacky/Nginx-UI/internal/logger"
	"github.com/0xJacky/Nginx-UI/settings"
	"github.com/jpillora/overseer"
	"github.com/minio/selfupdate"
	"github.com/pkg/errors"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync/atomic"
)

type Upgrader struct {
	Release TRelease
	RuntimeInfo
}

func NewUpgrader(channel string) (u *Upgrader, err error) {
	data, err := GetRelease(channel)
	if err != nil {
		return
	}
	runtimeInfo, err := GetRuntimeInfo()
	if err != nil {
		return
	}
	u = &Upgrader{
		Release:     data,
		RuntimeInfo: runtimeInfo,
	}
	return
}

type ProgressWriter struct {
	io.Writer
	totalSize    int64
	currentSize  int64
	progressChan chan<- float64
}

func (pw *ProgressWriter) Write(p []byte) (int, error) {
	n, err := pw.Writer.Write(p)
	pw.currentSize += int64(n)
	progress := float64(pw.currentSize) / float64(pw.totalSize) * 100
	pw.progressChan <- progress
	return n, err
}

func downloadRelease(url string, dir string, progressChan chan float64) (tarName string, err error) {
	client := &http.Client{}
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return
	}
	resp, err := client.Do(req)
	if err != nil {
		return
	}
	defer resp.Body.Close()

	totalSize, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
	if err != nil {
		return
	}

	file, err := os.CreateTemp(dir, "nginx-ui-temp-*.tar.gz")
	if err != nil {
		err = errors.Wrap(err, "service.DownloadLatestRelease CreateTemp error")
		return
	}
	defer file.Close()

	progressWriter := &ProgressWriter{Writer: file, totalSize: totalSize, progressChan: progressChan}
	multiWriter := io.MultiWriter(progressWriter)

	_, err = io.Copy(multiWriter, resp.Body)
	close(progressChan)

	tarName = file.Name()
	return
}

func (u *Upgrader) DownloadLatestRelease(progressChan chan float64) (tarName string, err error) {
	bytes, err := _github.DistFS.ReadFile("build/build_info.json")
	if err != nil {
		err = errors.Wrap(err, "service.DownloadLatestRelease Read build_info.json error")
		return
	}
	type buildArch struct {
		Arch string `json:"arch"`
		Name string `json:"name"`
	}
	var buildJson map[string]map[string]buildArch

	_ = json.Unmarshal(bytes, &buildJson)

	build, ok := buildJson[u.OS]
	if !ok {
		err = errors.Wrap(err, "os not support upgrade")
		return
	}
	arch, ok := build[u.Arch]
	if !ok {
		err = errors.Wrap(err, "arch not support upgrade")
		return
	}

	assetsMap := u.Release.GetAssetsMap()

	// asset
	asset, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz", arch.Name)]

	if !ok {
		err = errors.Wrap(err, "upgrader core asset is empty")
		return
	}

	downloadUrl := asset.BrowserDownloadUrl
	if downloadUrl == "" {
		err = errors.New("upgrader core downloadUrl is empty")
		return
	}

	// digest
	digest, ok := assetsMap[fmt.Sprintf("nginx-ui-%s.tar.gz.digest", arch.Name)]
	if !ok || digest.BrowserDownloadUrl == "" {
		err = errors.New("upgrader core digest is empty")
		return
	}

	if settings.ServerSettings.GithubProxy != "" {
		digest.BrowserDownloadUrl, err = url.JoinPath(settings.ServerSettings.GithubProxy, digest.BrowserDownloadUrl)
		if err != nil {
			err = errors.Wrap(err, "service.DownloadLatestRelease url.JoinPath error")
			return
		}
	}

	resp, err := http.Get(digest.BrowserDownloadUrl)
	if err != nil {
		err = errors.Wrap(err, "upgrader core download digest fail")
		return
	}

	defer resp.Body.Close()

	dir := filepath.Dir(u.ExPath)

	if settings.ServerSettings.GithubProxy != "" {
		downloadUrl, err = url.JoinPath(settings.ServerSettings.GithubProxy, downloadUrl)
		if err != nil {
			err = errors.Wrap(err, "service.DownloadLatestRelease url.JoinPath error")
			return
		}
	}

	tarName, err = downloadRelease(downloadUrl, dir, progressChan)
	if err != nil {
		err = errors.Wrap(err, "service.DownloadLatestRelease downloadFile error")
		return
	}

	// check tar digest
	digestFileBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		err = errors.Wrap(err, "digest file content read error")
		return
	}

	digestFileContent := strings.TrimSpace(string(digestFileBytes))

	logger.Debug("DownloadLatestRelease tar digest", helper.DigestSHA512(tarName))
	logger.Debug("DownloadLatestRelease digestFileContent", digestFileContent)

	if digestFileContent == "" {
		err = errors.New("digest file content is empty")
		return
	}

	exeSHA512 := helper.DigestSHA512(tarName)
	if exeSHA512 == "" {
		err = errors.New("executable binary file is empty")
		return
	}

	if digestFileContent != exeSHA512 {
		err = errors.Wrap(err, "digest not equal")
		return
	}

	return
}

var updateInProgress atomic.Bool

func (u *Upgrader) PerformCoreUpgrade(tarPath string) (err error) {
	if !updateInProgress.CompareAndSwap(false, true) {
		return errors.New("update already in progress")
	}
	defer updateInProgress.Store(false)

	opts := selfupdate.Options{}

	if err = opts.CheckPermissions(); err != nil {
		return err
	}

	tempDir, err := os.MkdirTemp("", "nginx-ui-upgrade-*")
	if err != nil {
		return err
	}
	defer os.RemoveAll(tempDir)

	err = helper.UnTar(tempDir, tarPath)
	if err != nil {
		err = errors.Wrap(err, "PerformCoreUpgrade unTar error")
		return
	}

	f, err := os.Open(filepath.Join(tempDir, "nginx-ui"))
	if err != nil {
		err = errors.Wrap(err, "PerformCoreUpgrade open error")
		return
	}
	defer f.Close()

	if err = selfupdate.PrepareAndCheckBinary(f, opts); err != nil {
		var pathErr *os.PathError
		if errors.As(err, &pathErr) {
			return pathErr.Err
		}
		return err
	}

	if err = selfupdate.CommitBinary(opts); err != nil {
		if rerr := selfupdate.RollbackError(err); rerr != nil {
			return rerr
		}
		var pathErr *os.PathError
		if errors.As(err, &pathErr) {
			return pathErr.Err
		}
		return err
	}

	// gracefully restart
	overseer.Restart()

	return
}
