package update

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

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

// 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"

// Manifest describes a remote release.
type Manifest struct {
	Version   string `json:"version"`
	URL       string `json:"url"`
	SHA256    string `json:"sha256,omitempty"`
	Notes     string `json:"notes,omitempty"`
	Mandatory bool   `json:"mandatory,omitempty"`
}

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

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

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil)
	if err != nil {
		return st, 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 {
		st.Error = err.Error()
		return st, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		err = fmt.Errorf("update feed HTTP %d", resp.StatusCode)
		st.Error = err.Error()
		return st, err
	}
	var m Manifest
	if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m); err != nil {
		st.Error = err.Error()
		return st, err
	}
	st.Latest = strings.TrimSpace(m.Version)
	st.Notes = m.Notes
	st.URL = m.URL
	st.Mandatory = m.Mandatory
	st.Available = Compare(st.Latest, CurrentVersion) > 0 && m.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
		}
		// strip pre-release suffix: 1.2.3-beta
		if i := strings.IndexAny(p, "-+"); i >= 0 {
			p = p[:i]
		}
		n, _ := strconv.Atoi(p)
		out = append(out, n)
	}
	return out
}

// Apply downloads the new exe and schedules replacement after exit.
func Apply(ctx context.Context, manifestURL string) (string, error) {
	st, err := Check(ctx, manifestURL)
	if err != nil {
		return "", err
	}
	if !st.Available {
		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
	}
	dir := filepath.Dir(exe)
	tmp := filepath.Join(dir, "Navis.exe.new")
	bat := filepath.Join(dir, "navis-update.bat")

	if err := downloadFile(ctx, st.URL, tmp); err != nil {
		return "", err
	}
	man, err := fetchManifest(ctx, manifestURL)
	if err == nil && man.SHA256 != "" {
		sum, err := fileSHA256(tmp)
		if err != nil {
			return "", err
		}
		if !strings.EqualFold(sum, man.SHA256) {
			_ = os.Remove(tmp)
			return "", fmt.Errorf("checksum mismatch")
		}
	}

	// Important: do NOT self-delete the .bat at the end — that causes
	// "Не удается найти пакетный файл" on paths with spaces (e.g. D:\vpn navi).
	script := "@echo off\r\n" +
		"setlocal EnableExtensions\r\n" +
		"cd /d \"" + dir + "\"\r\n" +
		"set \"EXE=" + exe + "\"\r\n" +
		"set \"NEW=" + tmp + "\"\r\n" +
		":wait\r\n" +
		"ping -n 2 127.0.0.1 >nul\r\n" +
		"del /f /q \"%EXE%\" >nul 2>&1\r\n" +
		"if exist \"%EXE%\" goto wait\r\n" +
		"move /y \"%NEW%\" \"%EXE%\" >nul\r\n" +
		"if not exist \"%EXE%\" exit /b 1\r\n" +
		"start \"\" \"%EXE%\"\r\n" +
		"exit /b 0\r\n"
	if err := os.WriteFile(bat, []byte(script), 0o755); err != nil {
		return "", err
	}

	// Detached cmd so Apply returns while updater keeps running.
	cmd := exec.Command("cmd.exe", "/C", bat)
	cmd.Dir = dir
	cmd.SysProcAttr = &syscall.SysProcAttr{
		HideWindow:    true,
		CreationFlags: 0x00000008 | 0x00000200, // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
	}
	if err := cmd.Start(); err != nil {
		return "", fmt.Errorf("start updater: %w", err)
	}
	return st.Latest, nil
}

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)
	client := &http.Client{Timeout: 20 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return Manifest{}, err
	}
	defer resp.Body.Close()
	var m Manifest
	err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m)
	return m, err
}

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
}
