//go:build windows || linux

package main

import (
	"archive/tar"
	"archive/zip"
	"compress/bzip2"
	"compress/gzip"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"

	"fyne.io/systray"
)

const selfManagesBackend = true

var downloading sync.Mutex

func resticBinaryName() string {
	if runtime.GOOS == "windows" {
		return "restic.exe"
	}
	return "restic"
}

func managedRestic() string {
	p := filepath.Join(dataDir(), resticBinaryName())
	if _, err := os.Stat(p); err == nil {
		return p
	}
	return ""
}

func rusticBinaryName() string {
	if runtime.GOOS == "windows" {
		return "rustic.exe"
	}
	return "rustic"
}

func managedRustic() string {
	p := filepath.Join(dataDir(), rusticBinaryName())
	if _, err := os.Stat(p); err == nil {
		return p
	}
	return ""
}

func resetBackendPath(b Backend) {
	if state := backendPaths[b]; state != nil {
		state.Lock()
		defer state.Unlock()
		state.path = ""
		state.managed = false
	}
}

func binaryVersion(path string, args ...string) string {
	cmd := exec.Command(path, args...)
	hideWindow(cmd)
	out, err := cmd.Output()
	if err != nil {
		return ""
	}
	parts := strings.Fields(string(out))
	if len(parts) >= 2 {
		return parts[1]
	}
	return ""
}

func latestGitHubRelease(repo string) (string, error) {
	client := &http.Client{
		Timeout: 10 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	resp, err := client.Get("https://github.com/" + repo + "/releases/latest")
	if err != nil {
		return "", err
	}
	resp.Body.Close()
	loc := resp.Header.Get("Location")
	if loc == "" {
		return "", fmt.Errorf("no redirect from github")
	}
	v := strings.TrimPrefix(filepath.Base(loc), "v")
	if v == "" || v == "." {
		return "", fmt.Errorf("invalid release redirect from github")
	}
	return v, nil
}

func downloadRestic(mStatus prefixedMenuItem) error {
	if !downloading.TryLock() {
		return nil
	}
	defer downloading.Unlock()

	mStatus.SetTitle("Fetching latest version...")
	version, err := latestGitHubRelease("restic/restic")
	if err != nil {
		log.Printf("failed to fetch latest version: %v", err)
		return err
	}
	if version == "" {
		log.Print("failed to determine latest version")
		return fmt.Errorf("failed to determine latest version")
	}

	ext := "bz2"
	if runtime.GOOS == "windows" {
		ext = "zip"
	}
	url := fmt.Sprintf("https://github.com/restic/restic/releases/download/v%s/restic_%s_%s_%s.%s",
		version, version, runtime.GOOS, runtime.GOARCH, ext)
	log.Printf("downloading restic %s", version)
	mStatus.SetTitle("Downloading restic " + version + "...")

	resp, err := http.Get(url)
	if err != nil {
		log.Printf("download failed: %v", err)
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		log.Printf("download failed: %s", resp.Status)
		return fmt.Errorf("download failed: %s", resp.Status)
	}

	if err = extractRestic(resp.Body); err == nil {
		log.Printf("installed restic %s", version)
		resetBackendPath(BackendRestic)
	}
	return err
}

func extractRestic(r io.Reader) error {
	if runtime.GOOS != "windows" {
		return installNamedBinary(bzip2.NewReader(r), resticBinaryName())
	}

	tmp, err := os.CreateTemp("", "restray-download-*.zip")
	if err != nil {
		return err
	}
	defer os.Remove(tmp.Name())
	defer tmp.Close()

	if _, err := io.Copy(tmp, r); err != nil {
		return err
	}

	zr, err := zip.OpenReader(tmp.Name())
	if err != nil {
		return err
	}
	defer zr.Close()

	for _, zf := range zr.File {
		if strings.HasSuffix(zf.Name, ".exe") {
			rc, err := zf.Open()
			if err != nil {
				return err
			}
			defer rc.Close()
			return installNamedBinary(rc, resticBinaryName())
		}
	}
	return fmt.Errorf("restic binary not found in zip")
}

func rusticTarget() (string, error) {
	if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" {
		return "x86_64-pc-windows-msvc", nil
	}
	if runtime.GOOS == "linux" && runtime.GOARCH == "amd64" {
		return "x86_64-unknown-linux-musl", nil
	}
	if runtime.GOOS == "linux" && runtime.GOARCH == "arm64" {
		return "aarch64-unknown-linux-musl", nil
	}
	return "", fmt.Errorf("rustic managed downloads are unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
}

func downloadRustic(mStatus prefixedMenuItem) error {
	if !downloading.TryLock() {
		return nil
	}
	defer downloading.Unlock()
	mStatus.SetTitle("Fetching latest Rustic version...")
	version, err := latestGitHubRelease("rustic-rs/rustic")
	if err != nil {
		return err
	}
	target, err := rusticTarget()
	if err != nil {
		return err
	}
	ext := "tar.gz"
	url := fmt.Sprintf("https://github.com/rustic-rs/rustic/releases/download/v%s/rustic-v%s-%s.%s", version, version, target, ext)
	mStatus.SetTitle("Downloading Rustic " + version + "...")
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download failed: %s", resp.Status)
	}
	err = extractRusticTar(resp.Body)
	if err == nil {
		resetBackendPath(BackendRustic)
		log.Printf("installed rustic %s", version)
	}
	return err
}

func extractRusticTar(r io.Reader) error {
	gz, err := gzip.NewReader(r)
	if err != nil {
		return err
	}
	defer gz.Close()
	tr := tar.NewReader(gz)
	for {
		h, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}
		if filepath.Base(h.Name) == rusticBinaryName() {
			return installNamedBinary(tr, rusticBinaryName())
		}
	}
	return fmt.Errorf("rustic binary not found in archive")
}

func installNamedBinary(r io.Reader, name string) error {
	p := filepath.Join(dataDir(), name)
	if err := os.MkdirAll(filepath.Dir(p), 0700); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(filepath.Dir(p), name+"-download-*")
	if err != nil {
		return err
	}
	tmpName := tmp.Name()
	if _, err := io.Copy(tmp, r); err != nil {
		tmp.Close()
		os.Remove(tmpName)
		return err
	}
	if err := tmp.Close(); err != nil {
		os.Remove(tmpName)
		return err
	}
	if err := os.Chmod(tmpName, 0755); err != nil {
		os.Remove(tmpName)
		return err
	}
	if runtime.GOOS == "windows" {
		if err := os.Remove(p); err != nil && !os.IsNotExist(err) {
			os.Remove(tmpName)
			return err
		}
	}
	if err := os.Rename(tmpName, p); err != nil {
		os.Remove(tmpName)
		return err
	}
	return nil
}

func downloadBackend(prof Profile, status prefixedMenuItem) error {
	if prof.backend() == BackendRustic {
		return downloadRustic(status)
	}
	return downloadRestic(status)
}
func checkBackendUpdate(prof Profile, path string, auto bool, item *systray.MenuItem, status prefixedMenuItem, done func()) {
	backend := prof.backend()
	versionArgs := []string{"version"}
	repo := "restic/restic"
	label := "Restic"
	download := downloadRestic
	if backend == BackendRustic {
		versionArgs = []string{"--version"}
		repo = "rustic-rs/rustic"
		label = "Rustic"
		download = downloadRustic
	}
	current := strings.TrimPrefix(binaryVersion(path, versionArgs...), "v")
	latest, err := latestGitHubRelease(repo)
	latest = strings.TrimPrefix(latest, "v")
	if err != nil || current == "" || latest == current {
		return
	}
	log.Printf("%s update available: %s → %s", strings.ToLower(label), current, latest)
	if auto {
		if download(status) == nil {
			done()
		}
		return
	}
	item.SetTitle("Update " + label + " (" + current + " → " + latest + ")")
	item.Show()
}
