package updater

import (
	"encoding/json"
	"errors"
	"net/http"
	"runtime"
	"strconv"
	"strings"
)

// Options are the update options supported by the
type Options struct {
	// IsBeta is for beta updates to be installed if available
	IsBeta bool

	// IsForced is to forcibly download the latest version regardless of the current version
	IsForced bool

	// RequestedVersion is the specific version to upgrade or downgrade to
	RequestedVersion string
}

// VersionResponse is the JSON response from the Workers API endpoint
type VersionResponse struct {
	URL          string `json:"url"`
	Version      string `json:"version"`
	Checksum     string `json:"checksum"`
	IsCompressed bool   `json:"compressed"`
	Error        string `json:"error"`
}

// WorkersService implements Service.
// It contains everything needed to check in with the WorkersAPI and download and apply the updates
type WorkersService struct {
	currentVersion string
	url            string
	targetPath     string
	opts           Options
}

// NewWorkersService creates a new updater Service object.
func NewWorkersService(currentVersion, url, targetPath string, opts Options) Service {
	return &WorkersService{
		currentVersion: currentVersion,
		url:            url,
		targetPath:     targetPath,
		opts:           opts,
	}
}

// Check does a check in with the Workers API to get a new version update
func (s *WorkersService) Check() (Version, error) {
	client := &http.Client{
		Timeout: clientTimeout,
	}

	req, err := http.NewRequest(http.MethodGet, s.url, nil)
	q := req.URL.Query()
	q.Add(OSKeyName, runtime.GOOS)
	q.Add(ArchitectureKeyName, runtime.GOARCH)

	if s.opts.IsBeta {
		q.Add(BetaKeyName, "true")
	}

	if s.opts.RequestedVersion != "" {
		q.Add(VersionKeyName, s.opts.RequestedVersion)
	}

	req.URL.RawQuery = q.Encode()
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var v VersionResponse
	if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
		return nil, err
	}

	if v.Error != "" {
		return nil, errors.New(v.Error)
	}

	if !s.opts.IsForced && !IsNewerVersion(s.currentVersion, v.Version) {
		return nil, nil
	}

	return NewWorkersVersion(v.URL, v.Version, v.Checksum, s.targetPath, v.IsCompressed), nil
}

// IsNewerVersion checks semantic versioning for the latest version
// cloudflared tagging is more of a date than a semantic version,
// but the same comparision logic still holds for major.minor.patch
// e.g. 2020.8.2 is newer than 2020.8.1.
func IsNewerVersion(current string, check string) bool {
	if strings.Contains(strings.ToLower(current), "dev") {
		return false // dev builds shouldn't update
	}

	cMajor, cMinor, cPatch, err := SemanticParts(current)
	if err != nil {
		return false
	}

	nMajor, nMinor, nPatch, err := SemanticParts(check)
	if err != nil {
		return false
	}

	if nMajor > cMajor {
		return true
	}

	if nMajor == cMajor && nMinor > cMinor {
		return true
	}

	if nMajor == cMajor && nMinor == cMinor && nPatch > cPatch {
		return true
	}
	return false
}

// SemanticParts gets the major, minor, and patch version of a semantic version string
// e.g. 3.1.2 would return 3, 1, 2, nil
func SemanticParts(version string) (major int, minor int, patch int, err error) {
	major = 0
	minor = 0
	patch = 0
	parts := strings.Split(version, ".")
	if len(parts) != 3 {
		err = errors.New("invalid version")
		return
	}
	major, err = strconv.Atoi(parts[0])
	if err != nil {
		return
	}

	minor, err = strconv.Atoi(parts[1])
	if err != nil {
		return
	}

	patch, err = strconv.Atoi(parts[2])
	if err != nil {
		return
	}
	return
}
