//go:build !windows

package updater

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"time"
)

// Update fetches the new binary, swaps it in, then exits so systemd
// restarts the process under the new binary. The caller should close
// the WS connection cleanly (so the server transitions the host to
// disconnected immediately rather than waiting for the heartbeat
// sweep) before invoking.
//
// Service-user assumption: the agent runs as root under the
// systemd-shipped unit, which can write the binary path directly.
// If the agent ever moves to a non-root service user, this breaks —
// would need a setuid helper or an out-of-process update service.
func Update(ctx context.Context, serverURL string) error {
	binPath, err := resolveOwnBinary()
	if err != nil {
		return err
	}
	stage, err := fetch(ctx, serverURL, binPath)
	if err != nil {
		return err
	}
	if err := swap(stage, binPath); err != nil {
		return err
	}
	slog.Info("agent self-update: binary swapped, exiting for systemd restart",
		"binary", binPath)
	// Give logger / WS close-frame a moment to flush, then exit.
	time.Sleep(200 * time.Millisecond)
	os.Exit(0)
	return nil // unreachable
}

// swap copies the running binary to <bin>.old (M1 — keep one revision
// back for hand-rolled rollback), then atomic-renames the staged
// binary into place. Linux supports rename-while-open so this works
// even though the running process holds the source open.
func swap(stagePath, binPath string) error {
	src, err := os.Open(binPath)
	if err != nil {
		return fmt.Errorf("open running binary: %w", err)
	}
	defer func() { _ = src.Close() }()
	dst, err := os.OpenFile(binPath+".old", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
	if err != nil {
		return fmt.Errorf("open .old: %w", err)
	}
	if _, err := io.Copy(dst, src); err != nil {
		_ = dst.Close()
		return fmt.Errorf("copy to .old: %w", err)
	}
	if err := dst.Sync(); err != nil {
		_ = dst.Close()
		return err
	}
	if err := dst.Close(); err != nil {
		return err
	}
	if err := os.Rename(stagePath, binPath); err != nil {
		return fmt.Errorf("rename .new over running binary: %w", err)
	}
	return nil
}
