package confmgr

import (
	"fmt"
	"reflect"
	"strings"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	cueerrors "cuelang.org/go/cue/errors"
)

// ConfigRule is a CUE-based validation/transformation rule bound to a config
// set. Its Source is CUE describing constraints and/or derivations over the
// instance's parameter values. Regular CUE fields whose key matches a set
// parameter are unified with the instance value; constraints that fail produce
// violations, and concrete fields that differ from the instance value are
// reported (and persisted) as transformations. Hidden fields (_x) and
// definitions (#X) are available for helpers and are excluded from both
// concreteness checks and transformation output. Rules are versioned git-style,
// exactly like sets and instances.
type ConfigRule struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	SetID       string `json:"setId"`
	Description string `json:"description,omitempty"`
	// Enabled gates whether the rule runs when instances of the bound set are
	// saved/applied. A nil pointer (legacy rules created before the flag) is
	// treated as enabled, so existing rules keep their behaviour.
	Enabled *bool  `json:"enabled,omitempty"`
	Version int    `json:"version"`
	Tag     string `json:"tag,omitempty"`
	Owner   string `json:"owner,omitempty"`
	Source  string `json:"source"`
}

// IsEnabled reports whether the rule participates in instance evaluation. A nil
// Enabled flag (legacy rules) is treated as enabled.
func (r ConfigRule) IsEnabled() bool { return r.Enabled == nil || *r.Enabled }

// RuleViolation is a single constraint failure from evaluating a rule.
type RuleViolation struct {
	Rule    string `json:"rule,omitempty"` // rule ID that produced it (aggregate runs)
	Path    string `json:"path,omitempty"` // dotted parameter path, when known
	Message string `json:"message"`
}

// RuleResult is the outcome of evaluating one or more rules against a set of
// instance values. OK is true only when there is no compile error and no
// violation. Transformed holds the parameter values the rule(s) derived or
// overrode (keys are parameter keys; values are the new concrete values).
type RuleResult struct {
	OK           bool            `json:"ok"`
	Violations   []RuleViolation `json:"violations,omitempty"`
	Transformed  map[string]any  `json:"transformed,omitempty"`
	CompileError string          `json:"compileError,omitempty"`
}

// RuleError wraps a failing RuleResult so it can flow through the store's
// error-returning API while preserving the structured violations.
type RuleError struct {
	Result RuleResult
}

func (e *RuleError) Error() string {
	if e.Result.CompileError != "" {
		return "rule compile error: " + e.Result.CompileError
	}
	msgs := make([]string, 0, len(e.Result.Violations))
	for _, v := range e.Result.Violations {
		if v.Path != "" {
			msgs = append(msgs, v.Path+": "+v.Message)
		} else {
			msgs = append(msgs, v.Message)
		}
	}
	if len(msgs) == 0 {
		return "rule validation failed"
	}
	return "rule validation failed: " + strings.Join(msgs, "; ")
}

// Validate compiles the rule's CUE source and checks basic metadata. It does
// not require concrete values — only that the source is syntactically and
// structurally well-formed CUE.
func (r ConfigRule) Validate() error {
	if strings.TrimSpace(r.Name) == "" {
		return fmt.Errorf("rule name must not be empty")
	}
	if strings.TrimSpace(r.SetID) == "" {
		return fmt.Errorf("rule must reference a config set")
	}
	ctx := cuecontext.New()
	v := ctx.CompileString(r.Source)
	if err := v.Err(); err != nil {
		return fmt.Errorf("invalid CUE: %s", firstError(err))
	}
	return nil
}

// EvaluateRule unifies a single CUE source with the given instance values and
// reports violations plus any transformed values. A compile error yields a
// non-OK result with CompileError populated (and no violations), so callers can
// distinguish "the rule is broken" from "the values are invalid".
func EvaluateRule(source string, values map[string]any) RuleResult {
	ctx := cuecontext.New()
	schema := ctx.CompileString(source)
	if err := schema.Err(); err != nil {
		return RuleResult{OK: false, CompileError: firstError(err)}
	}
	data := ctx.Encode(values)
	if err := data.Err(); err != nil {
		return RuleResult{OK: false, CompileError: "cannot encode values: " + firstError(err)}
	}
	unified := schema.Unify(data)

	res := RuleResult{OK: true}
	if err := unified.Validate(cue.Concrete(true), cue.All()); err != nil {
		res.OK = false
		for _, e := range cueerrors.Errors(err) {
			res.Violations = append(res.Violations, RuleViolation{
				Path:    strings.Join(e.Path(), "."),
				Message: cleanMessage(e),
			})
		}
		if len(res.Violations) == 0 {
			res.Violations = append(res.Violations, RuleViolation{Message: firstError(err)})
		}
		return res
	}

	// Extract transformations: regular concrete fields whose value differs from
	// the supplied input. Definitions and hidden fields are skipped by Fields().
	iter, err := unified.Fields()
	if err != nil {
		return res
	}
	for iter.Next() {
		key := iter.Selector().Unquoted()
		if key == "" {
			key = strings.Trim(iter.Selector().String(), `"`)
		}
		var v any
		if err := iter.Value().Decode(&v); err != nil {
			continue
		}
		if !reflect.DeepEqual(v, values[key]) {
			if res.Transformed == nil {
				res.Transformed = map[string]any{}
			}
			res.Transformed[key] = v
		}
	}
	return res
}

// evaluateRules runs several rules against the same values and aggregates the
// outcome. Violations are tagged with the rule ID. Transformations are applied
// cumulatively in order; a later rule sees earlier rules' outputs.
func evaluateRules(rules []ConfigRule, values map[string]any) RuleResult {
	agg := RuleResult{OK: true}
	cur := make(map[string]any, len(values))
	for k, v := range values {
		cur[k] = v
	}
	for _, r := range rules {
		one := EvaluateRule(r.Source, cur)
		if one.CompileError != "" {
			agg.OK = false
			agg.Violations = append(agg.Violations, RuleViolation{Rule: r.ID, Message: "compile error: " + one.CompileError})
			continue
		}
		if !one.OK {
			agg.OK = false
			for _, v := range one.Violations {
				v.Rule = r.ID
				agg.Violations = append(agg.Violations, v)
			}
			continue
		}
		for k, v := range one.Transformed {
			if agg.Transformed == nil {
				agg.Transformed = map[string]any{}
			}
			agg.Transformed[k] = v
			cur[k] = v
		}
	}
	return agg
}

// firstError renders the first CUE error as a single line.
func firstError(err error) string {
	errs := cueerrors.Errors(err)
	if len(errs) == 0 {
		return err.Error()
	}
	return cleanMessage(errs[0])
}

// cleanMessage formats a CUE error to a compact single-line string without the
// noisy file:line position prefix that cuecontext synthesises for anonymous
// sources.
func cleanMessage(e cueerrors.Error) string {
	format, args := e.Msg()
	return fmt.Sprintf(format, args...)
}
