// Package selfupdate implements the AGENT-Go half of the operator-signed A/B self-update (TASK D1).
// The ROOT half (the atomic binary flip + rollback) is the felhom-selfupdate-guarded shell wrapper
// (configs/); systemd owns the crash-loop auto-rollback. This package:
//   - downloads the operator-pinned binary from the configured artifact host,
//   - verifies it against the SIGNED sha256 (the only integrity root),
//   - hands it to the wrapper's `apply` verb via `sudo -n` (the Executor, driven by signedjobs),
//   - after the NEW binary has run cleanly for the dwell, calls the wrapper's `commit` (the Manager).
//
// The design principle (SPIKE-agent-selfupdate-2026-07-05): the thing that performs rollback is
// never the thing being updated. This package only ever STARTS an update and COMMITS a good one;
// it never rolls back (that is systemd + the wrapper, so a crash-looping new binary cannot fail to
// revert itself).
package selfupdate

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"regexp"
)

// opAgentUpdate is the op class this executor serves (mirrors reconcile.ClassAgentUpdate; the
// literal avoids importing reconcile here just for the string).
const opAgentUpdate = "agent_update"

// versionRe bounds the version string that is interpolated into a URL and a filename. Bare semver
// with an optional pre-release suffix (e.g. 0.70.1, 0.70.2-crash) — no slashes, spaces, or dots-only.
var versionRe = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$`)

// sha256Re is the strict 64-lowercase-hex form of the signed sha.
var sha256Re = regexp.MustCompile(`^[0-9a-f]{64}$`)

// PendingMarker is the JSON the wrapper writes at <StateDir>/selfupdate/pending.json after a flip.
// The agent reads it on startup to decide whether to commit. Field names match the wrapper.
type PendingMarker struct {
	OldVersion string `json:"old_version"`
	NewVersion string `json:"new_version"`
	SHA256     string `json:"sha256"`
	AppliedAt  string `json:"applied_at"`
}

// stagingDir is the agent-writable staging subdir (spike S4a: agent-owned StateDir, 0750).
func stagingDir(stateDir string) string { return filepath.Join(stateDir, "selfupdate") }

// pendingPath is the wrapper's pending-marker path.
func pendingPath(stateDir string) string { return filepath.Join(stagingDir(stateDir), "pending.json") }

// readPending returns the pending marker, or (nil, nil) when none exists.
func readPending(stateDir string) (*PendingMarker, error) {
	data, err := os.ReadFile(pendingPath(stateDir))
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, err
	}
	var m PendingMarker
	if err := json.Unmarshal(data, &m); err != nil {
		return nil, fmt.Errorf("selfupdate: pending marker unparseable: %w", err)
	}
	return &m, nil
}

// interpolateURL substitutes the (validated) version into the URL template's {version} placeholder.
func interpolateURL(template, version string) string {
	return regexp.MustCompile(`\{version\}`).ReplaceAllString(template, version)
}

// noopLogger is a discard logger for nil-safety.
func orDefaultLogger(l *slog.Logger) *slog.Logger {
	if l == nil {
		return slog.Default()
	}
	return l
}
