package main

import (
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"sync/atomic"
	"time"
)

const repoBase = "https://tangled.org/devins.page/restray"

var latestKnownVersion atomic.Pointer[string]

var httpClient = &http.Client{Timeout: 10 * time.Second}

func isHTML(resp *http.Response) bool {
	return strings.Contains(resp.Header.Get("Content-Type"), "text/html")
}

func latestVersion() (string, error) {
	resp, err := httpClient.Get(repoBase + "/feed.atom?include=tags")
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("feed fetch failed: %s", resp.Status)
	}

	var feed struct {
		Entries []struct {
			Title string `xml:"title"`
		} `xml:"entry"`
	}
	if err := xml.NewDecoder(resp.Body).Decode(&feed); err != nil {
		return "", err
	}

	best := ""
	for _, e := range feed.Entries {
		t := strings.TrimSpace(strings.TrimPrefix(e.Title, "[Tag]"))
		v := strings.TrimPrefix(t, "v")
		if _, ok := parseVersion(v); !ok {
			continue
		}
		if best == "" || compareVersion(v, best) > 0 {
			best = v
		}
	}
	if best == "" {
		return "", fmt.Errorf("no tags in feed")
	}
	return best, nil
}

func parseVersion(v string) ([3]int, bool) {
	parts := strings.Split(v, ".")
	if len(parts) != 3 {
		return [3]int{}, false
	}
	var nums [3]int
	for i, p := range parts {
		n, err := strconv.Atoi(p)
		if err != nil {
			return [3]int{}, false
		}
		nums[i] = n
	}
	return nums, true
}

func compareVersion(a, b string) int {
	pa, okA := parseVersion(a)
	pb, okB := parseVersion(b)
	if !okA || !okB {
		return 0
	}
	for i := range pa {
		if pa[i] != pb[i] {
			return pa[i] - pb[i]
		}
	}
	return 0
}

func artifactURL(ver string) string {
	var name string
	switch runtime.GOOS {
	case "windows":
		name = fmt.Sprintf("restray-%s-windows-%s-setup.exe", ver, runtime.GOARCH)
	case "darwin":
		name = fmt.Sprintf("restray-%s-darwin-%s-app.zip", ver, runtime.GOARCH)
	default:
		return ""
	}
	return fmt.Sprintf("%s/tags/v%s/download/%s", repoBase, ver, name)
}

func artifactExists(url string) bool {
	req, err := http.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return false
	}
	req.Header.Set("Range", "bytes=0-0")
	resp, err := httpClient.Do(req)
	if err != nil {
		return false
	}
	resp.Body.Close()
	if resp.StatusCode != 200 && resp.StatusCode != 206 {
		return false
	}
	return !isHTML(resp)
}

func applyUpdate(ver string) (quit bool) {
	url := artifactURL(ver)
	if url == "" {
		return false
	}

	switch runtime.GOOS {
	case "darwin":
		openFile(url)
	case "windows":
		path, err := downloadToTemp(url)
		if err != nil {
			log.Printf("update: download failed: %v", err)
			return false
		}
		if err := exec.Command(path).Start(); err != nil {
			log.Printf("update: launch installer failed: %v", err)
			return false
		}
		return true
	}
	return false
}

func downloadToTemp(url string) (string, error) {
	resp, err := httpClient.Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return "", fmt.Errorf("download failed: %s", resp.Status)
	}
	if isHTML(resp) {
		return "", fmt.Errorf("download failed: artifact not found")
	}
	path := filepath.Join(os.TempDir(), filepath.Base(url))
	f, err := os.Create(path)
	if err != nil {
		return "", err
	}
	if _, err := io.Copy(f, io.LimitReader(resp.Body, 200<<20)); err != nil {
		f.Close()
		os.Remove(path)
		return "", err
	}
	f.Close()
	return path, nil
}
