package litigationplanner

import "encoding/json"

// allFlagsSet returns true when every element of `required` is present in
// `set`. Empty `required` returns true (no condition). Retained as the
// fallback predicate used by EvalConditionExpr when condition_expr is
// NULL but the legacy condition_flag text[] is set — preserves
// transition-window behaviour for any row Slice 2 missed (it shouldn't,
// but defensive).
func allFlagsSet(required []string, set map[string]struct{}) bool {
	for _, f := range required {
		if _, ok := set[f]; !ok {
			return false
		}
	}
	return true
}

// EvalConditionExpr returns true iff the rule's gate predicate is
// satisfied for the caller's flag set. Drives flag-conditional rendering
// + flag-conditional alt-swap throughout the calculator.
//
// Grammar (design §2.4 long form, mig 084 backfill):
//
//	{"flag": "<name>"}              — leaf: true iff <name> ∈ flags
//	{"op": "and", "args": [<n>...]} — true iff every arg evaluates true
//	{"op": "or",  "args": [<n>...]} — true iff any arg evaluates true
//	{"op": "not", "args": [<one>]}  — true iff the single arg is false
//
// NULL / empty / "null" expression → true (unconditional). Malformed
// JSON → true (defensive: the rule still renders, the lawyer sees
// it even if the gate is broken).
//
// Slice 9 (t-paliad-195, mig 091) dropped the legacy condition_flag
// text[] column; the fallback that AND'd over it is gone. Any future
// row needing array-of-flags semantics writes the equivalent
// {"op":"and","args":[{"flag":"<a>"},...]} jsonb directly.
func EvalConditionExpr(expr []byte, flags map[string]struct{}) bool {
	if len(expr) == 0 || string(expr) == "null" {
		return true
	}
	return EvalConditionExprNode(expr, flags)
}

// EvalConditionExprNode walks one node of the condition_expr jsonb
// tree. Recursion depth is bounded by the editor (Slice 11 caps tree
// depth + arg count); pre-Slice-11 backfilled rows have at most a
// 2-arg AND (mig 084).
func EvalConditionExprNode(raw []byte, flags map[string]struct{}) bool {
	var node struct {
		Flag string            `json:"flag"`
		Op   string            `json:"op"`
		Args []json.RawMessage `json:"args"`
	}
	if err := json.Unmarshal(raw, &node); err != nil {
		// Malformed → unconditional. The Slice 11 editor's validation
		// will block such writes; in the live corpus today mig 084's
		// jsonb_build_object output is well-formed by construction.
		return true
	}
	if node.Flag != "" {
		_, ok := flags[node.Flag]
		return ok
	}
	switch node.Op {
	case "and":
		for _, a := range node.Args {
			if !EvalConditionExprNode(a, flags) {
				return false
			}
		}
		return true
	case "or":
		for _, a := range node.Args {
			if EvalConditionExprNode(a, flags) {
				return true
			}
		}
		return false
	case "not":
		if len(node.Args) != 1 {
			// Malformed NOT — fall through to unconditional rather
			// than risk suppressing a rule the lawyer expects to see.
			return true
		}
		return !EvalConditionExprNode(node.Args[0], flags)
	}
	// Unknown op (forward-compat with editor extensions): treat as
	// unconditional so the rule still renders.
	return true
}

// HasConditionExpr returns true when the rule carries a non-empty,
// non-"null" jsonb gate. Slice 9 (t-paliad-195) replacement for the
// pre-drop `len(r.ConditionFlag) > 0` predicate that guarded the
// flag-keyed alt-swap branch. Same intent: "this rule has a gate;
// when the gate flips to met, swap to alt".
func HasConditionExpr(expr NullableJSON) bool {
	if len(expr) == 0 {
		return false
	}
	s := string(expr)
	return s != "null" && s != "{}"
}

// ExtractFlagsFromExpr walks the jsonb gate and returns the unique
// flag names referenced as {"flag":"<name>"} leaves. Used by
// CalculateRule's response (FlagsRequired) so the result-card calc
// panel can render flag checkboxes for each gate input. Replaces the
// dropped condition_flag text[] enumeration. Returns nil on a NULL
// expression or one that contains no flag leaves.
func ExtractFlagsFromExpr(expr NullableJSON) []string {
	if !HasConditionExpr(expr) {
		return nil
	}
	seen := make(map[string]struct{})
	walkFlagLeaves([]byte(expr), seen)
	if len(seen) == 0 {
		return nil
	}
	out := make([]string, 0, len(seen))
	for f := range seen {
		out = append(out, f)
	}
	return out
}

func walkFlagLeaves(raw []byte, into map[string]struct{}) {
	var node struct {
		Flag string            `json:"flag"`
		Op   string            `json:"op"`
		Args []json.RawMessage `json:"args"`
	}
	if err := json.Unmarshal(raw, &node); err != nil {
		return
	}
	if node.Flag != "" {
		into[node.Flag] = struct{}{}
		return
	}
	for _, a := range node.Args {
		walkFlagLeaves(a, into)
	}
}
