package api

import (
	"context"
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"sync"
	"time"

	"github.com/uopi/uopi/internal/broker"
	"github.com/uopi/uopi/internal/confmgr"
)

// configEnabled guards every config-manager handler; the store is always
// constructed today, but the nil check keeps the handlers safe if it is ever
// made optional.
func (h *Handler) configEnabled(w http.ResponseWriter) bool {
	if h.cfg == nil {
		jsonError(w, http.StatusServiceUnavailable, "configuration manager not enabled")
		return false
	}
	return true
}

func pathVersion(r *http.Request) (int, error) {
	return strconv.Atoi(r.PathValue("version"))
}

func configStatus(err error) int {
	if errors.Is(err, confmgr.ErrNotFound) {
		return http.StatusNotFound
	}
	return http.StatusBadRequest
}

// ── config sets ─────────────────────────────────────────────────────────────

func (h *Handler) listConfigSets(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	sets, err := h.cfg.List(confmgr.KindSet)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	jsonOK(w, h.filterConfigMetas(r, sets))
}

func (h *Handler) getConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	set, err := h.cfg.GetSet(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, set)
}

func (h *Handler) createConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	var set confmgr.ConfigSet
	if err := json.NewDecoder(r.Body).Decode(&set); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	set.ID = ""
	set.Owner = caller(r)
	out, err := h.cfg.CreateSet(set, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.set.create", out.ID+" "+out.Name)
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, out)
}

func (h *Handler) updateConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	var set confmgr.ConfigSet
	if err := json.NewDecoder(r.Body).Decode(&set); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	if prev, err := h.cfg.GetSet(id); err == nil {
		set.Owner = prev.Owner // owner is immutable across revisions
	}
	out, err := h.cfg.UpdateSet(id, set, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.set.update", out.ID+" v"+strconv.Itoa(out.Version))
	jsonOK(w, out)
}

func (h *Handler) deleteConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Delete(confmgr.KindSet, id); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.set.delete", id)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) listConfigSetVersions(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	versions, err := h.cfg.Versions(confmgr.KindSet, r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, versions)
}

func (h *Handler) getConfigSetVersion(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	set, err := h.cfg.GetSetVersion(r.PathValue("id"), version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, set)
}

func (h *Handler) promoteConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Promote(confmgr.KindSet, id, version); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.set.promote", id+" v"+strconv.Itoa(version))
	set, err := h.cfg.GetSet(id)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, set)
}

func (h *Handler) forkConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	newID, err := h.cfg.Fork(confmgr.KindSet, id, version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.set.fork", id+" v"+strconv.Itoa(version)+" -> "+newID)
	set, err := h.cfg.GetSet(newID)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, set)
}

// diffConfigSets compares two set revisions. Query params: a, b (ids);
// optional av, bv (versions, default current).
func (h *Handler) diffConfigSets(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	q := r.URL.Query()
	left, err := h.resolveSet(q.Get("a"), q.Get("av"))
	if err != nil {
		jsonError(w, configStatus(err), "left set: "+err.Error())
		return
	}
	right, err := h.resolveSet(q.Get("b"), q.Get("bv"))
	if err != nil {
		jsonError(w, configStatus(err), "right set: "+err.Error())
		return
	}
	jsonOK(w, confmgr.DiffSets(left, right))
}

func (h *Handler) resolveSet(id, version string) (confmgr.ConfigSet, error) {
	if id == "" {
		return confmgr.ConfigSet{}, errors.New("missing set id")
	}
	if version == "" {
		return h.cfg.GetSet(id)
	}
	v, err := strconv.Atoi(version)
	if err != nil {
		return confmgr.ConfigSet{}, errors.New("invalid version")
	}
	return h.cfg.GetSetVersion(id, v)
}

// ── config instances ────────────────────────────────────────────────────────

func (h *Handler) listConfigInstances(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	insts, err := h.cfg.List(confmgr.KindInstance)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	jsonOK(w, h.filterConfigMetas(r, insts))
}

// filterConfigMetas drops entries the caller may not see per their scope
// (private/group/global). Owner always sees their own; empty scope = global.
func (h *Handler) filterConfigMetas(r *http.Request, metas []confmgr.Meta) []confmgr.Meta {
	user := caller(r)
	out := make([]confmgr.Meta, 0, len(metas))
	for _, m := range metas {
		if h.policy.CanSee(user, m.Owner, m.Scope, m.Groups) {
			out = append(out, m)
		}
	}
	return out
}

func (h *Handler) getConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	inst, err := h.cfg.GetInstance(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, inst)
}

func (h *Handler) createConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	var inst confmgr.ConfigInstance
	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	inst.ID = ""
	inst.Owner = caller(r)
	out, err := h.cfg.CreateInstance(inst, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.create", out.ID+" "+out.Name)
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, out)
}

func (h *Handler) updateConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	var inst confmgr.ConfigInstance
	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	if prev, err := h.cfg.GetInstance(id); err == nil {
		inst.Owner = prev.Owner // owner is immutable across revisions
	}
	out, err := h.cfg.UpdateInstance(id, inst, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.update", out.ID+" v"+strconv.Itoa(out.Version))
	jsonOK(w, out)
}

func (h *Handler) deleteConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Delete(confmgr.KindInstance, id); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.delete", id)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) listConfigInstanceVersions(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	versions, err := h.cfg.Versions(confmgr.KindInstance, r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, versions)
}

func (h *Handler) getConfigInstanceVersion(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	inst, err := h.cfg.GetInstanceVersion(r.PathValue("id"), version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, inst)
}

func (h *Handler) promoteConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Promote(confmgr.KindInstance, id, version); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.promote", id+" v"+strconv.Itoa(version))
	inst, err := h.cfg.GetInstance(id)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, inst)
}

func (h *Handler) forkConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	newID, err := h.cfg.Fork(confmgr.KindInstance, id, version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.fork", id+" v"+strconv.Itoa(version)+" -> "+newID)
	inst, err := h.cfg.GetInstance(newID)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, inst)
}

// applyConfigInstance writes every resolvable parameter value of an instance to
// its target signal via the broker. Per-parameter outcomes are returned so a
// partial apply is reported faithfully.
func (h *Handler) applyConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	inst, err := h.cfg.GetInstance(id)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	set, err := h.cfg.SetForInstance(inst)
	if err != nil {
		jsonError(w, configStatus(err), "load set: "+err.Error())
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
	defer cancel()
	write := func(ds, signal string, value any) error {
		src, ok := h.broker.Source(ds)
		if !ok {
			return errors.New("unknown data source: " + ds)
		}
		return src.Write(ctx, signal, value)
	}
	res := confmgr.Apply(set, inst, write)
	h.recordMutation(r, "config.instance.apply", id+" applied="+strconv.Itoa(res.Applied)+" failed="+strconv.Itoa(res.Failed))
	jsonOK(w, res)
}

// diffConfigInstances compares two instance revisions. Query params: a, b
// (ids); optional av, bv (versions, default current).
func (h *Handler) diffConfigInstances(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	q := r.URL.Query()
	left, err := h.resolveInstance(q.Get("a"), q.Get("av"))
	if err != nil {
		jsonError(w, configStatus(err), "left instance: "+err.Error())
		return
	}
	right, err := h.resolveInstance(q.Get("b"), q.Get("bv"))
	if err != nil {
		jsonError(w, configStatus(err), "right instance: "+err.Error())
		return
	}
	jsonOK(w, confmgr.DiffInstances(left, right))
}

func (h *Handler) resolveInstance(id, version string) (confmgr.ConfigInstance, error) {
	if id == "" {
		return confmgr.ConfigInstance{}, errors.New("missing instance id")
	}
	if version == "" {
		return h.cfg.GetInstance(id)
	}
	v, err := strconv.Atoi(version)
	if err != nil {
		return confmgr.ConfigInstance{}, errors.New("invalid version")
	}
	return h.cfg.GetInstanceVersion(id, v)
}

// validateConfigInstance evaluates the CUE rules bound to an instance's set
// against its stored values, without persisting anything. The structured
// RuleResult (violations + any transformed values) lets the UI surface
// per-parameter failures.
func (h *Handler) validateConfigInstance(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	inst, err := h.cfg.GetInstance(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	res, err := h.cfg.ValidateInstanceRules(inst)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	jsonOK(w, res)
}

// ── config snapshot / live diff ─────────────────────────────────────────────

// readResult is the per-signal outcome of a parallel live read.
type readResult struct {
	val any
	err error
}

// readSetSignals reads the current value of every distinct target signal of a
// set in parallel (deduplicated by ds+signal), bounded by ctx. The returned map
// is keyed by {ds, signal}.
func (h *Handler) readSetSignals(ctx context.Context, set confmgr.ConfigSet) map[[2]string]readResult {
	jobs := make(map[[2]string]struct{}, len(set.Parameters))
	for _, p := range set.Parameters {
		jobs[[2]string{p.DS, p.Signal}] = struct{}{}
	}
	results := make(map[[2]string]readResult, len(jobs))
	var mu sync.Mutex
	var wg sync.WaitGroup
	for k := range jobs {
		wg.Add(1)
		go func(ds, signal string) {
			defer wg.Done()
			v, err := h.broker.ReadNow(ctx, broker.SignalRef{DS: ds, Name: signal})
			mu.Lock()
			results[[2]string{ds, signal}] = readResult{val: v.Data, err: err}
			mu.Unlock()
		}(k[0], k[1])
	}
	wg.Wait()
	return results
}

// snapshotReader builds a confmgr.ReadFunc backed by a pre-read results map.
func snapshotReader(results map[[2]string]readResult) confmgr.ReadFunc {
	return func(ds, signal string) (any, error) {
		r, ok := results[[2]string{ds, signal}]
		if !ok {
			return nil, errors.New("signal not read")
		}
		return r.val, r.err
	}
}

// snapshotConfigSet captures the current value of every target signal of a set
// and stores them as a new config instance. Body: optional {name}. Returns the
// created instance plus the per-parameter capture result.
func (h *Handler) snapshotConfigSet(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	set, err := h.cfg.GetSet(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	var body struct {
		Name string `json:"name"`
	}
	if r.Body != nil {
		_ = json.NewDecoder(r.Body).Decode(&body) // body is optional
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	results := h.readSetSignals(ctx, set)
	snap := confmgr.Snapshot(set, snapshotReader(results))

	name := body.Name
	if name == "" {
		name = set.Name + " snapshot " + time.Now().Format("2006-01-02 15:04:05")
	}
	inst := confmgr.ConfigInstance{
		Name:   name,
		SetID:  set.ID,
		Owner:  caller(r),
		Values: snap.Values,
	}
	out, err := h.cfg.CreateInstance(inst, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.instance.snapshot", out.ID+" "+out.Name+" captured="+strconv.Itoa(snap.Captured)+" failed="+strconv.Itoa(snap.Failed))
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, struct {
		Instance confmgr.ConfigInstance `json:"instance"`
		Snapshot confmgr.SnapshotResult `json:"snapshot"`
	}{out, snap})
}

// diffConfigInstanceLive compares a stored instance against the current live
// values of its set's target signals, so an operator can see how the saved
// config differs from what the hardware currently holds. The stored side uses
// resolved values (instance value or parameter default) so default-backed
// parameters are not reported as spurious additions.
func (h *Handler) diffConfigInstanceLive(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	inst, err := h.cfg.GetInstance(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	set, err := h.cfg.SetForInstance(inst)
	if err != nil {
		jsonError(w, configStatus(err), "load set: "+err.Error())
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	results := h.readSetSignals(ctx, set)
	snap := confmgr.Snapshot(set, snapshotReader(results))

	left := confmgr.ConfigInstance{ID: inst.ID, Name: inst.Name, Values: map[string]any{}}
	for _, p := range set.Parameters {
		if v, ok := inst.Resolve(p); ok {
			left.Values[p.Key] = v
		}
	}
	current := confmgr.ConfigInstance{Name: "current", Values: snap.Values}
	jsonOK(w, confmgr.DiffInstances(left, current))
}

// ── config rules (CUE validation/transformation) ────────────────────────────

func (h *Handler) listConfigRules(w http.ResponseWriter, _ *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	rules, err := h.cfg.List(confmgr.KindRule)
	if err != nil {
		jsonError(w, http.StatusInternalServerError, err.Error())
		return
	}
	jsonOK(w, rules)
}

func (h *Handler) getConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	rule, err := h.cfg.GetRule(r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, rule)
}

func (h *Handler) createConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	var rule confmgr.ConfigRule
	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	rule.ID = ""
	rule.Owner = caller(r)
	out, err := h.cfg.CreateRule(rule, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.rule.create", out.ID+" "+out.Name)
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, out)
}

func (h *Handler) updateConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	var rule confmgr.ConfigRule
	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	out, err := h.cfg.UpdateRule(id, rule, r.URL.Query().Get("tag"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.rule.update", out.ID+" v"+strconv.Itoa(out.Version))
	jsonOK(w, out)
}

func (h *Handler) deleteConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Delete(confmgr.KindRule, id); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.rule.delete", id)
	w.WriteHeader(http.StatusNoContent)
}

func (h *Handler) listConfigRuleVersions(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	versions, err := h.cfg.Versions(confmgr.KindRule, r.PathValue("id"))
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, versions)
}

func (h *Handler) getConfigRuleVersion(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	rule, err := h.cfg.GetRuleVersion(r.PathValue("id"), version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, rule)
}

func (h *Handler) promoteConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	if err := h.cfg.Promote(confmgr.KindRule, id, version); err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.rule.promote", id+" v"+strconv.Itoa(version))
	rule, err := h.cfg.GetRule(id)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	jsonOK(w, rule)
}

func (h *Handler) forkConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	version, err := pathVersion(r)
	if err != nil {
		jsonError(w, http.StatusBadRequest, "invalid version")
		return
	}
	id := r.PathValue("id")
	newID, err := h.cfg.Fork(confmgr.KindRule, id, version)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	h.recordMutation(r, "config.rule.fork", id+" v"+strconv.Itoa(version)+" -> "+newID)
	rule, err := h.cfg.GetRule(newID)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	w.WriteHeader(http.StatusCreated)
	jsonOK(w, rule)
}

// checkConfigRule compiles a (possibly unsaved) CUE source and, when sample
// values are supplied, evaluates it against them. It powers the editor's live
// validation panel; nothing is persisted.
func (h *Handler) checkConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	var body struct {
		Source string         `json:"source"`
		Values map[string]any `json:"values"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	jsonOK(w, confmgr.EvaluateRule(body.Source, body.Values))
}

// previewConfigRule evaluates a (possibly unsaved) CUE source against a live
// snapshot of the bound set's target signals, without storing anything. Unlike
// check (which uses sample/default values), preview reads the current hardware
// values so the operator sees exactly what the rule would derive from the real
// configuration. Body: {setId, source}. Returns the captured snapshot plus the
// rule result (violations + transformed values).
func (h *Handler) previewConfigRule(w http.ResponseWriter, r *http.Request) {
	if !h.configEnabled(w) {
		return
	}
	var body struct {
		SetID  string `json:"setId"`
		Source string `json:"source"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
		return
	}
	set, err := h.cfg.GetSet(body.SetID)
	if err != nil {
		jsonError(w, configStatus(err), err.Error())
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	results := h.readSetSignals(ctx, set)
	snap := confmgr.Snapshot(set, snapshotReader(results))
	res := confmgr.EvaluateRule(body.Source, snap.Values)
	jsonOK(w, struct {
		Snapshot confmgr.SnapshotResult `json:"snapshot"`
		Result   confmgr.RuleResult     `json:"result"`
	}{snap, res})
}
