package selfupdate

import (
	"context"
	"log/slog"
	"time"
)

// Manager owns the post-restart COMMIT half of the A/B update (TASK D1 Scenario D). After a flip,
// the NEW binary boots with a pending marker on disk; once it has run cleanly for the dwell AND
// core init is done, it calls the wrapper's `commit` to clear the marker. A crash before that →
// systemd + the wrapper roll back to .prev (this Manager never rolls back).
type Manager struct {
	stateDir       string
	runningVersion string
	dwell          time.Duration
	runner         WrapperRunner
	logger         *slog.Logger
	now            func() time.Time // injectable for tests
	sleep          func(context.Context, time.Duration)
}

// ManagerConfig wires the commit manager.
type ManagerConfig struct {
	StateDir       string
	RunningVersion string        // this binary's own version (main.version)
	Dwell          time.Duration // 0 → 60s
	Runner         WrapperRunner
	Logger         *slog.Logger
}

// NewManager builds the commit manager.
func NewManager(cfg ManagerConfig) *Manager {
	dwell := cfg.Dwell
	if dwell == 0 {
		dwell = 60 * time.Second
	}
	return &Manager{
		stateDir:       cfg.StateDir,
		runningVersion: cfg.RunningVersion,
		dwell:          dwell,
		runner:         cfg.Runner,
		logger:         orDefaultLogger(cfg.Logger),
		now:            func() time.Time { return time.Now() },
		sleep: func(ctx context.Context, d time.Duration) {
			t := time.NewTimer(d)
			defer t.Stop()
			select {
			case <-ctx.Done():
			case <-t.C:
			}
		},
	}
}

// PendingStatus is the report-facing snapshot: whether an update is awaiting commit, and (when so)
// the target version. Read synchronously at report-build time — cheap (one stat+parse).
type PendingStatus struct {
	Pending bool
	Version string
}

// Status returns the current pending-marker state for the host report. A read error is treated as
// "not pending" for the report (the loud path is the commit goroutine's WARNs), never fatal.
func (m *Manager) Status() PendingStatus {
	pm, err := readPending(m.stateDir)
	if err != nil || pm == nil {
		return PendingStatus{}
	}
	return PendingStatus{Pending: true, Version: pm.NewVersion}
}

// SelfUpdatePending satisfies hub.SelfUpdateReporter (the report seam): pending + awaited version.
func (m *Manager) SelfUpdatePending() (bool, string) {
	s := m.Status()
	return s.Pending, s.Version
}

// MaybeCommit runs the startup commit decision, blocking for the dwell when a matching update is
// pending. Call it in a goroutine AFTER core init (config parsed, local API up, control loop
// started) — never on the startup critical path. Behaviour (Scenario C5 + D):
//   - no pending marker → nothing to do (the common case).
//   - pending.new_version == this running version → dwell, then `commit` (clears the marker).
//   - pending.new_version != running version → do NOT commit; loud WARN; leave the marker so the
//     hub report shows pending=true (a human decides — the box is in a weird state).
func (m *Manager) MaybeCommit(ctx context.Context) {
	pm, err := readPending(m.stateDir)
	if err != nil {
		m.logger.Error("selfupdate: cannot read pending marker — not committing", "err", err)
		return
	}
	if pm == nil {
		return // no update in flight
	}
	if pm.NewVersion != m.runningVersion {
		// The running binary is NOT the one this pending marker describes. Do not commit (committing
		// would bless a state we can't explain). Leave the marker → the report surfaces pending=true.
		m.logger.Warn("selfupdate: pending marker version does not match the running binary — NOT committing (human review)",
			"pending_new_version", pm.NewVersion, "running_version", m.runningVersion, "pending_old_version", pm.OldVersion)
		return
	}

	m.logger.Warn("selfupdate: new version running — dwelling before commit",
		"version", m.runningVersion, "prev", pm.OldVersion, "dwell", m.dwell)
	m.sleep(ctx, m.dwell)
	if ctx.Err() != nil {
		// Shutting down before the dwell elapsed — leave pending; the next start re-dwells and
		// commits. A restart is not a crash (no OnFailure), so this does not trigger rollback.
		m.logger.Warn("selfupdate: shutdown before commit dwell elapsed — pending left for next start")
		return
	}
	if m.runner == nil {
		m.logger.Error("selfupdate: no wrapper runner — cannot commit (marker left)")
		return
	}
	stdout, stderr, err := m.runner.Run(ctx, wrapperPath, "commit")
	if err != nil {
		m.logger.Error("selfupdate: commit failed (marker left; will retry next start)", "err", err, "stderr", string(stderr))
		return
	}
	m.logger.Warn("selfupdate: update committed", "version", m.runningVersion, "wrapper", trim(stdout))
}
