//go:build windows

package update

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"syscall"
	"time"

	"golang.org/x/sys/windows"
)

const finishUpdateFlag = "--navis-finish-update"

// Apply downloads the new build and replaces the running Navis.exe after this process exits.
func Apply(ctx context.Context, manifestURL string) (string, error) {
	latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
	if err != nil {
		return "", err
	}
	pending := filepath.Join(filepath.Dir(exe), "Navis-pending.exe")
	_ = os.Remove(pending)
	if err := os.Rename(tmp, pending); err != nil {
		if err2 := copyFile(tmp, pending); err2 != nil {
			_ = os.Remove(tmp)
			return "", fmt.Errorf("prepare updater: %w", err2)
		}
		_ = os.Remove(tmp)
	}
	_ = os.Chmod(pending, 0o755)
	_ = os.Remove(exe + ".new")

	cmd := exec.Command(pending, finishUpdateFlag, strconv.Itoa(os.Getpid()), exe)
	cmd.Dir = filepath.Dir(exe)
	const (
		createNewProcessGroup = 0x00000200
		detachedProcess       = 0x00000008
		createNoWindow        = 0x08000000
	)
	cmd.SysProcAttr = &syscall.SysProcAttr{
		CreationFlags: createNewProcessGroup | detachedProcess | createNoWindow,
		HideWindow:    true,
	}
	if err := cmd.Start(); err != nil {
		_ = os.Remove(pending)
		return "", fmt.Errorf("start updater: %w", err)
	}
	_ = cmd.Process.Release()
	return latest, nil
}

// MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update <pid> <targetExe>
// Replaces targetExe in-place with this binary, then relaunches it.
func MaybeFinishUpdate(args []string) bool {
	if len(args) < 3 || args[0] != finishUpdateFlag {
		return false
	}
	pid, err := strconv.Atoi(args[1])
	if err != nil || pid <= 0 {
		return true
	}
	target := args[2]
	self, err := os.Executable()
	if err != nil {
		return true
	}
	self, _ = filepath.Abs(self)
	target, _ = filepath.Abs(target)

	waitPIDExit(uint32(pid), 120*time.Second)
	time.Sleep(400 * time.Millisecond)

	if err := replaceExecutable(self, target); err != nil {
		time.Sleep(2 * time.Second)
		if err2 := replaceExecutable(self, target); err2 != nil {
			return true
		}
	}

	_ = os.Remove(target + ".bak")
	_ = os.Remove(target + ".new")
	_ = os.Remove(self)

	cmd := exec.Command(target)
	cmd.Dir = filepath.Dir(target)
	_ = cmd.Start()
	return true
}

// CleanupStaleDownloads removes leftover update temps from failed/old runs.
func CleanupStaleDownloads() {
	exe, err := os.Executable()
	if err != nil {
		return
	}
	exe, _ = filepath.Abs(exe)
	dir := filepath.Dir(exe)
	base := filepath.Base(exe)
	_ = os.Remove(exe + ".new")
	_ = os.Remove(filepath.Join(dir, base+".new"))
	_ = os.Remove(filepath.Join(dir, "Navis-update.part"))
	_ = os.Remove(filepath.Join(dir, ".navis-update-download"))
}

func replaceExecutable(src, dst string) error {
	backup := dst + ".bak"
	_ = os.Remove(backup)

	var last error
	for i := 0; i < 40; i++ {
		if err := os.Rename(dst, backup); err == nil {
			last = nil
			break
		} else {
			last = err
			time.Sleep(250 * time.Millisecond)
		}
	}
	if last != nil {
		staged := dst + ".stage"
		_ = os.Remove(staged)
		if err := copyFile(src, staged); err != nil {
			return fmt.Errorf("stage new exe: %w", err)
		}
		if err := moveFileReplace(staged, dst); err != nil {
			_ = os.Remove(staged)
			return fmt.Errorf("replace locked exe: %w (also: %v)", err, last)
		}
		return nil
	}

	if err := copyFile(src, dst); err != nil {
		_ = os.Rename(backup, dst)
		return err
	}
	_ = os.Chmod(dst, 0o755)
	_ = os.Remove(backup)
	return nil
}

func moveFileReplace(src, dst string) error {
	from, err := windows.UTF16PtrFromString(src)
	if err != nil {
		return err
	}
	to, err := windows.UTF16PtrFromString(dst)
	if err != nil {
		return err
	}
	const (
		moveFileReplaceExisting = 0x1
		moveFileWriteThrough    = 0x8
	)
	return windows.MoveFileEx(from, to, moveFileReplaceExisting|moveFileWriteThrough)
}

func waitPIDExit(pid uint32, timeout time.Duration) {
	const synchronize = 0x00100000
	h, err := windows.OpenProcess(synchronize, false, pid)
	if err != nil {
		time.Sleep(1500 * time.Millisecond)
		return
	}
	defer windows.CloseHandle(h)
	ms := uint32(timeout / time.Millisecond)
	if ms == 0 {
		ms = 1000
	}
	_, _ = windows.WaitForSingleObject(h, ms)
}

func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
	if err != nil {
		return err
	}
	defer out.Close()
	if _, err := io.Copy(out, in); err != nil {
		return err
	}
	return out.Close()
}
