// SPDX-License-Identifier: GPL-3.0-or-later

package updater

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
)

// writeStaged writes data to path, creating any missing parent directory,
// flushes to disk, and chmods to 0755 (executable). On Windows the
// executable bit is meaningless, but the .exe extension carries semantics.
func writeStaged(path string, data []byte) error {
	if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(filepath.Dir(path), ".staging-*")
	if err != nil {
		return err
	}
	cleanup := func() {
		_ = tmp.Close()
		_ = os.Remove(tmp.Name())
	}
	if _, err := tmp.Write(data); err != nil {
		cleanup()
		return err
	}
	if err := tmp.Sync(); err != nil {
		cleanup()
		return err
	}
	if err := tmp.Close(); err != nil {
		_ = os.Remove(tmp.Name())
		return err
	}
	if err := os.Chmod(tmp.Name(), 0755); err != nil {
		_ = os.Remove(tmp.Name())
		return err
	}
	if err := os.Rename(tmp.Name(), path); err != nil {
		_ = os.Remove(tmp.Name())
		return err
	}
	return nil
}

// promoteBinary atomically swaps a staged binary into the live exec path.
//
// On Unix this is a single os.Rename, which is atomic on the same
// filesystem; the running process keeps executing the old (now unlinked)
// inode and only the next exec sees the new bytes.
//
// On Windows, you cannot rename over a running .exe. We rename the live
// binary out of the way (as <path>.old, deleted on next startup) and then
// rename the staged file in. The service manager then needs to restart
// the process for the swap to take effect.
//
// Files written by our own process do NOT receive the macOS
// `com.apple.quarantine` extended attribute — that xattr is only set by
// Launch Services-aware downloaders (browsers, Mail, Messages). As long
// as the originating .app bundle was signed and notarised, replacing the
// daemon binary in place does not re-trigger Gatekeeper warnings.
func promoteBinary(staged, live string) error {
	if runtime.GOOS == "windows" {
		old := live + ".old"
		_ = os.Remove(old) // best-effort cleanup of previous swap
		if _, err := os.Stat(live); err == nil {
			if err := os.Rename(live, old); err != nil {
				return fmt.Errorf("renaming live binary out of the way: %w", err)
			}
		}
		if err := os.Rename(staged, live); err != nil {
			// Try to put the old one back on failure.
			_ = os.Rename(old, live)
			return fmt.Errorf("renaming staged into place: %w", err)
		}
		return nil
	}
	// Unix: ensure exec mode is preserved and rename atomically.
	if err := os.Chmod(staged, 0755); err != nil {
		return err
	}
	if err := os.Rename(staged, live); err != nil {
		return fmt.Errorf("renaming staged binary: %w", err)
	}
	return nil
}

// copyFile is a small helper used by tests that need to seed a staging
// directory with a known artifact. It deliberately does not preserve any
// metadata beyond mode bits.
func copyFile(src, dst string) error {
	in, err := os.Open(src)
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
	defer out.Close()
	_, err = io.Copy(out, in)
	return err
}
