package services

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/jmoiron/sqlx"
	"github.com/lib/pq"
)

// condition_expr grammar per design §4.1 (m/paliad#149 Phase 2 P2):
//
//   CondExpr := { "flag": "<known_flag>" }
//             | { "op": "and"|"or", "args": [<CondExpr>, <CondExpr>, ...] }
//
// Leaf nodes reference a flag in paliad.scenario_flag_catalog by key.
// Composite nodes are recursive — and/or take ≥1 arg each. JSON null
// (or empty bytes) is also accepted — that's the "no gate" shape and
// stores as a NULL column.
//
// The validator is called from RuleEditorService.Create and
// UpdateDraft before the row is written. Surfaces friendly errors
// wrapping ErrInvalidInput so the handler maps cleanly to 400.

// ValidateConditionExpr parses the bytes as a CondExpr and verifies
// every leaf flag is present in the scenario_flag_catalog (one DB
// lookup, regardless of expression depth). Empty/null input is OK —
// caller stores NULL.
func ValidateConditionExpr(ctx context.Context, db *sqlx.DB, raw json.RawMessage) error {
	if len(raw) == 0 || string(raw) == "null" {
		return nil
	}
	var parsed condExprNode
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return fmt.Errorf("%w: condition_expr is not valid JSON: %v", ErrInvalidInput, err)
	}
	flagNames := map[string]struct{}{}
	if err := walkCondExpr(&parsed, flagNames); err != nil {
		return err
	}
	if len(flagNames) == 0 {
		// Empty leaf set is impossible for a valid CondExpr — walkCondExpr
		// would have rejected it. Defensive belt-and-braces.
		return fmt.Errorf("%w: condition_expr resolved to zero leaf flags", ErrInvalidInput)
	}
	keys := make([]string, 0, len(flagNames))
	for k := range flagNames {
		keys = append(keys, k)
	}
	known, err := loadCatalogFlagKeys(ctx, db, keys)
	if err != nil {
		return err
	}
	for _, k := range keys {
		if _, ok := known[k]; !ok {
			return fmt.Errorf("%w: condition_expr references unknown flag %q (not in paliad.scenario_flag_catalog)", ErrInvalidInput, k)
		}
	}
	return nil
}

// condExprNode is the loose-typed parse target. Either Flag is set
// (leaf) or Op + Args (composite); the validator below enforces
// mutual exclusivity.
type condExprNode struct {
	Flag *string         `json:"flag,omitempty"`
	Op   *string         `json:"op,omitempty"`
	Args []condExprNode  `json:"args,omitempty"`
	// Extra catches stray keys so we can reject typos like "fla" or
	// "operator" loudly instead of silently treating them as composite.
	Extra map[string]json.RawMessage `json:"-"`
}

// walkCondExpr descends the tree, collecting flag names and validating
// every node's shape.
func walkCondExpr(n *condExprNode, flagNames map[string]struct{}) error {
	hasFlag := n.Flag != nil
	hasOp := n.Op != nil
	hasArgs := n.Args != nil

	if hasFlag && (hasOp || hasArgs) {
		return fmt.Errorf("%w: condition_expr node has both 'flag' and 'op'/'args' — leaf and composite shapes are mutually exclusive", ErrInvalidInput)
	}
	if !hasFlag && !hasOp {
		return fmt.Errorf("%w: condition_expr node must carry either 'flag' (leaf) or 'op'+'args' (composite)", ErrInvalidInput)
	}

	if hasFlag {
		if *n.Flag == "" {
			return fmt.Errorf("%w: condition_expr leaf has empty flag", ErrInvalidInput)
		}
		flagNames[*n.Flag] = struct{}{}
		return nil
	}

	// Composite — op must be "and" or "or"; args must be non-empty.
	op := *n.Op
	if op != "and" && op != "or" {
		return fmt.Errorf("%w: condition_expr op=%q must be 'and' or 'or'", ErrInvalidInput, op)
	}
	if len(n.Args) == 0 {
		return fmt.Errorf("%w: condition_expr composite op=%q has empty args", ErrInvalidInput, op)
	}
	for i := range n.Args {
		if err := walkCondExpr(&n.Args[i], flagNames); err != nil {
			return err
		}
	}
	return nil
}

// loadCatalogFlagKeys returns the subset of `flagKeys` present in
// paliad.scenario_flag_catalog. One round-trip regardless of how many
// keys the expression carries.
func loadCatalogFlagKeys(ctx context.Context, db *sqlx.DB, flagKeys []string) (map[string]struct{}, error) {
	if len(flagKeys) == 0 {
		return map[string]struct{}{}, nil
	}
	rows, err := db.QueryContext(ctx,
		`SELECT flag_key FROM paliad.scenario_flag_catalog WHERE flag_key = ANY($1)`,
		pq.Array(flagKeys))
	if err != nil {
		return nil, fmt.Errorf("lookup scenario_flag_catalog: %w", err)
	}
	defer rows.Close()
	out := map[string]struct{}{}
	for rows.Next() {
		var k string
		if err := rows.Scan(&k); err != nil {
			return nil, err
		}
		out[k] = struct{}{}
	}
	return out, rows.Err()
}
