package report

import "log"

// ConfigRefresher reconciles the hub-advertised config_version (from the report ACK) against the
// controller's last-applied version and, on a change, re-pulls controller.yaml and self-restarts —
// the pull-based config-delivery path (the hub never connects into the box; this rides the existing
// report cycle exactly like the Phase 2 version floor).
//
// All side effects are injected so the reconcile is unit-testable without a real hub / filesystem /
// process exit:
//   - Applied  reads the persisted last-applied config_version (0 = none recorded yet).
//   - Record   persists a newly-applied config_version.
//   - Refresh  re-pulls controller.yaml from the hub and writes it (re-merging local_api). It must
//              NOT touch settings.json. A hub-unreachable / write failure returns an error.
//   - Restart  triggers the graceful self-restart (process exit → Docker restart → fresh config).
type ConfigRefresher struct {
	Applied func() int
	Record  func(int) error
	Refresh func() error
	Restart func()
	Logger  *log.Logger
}

// Reconcile applies the config-refresh decision for one report ACK. Rules (acceptance B + safety §5):
//   - ackVersion == 0           → no-op (hub didn't advertise; old hub / no config row).
//   - no version recorded yet   → record the baseline WITHOUT restarting (first-ever ACK; the
//     first-boot pull already fetched the current config).
//   - ackVersion == applied     → no-op (no change; this is what prevents a restart storm — after a
//     refresh, applied == ackVersion so the next report is a no-op).
//   - ackVersion != applied     → Refresh; on success Record THEN Restart (record-before-restart so
//     the post-restart process sees it applied); on a failed Refresh keep the current config, do NOT
//     record, do NOT restart — retried on the next report cycle.
func (cr *ConfigRefresher) Reconcile(ackVersion int) {
	if ackVersion == 0 {
		return // hub didn't advertise a config_version
	}
	applied := cr.Applied()
	if applied == 0 {
		// First-ever ACK carrying a config_version: record the baseline, do NOT restart (the box came
		// up on the first-boot pull, which already has the current config). Mirrors the floor's
		// first-run-records-baseline.
		if err := cr.Record(ackVersion); err != nil {
			cr.logf("[WARN] config-refresh: failed to record baseline config_version=%d: %v", ackVersion, err)
			return
		}
		cr.logf("[INFO] config-refresh: baseline config_version=%d recorded (no restart)", ackVersion)
		return
	}
	if ackVersion == applied {
		return // no change
	}
	cr.logf("[INFO] config-refresh: hub config_version=%d != applied=%d — re-pulling controller.yaml", ackVersion, applied)
	if err := cr.Refresh(); err != nil {
		// Fail-safe: keep the current config, do NOT record, do NOT restart — retry next cycle.
		cr.logf("[WARN] config-refresh: re-pull failed: %v — keeping current config, will retry next report", err)
		return
	}
	// Record BEFORE restarting so the freshly-started process sees the version as applied and does not
	// loop. (The restart is delayed, so the record persists first.)
	if err := cr.Record(ackVersion); err != nil {
		cr.logf("[WARN] config-refresh: applied config but failed to record version=%d: %v — skipping restart to avoid a loop", ackVersion, err)
		return
	}
	cr.logf("[INFO] config-refresh: applied config_version=%d — self-restarting to load it", ackVersion)
	if cr.Restart != nil {
		cr.Restart()
	}
}

func (cr *ConfigRefresher) logf(format string, args ...interface{}) {
	if cr.Logger != nil {
		cr.Logger.Printf(format, args...)
	}
}
