package update

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"
)

// CurrentVersion is the shipped client version.
const CurrentVersion = "1.8.1"

// DefaultManifestURL is the update feed (hosted in the project git repo).
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"

// PlatformAsset is one OS/arch binary in the feed.
type PlatformAsset struct {
	URL       string `json:"url"`
	SHA256    string `json:"sha256,omitempty"`
	OS        string `json:"os,omitempty"`
	Arch      string `json:"arch,omitempty"`
	DmgURL    string `json:"dmg_url,omitempty"`
	DmgSHA256 string `json:"dmg_sha256,omitempty"`
	ZipURL    string `json:"zip_url,omitempty"`
	ZipSHA256 string `json:"zip_sha256,omitempty"`
}

// Manifest describes a remote release (multi-platform + legacy single URL).
type Manifest struct {
	Version    string                   `json:"version"`
	URL        string                   `json:"url,omitempty"` // legacy windows-amd64
	SHA256     string                   `json:"sha256,omitempty"`
	Notes      string                   `json:"notes,omitempty"`
	Mandatory  bool                     `json:"mandatory,omitempty"`
	Platform   string                   `json:"platform,omitempty"` // legacy
	OS         string                   `json:"os,omitempty"`
	Arch       string                   `json:"arch,omitempty"`
	Platforms  map[string]PlatformAsset `json:"platforms,omitempty"`
}

// Status is returned to the UI / CLI.
type Status struct {
	Current     string `json:"current"`
	Latest      string `json:"latest,omitempty"`
	Notes       string `json:"notes,omitempty"`
	URL         string `json:"url,omitempty"`
	SHA256      string `json:"sha256,omitempty"`
	Platform    string `json:"platform,omitempty"`
	Available   bool   `json:"available"`
	Mandatory   bool   `json:"mandatory"`
	Checking    bool   `json:"checking,omitempty"`
	Error       string `json:"error,omitempty"`
	ManifestURL string `json:"manifest_url"`
}

// PlatformKey is "GOOS-GOARCH", e.g. windows-amd64, darwin-arm64.
func PlatformKey() string {
	return runtime.GOOS + "-" + runtime.GOARCH
}

// AssetFor returns download URL + checksum for the given platform key.
func (m Manifest) AssetFor(key string) (url, sha string, ok bool) {
	if key == "" {
		key = PlatformKey()
	}
	if m.Platforms != nil {
		if a, found := m.Platforms[key]; found && strings.TrimSpace(a.URL) != "" {
			return strings.TrimSpace(a.URL), strings.TrimSpace(a.SHA256), true
		}
	}
	// Legacy single-asset manifests are windows-amd64.
	if (key == "windows-amd64" || key == "windows-386") && strings.TrimSpace(m.URL) != "" {
		return strings.TrimSpace(m.URL), strings.TrimSpace(m.SHA256), true
	}
	return "", "", false
}

// Check fetches the manifest and compares versions for this platform.
func Check(ctx context.Context, manifestURL string) (Status, error) {
	if manifestURL == "" {
		manifestURL = DefaultManifestURL
	}
	key := PlatformKey()
	st := Status{Current: CurrentVersion, ManifestURL: manifestURL, Platform: key}

	m, err := fetchManifest(ctx, manifestURL)
	if err != nil {
		st.Error = err.Error()
		return st, err
	}
	st.Latest = strings.TrimSpace(m.Version)
	st.Notes = m.Notes
	st.Mandatory = m.Mandatory

	url, sha, ok := m.AssetFor(key)
	if !ok {
		st.Error = fmt.Sprintf("нет сборки для %s", key)
		st.Available = false
		return st, nil
	}
	st.URL = url
	st.SHA256 = sha
	st.Available = Compare(st.Latest, CurrentVersion) > 0 && url != ""
	return st, nil
}

// Compare returns 1 if a>b, -1 if a<b, 0 if equal (semver-ish dotted ints).
func Compare(a, b string) int {
	as := splitVer(a)
	bs := splitVer(b)
	n := len(as)
	if len(bs) > n {
		n = len(bs)
	}
	for i := 0; i < n; i++ {
		var ai, bi int
		if i < len(as) {
			ai = as[i]
		}
		if i < len(bs) {
			bi = bs[i]
		}
		if ai > bi {
			return 1
		}
		if ai < bi {
			return -1
		}
	}
	return 0
}

func splitVer(v string) []int {
	v = strings.TrimPrefix(strings.TrimSpace(v), "v")
	parts := strings.Split(v, ".")
	out := make([]int, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			out = append(out, 0)
			continue
		}
		if i := strings.IndexAny(p, "-+"); i >= 0 {
			p = p[:i]
		}
		n, _ := strconv.Atoi(p)
		out = append(out, n)
	}
	return out
}

func allowedDownloadURL(u string) bool {
	u = strings.ToLower(strings.TrimSpace(u))
	return strings.HasPrefix(u, "https://git.evilfox.cc/") ||
		strings.HasPrefix(u, "https://files.de4ima.uk/")
}

func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
	if manifestURL == "" {
		manifestURL = DefaultManifestURL
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil)
	if err != nil {
		return Manifest{}, err
	}
	req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
	req.Header.Set("Accept", "application/json")
	client := &http.Client{Timeout: 20 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return Manifest{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return Manifest{}, fmt.Errorf("update feed HTTP %d", resp.StatusCode)
	}
	var m Manifest
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m); err != nil {
		return Manifest{}, err
	}
	return m, nil
}

func downloadFile(ctx context.Context, url, dest string) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
	client := &http.Client{Timeout: 10 * time.Minute}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("download HTTP %d", resp.StatusCode)
	}
	f, err := os.Create(dest)
	if err != nil {
		return err
	}
	defer f.Close()
	_, err = io.Copy(f, io.LimitReader(resp.Body, 200<<20))
	return err
}

func fileSHA256(path string) (string, error) {
	f, err := os.Open(path)
	if err != nil {
		return "", err
	}
	defer f.Close()
	h := sha256.New()
	if _, err := io.Copy(h, f); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)), nil
}

// prepareDownload fetches the update next to the running binary as "<exe>.new".
func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha, exePath, tmpPath string, err error) {
	st, err := Check(ctx, manifestURL)
	if err != nil {
		return "", "", "", "", "", err
	}
	if !st.Available {
		if st.Error != "" {
			return "", "", "", "", "", fmt.Errorf("%s", st.Error)
		}
		return "", "", "", "", "", fmt.Errorf("обновление не найдено")
	}
	if !allowedDownloadURL(st.URL) {
		return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
	}
	exe, err := os.Executable()
	if err != nil {
		return "", "", "", "", "", err
	}
	exe, err = filepath.Abs(exe)
	if err != nil {
		return "", "", "", "", "", err
	}
	tmp := filepath.Join(filepath.Dir(exe), ".navis-update-download")
	_ = os.Remove(tmp)
	// Also clear legacy leftover from older updaters.
	_ = os.Remove(exe + ".new")
	if err := downloadFile(ctx, st.URL, tmp); err != nil {
		return "", "", "", "", "", err
	}
	wantSHA := st.SHA256
	if wantSHA == "" {
		if man, err := fetchManifest(ctx, manifestURL); err == nil {
			if _, sha2, ok := man.AssetFor(PlatformKey()); ok {
				wantSHA = sha2
			}
		}
	}
	if wantSHA != "" {
		sum, err := fileSHA256(tmp)
		if err != nil {
			_ = os.Remove(tmp)
			return "", "", "", "", "", err
		}
		if !strings.EqualFold(sum, wantSHA) {
			_ = os.Remove(tmp)
			return "", "", "", "", "", fmt.Errorf("checksum mismatch")
		}
	}
	return st.Latest, st.URL, wantSHA, exe, tmp, nil
}
