// Package scheduler — cel.go implements a minimal CEL-subset evaluator
// for the CLI-side scheduler constraint expressions (REQ-083, P05).
//
// The full CEL specification (google.golang.org/genproto/...
// googleapis/api/expr/v1alpha1) is intentionally NOT a dependency of
// this module (see go.mod): adding it for a single callsite would pull
// in a large transitive graph and contradict the "stdlib + minimal
// deps" guardrail. Instead this file implements a hand-rolled
// recursive-descent evaluator for the subset the PRD exercises:
//
//   - attribute access on a `node.<name>` object (hostname, kind,
//     cpus, memory, tags, runtimes)
//   - string and integer literals (double-quoted)
//   - comparison operators: == != >= <= > <
//   - membership: <expr> in <expr>, <expr> not in <expr>
//   - boolean composition: and, or, not (parenthesised)
//
// Anything outside this subset returns an error rather than a silent
// wrong answer; that is the documented limitation. The grammar is
// small enough to be unambiguous with a top-down precedence-climbing
// parser.
package scheduler

import (
	"fmt"
	"strconv"
	"strings"
	"unicode"
)

// EvaluateConstraint evaluates a single CEL-subset expression against
// the supplied NodeInfo. Returns (matched, err). An expression that
// references an unknown attribute, uses an unsupported operator, or
// fails to parse yields an error. Schedule treats a constraint
// evaluation error as a non-fit (the node is silently skipped) rather
// than a hard fail because operators routinely write exploratory
// constraints against attributes the local cluster does not expose.
func EvaluateConstraint(expr string, node NodeInfo) (bool, error) {
	p := newParser(strings.TrimSpace(expr), node)
	if p.len() == 0 {
		return false, fmt.Errorf("cel: empty expression")
	}
	v, err := p.parseExpr()
	if err != nil {
		return false, err
	}
	if p.tok.kind != tokEOF {
		return false, fmt.Errorf("cel: trailing input near %q", p.tok.text)
	}
	b, ok := v.(bool)
	if !ok {
		return false, fmt.Errorf("cel: expression did not evaluate to bool (got %T)", v)
	}
	return b, nil
}

// EvaluateAll returns true iff every constraint evaluates to true
// against the node (logical AND). An empty constraint list is vacuously
// true. The first evaluation error short-circuits and is returned.
func EvaluateAll(constraints []string, node NodeInfo) (bool, error) {
	for _, c := range constraints {
		ok, err := EvaluateConstraint(c, node)
		if err != nil {
			return false, fmt.Errorf("constraint %q: %w", c, err)
		}
		if !ok {
			return false, nil
		}
	}
	return true, nil
}

// ----------------------------------------------------------------------------
// Value model
// ----------------------------------------------------------------------------

// celValue is the union of values the evaluator produces. We use the
// Go interface{} representation so that comparisons can be polymorphic
// without a tagged-union ceremony; the supported concrete types are
// bool, int64, and string. Lists are []celValue of the above.
type celValue = interface{}

// ----------------------------------------------------------------------------
// Tokenizer
// ----------------------------------------------------------------------------

type tokKind int

const (
	tokEOF tokKind = iota
	tokIdent
	tokInt
	tokStr
	tokOp  // ==, !=, >=, <=, >, <, (, ), .
	tokIn  // "in"
	tokAnd // "and"
	tokOr  // "or"
	tokNot // "not"
)

type token struct {
	kind tokKind
	text string
}

type lexer struct {
	src string
	pos int
}

func (l *lexer) next() (token, error) {
	for l.pos < len(l.src) && unicode.IsSpace(rune(l.src[l.pos])) {
		l.pos++
	}
	if l.pos >= len(l.src) {
		return token{kind: tokEOF}, nil
	}
	c := l.src[l.pos]
	// string literal
	if c == '"' {
		start := l.pos
		l.pos++
		for l.pos < len(l.src) && l.src[l.pos] != '"' {
			l.pos++
		}
		if l.pos >= len(l.src) {
			return token{}, fmt.Errorf("cel: unterminated string at %d", start)
		}
		val := l.src[start+1 : l.pos]
		l.pos++ // consume closing quote
		return token{kind: tokStr, text: val}, nil
	}
	// integer literal
	if unicode.IsDigit(rune(c)) {
		start := l.pos
		for l.pos < len(l.src) && unicode.IsDigit(rune(l.src[l.pos])) {
			l.pos++
		}
		return token{kind: tokInt, text: l.src[start:l.pos]}, nil
	}
	// identifier / keyword
	if isIdentStart(c) {
		start := l.pos
		for l.pos < len(l.src) && isIdentPart(l.src[l.pos]) {
			l.pos++
		}
		word := l.src[start:l.pos]
		switch word {
		case "in":
			return token{kind: tokIn, text: word}, nil
		case "and":
			return token{kind: tokAnd, text: word}, nil
		case "or":
			return token{kind: tokOr, text: word}, nil
		case "not":
			return token{kind: tokNot, text: word}, nil
		default:
			return token{kind: tokIdent, text: word}, nil
		}
	}
	// operators
	if strings.ContainsRune("()=!<>.", rune(c)) {
		// multi-char operators
		if l.pos+1 < len(l.src) {
			two := l.src[l.pos : l.pos+2]
			switch two {
			case "==", "!=", ">=", "<=":
				l.pos += 2
				return token{kind: tokOp, text: two}, nil
			}
		}
		l.pos++
		return token{kind: tokOp, text: string(c)}, nil
	}
	return token{}, fmt.Errorf("cel: unexpected character %q at %d", c, l.pos)
}

func isIdentStart(c byte) bool {
	return c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}

func isIdentPart(c byte) bool {
	return isIdentStart(c) || (c >= '0' && c <= '9')
}

// ----------------------------------------------------------------------------
// Parser (recursive descent, precedence climbing)
// ----------------------------------------------------------------------------

type parser struct {
	src  string
	pos  int
	tok  token
	err  error
	node NodeInfo
}

func newParser(src string, node NodeInfo) *parser {
	p := &parser{src: src, node: node}
	p.advance()
	return p
}

func (p *parser) len() int { return len(p.src) }

func (p *parser) advance() {
	if p.err != nil {
		return
	}
	l := lexer{src: p.src, pos: p.pos}
	t, err := l.next()
	if err != nil {
		p.err = err
		return
	}
	p.pos = l.pos
	p.tok = t
}

// Grammar (lowest precedence first):
//
//	expr     := orExpr
//	orExpr   := andExpr ("or" andExpr)*
//	andExpr  := notExpr ("and" notExpr)*
//	notExpr  := "not" notExpr | cmpExpr
//	cmpExpr  := primary (op primary | "in" primary | "not" "in" primary)?
//	primary  := "(" expr ")"
//	          | int
//	          | str
//	          | "true" | "false"
//	          | nodeAttr  ("." ident)?   // node.<field>
//	          | ident                    // bare attribute (e.g. region)
//	nodeAttr := "node"

func (p *parser) parseExpr() (celValue, error) {
	if p.err != nil {
		return nil, p.err
	}
	return p.parseOr()
}

func (p *parser) parseOr() (celValue, error) {
	left, err := p.parseAnd()
	if err != nil {
		return nil, err
	}
	for p.tok.kind == tokOr {
		p.advance()
		right, err := p.parseAnd()
		if err != nil {
			return nil, err
		}
		lb, ok := left.(bool)
		if !ok {
			return nil, fmt.Errorf("cel: 'or' operand not bool: %T", left)
		}
		rb, ok := right.(bool)
		if !ok {
			return nil, fmt.Errorf("cel: 'or' operand not bool: %T", right)
		}
		left = lb || rb
	}
	return left, nil
}

func (p *parser) parseAnd() (celValue, error) {
	left, err := p.parseNot()
	if err != nil {
		return nil, err
	}
	for p.tok.kind == tokAnd {
		p.advance()
		right, err := p.parseNot()
		if err != nil {
			return nil, err
		}
		lb, ok := left.(bool)
		if !ok {
			return nil, fmt.Errorf("cel: 'and' operand not bool: %T", left)
		}
		rb, ok := right.(bool)
		if !ok {
			return nil, fmt.Errorf("cel: 'and' operand not bool: %T", right)
		}
		left = lb && rb
	}
	return left, nil
}

func (p *parser) parseNot() (celValue, error) {
	if p.tok.kind == tokNot {
		// "not" at the start of a primary is logical negation. "not in"
		// is handled in parseCmp where it follows a primary.
		p.advance()
		v, err := p.parseNot()
		if err != nil {
			return nil, err
		}
		b, ok := v.(bool)
		if !ok {
			return nil, fmt.Errorf("cel: 'not' operand not bool: %T", v)
		}
		return !b, nil
	}
	return p.parseCmp()
}

func (p *parser) parseCmp() (celValue, error) {
	left, err := p.parsePrimary()
	if err != nil {
		return nil, err
	}
	// "not in"
	if p.tok.kind == tokNot {
		p.advance()
		if p.tok.kind != tokIn {
			return nil, fmt.Errorf("cel: expected 'in' after 'not', got %q", p.tok.text)
		}
		p.advance()
		right, err := p.parsePrimary()
		if err != nil {
			return nil, err
		}
		member, err := inMember(left, right)
		if err != nil {
			return nil, err
		}
		return !member, nil
	}
	// "in"
	if p.tok.kind == tokIn {
		p.advance()
		right, err := p.parsePrimary()
		if err != nil {
			return nil, err
		}
		return inMember(left, right)
	}
	// comparison operators
	if p.tok.kind == tokOp {
		op := p.tok.text
		switch op {
		case "==", "!=", ">=", "<=", ">", "<":
			p.advance()
			right, err := p.parsePrimary()
			if err != nil {
				return nil, err
			}
			return compare(op, left, right)
		default:
			return nil, fmt.Errorf("cel: unexpected operator %q", op)
		}
	}
	return left, nil
}

// inMember reports whether left is a member of right. right must be a
// list ([]celValue) of comparable values; left may be a string or
// int64.
func inMember(left, right celValue) (bool, error) {
	list, ok := right.([]celValue)
	if !ok {
		return false, fmt.Errorf("cel: 'in' rhs not a list: %T", right)
	}
	for _, e := range list {
		if valuesEqual(left, e) {
			return true, nil
		}
	}
	return false, nil
}

func valuesEqual(a, b celValue) bool {
	switch av := a.(type) {
	case string:
		bv, ok := b.(string)
		return ok && av == bv
	case int64:
		bv, ok := b.(int64)
		return ok && av == bv
	case bool:
		bv, ok := b.(bool)
		return ok && av == bv
	}
	return false
}

// compare applies a binary comparison operator to two scalar values.
// Strings compare lexicographically; ints numerically; bools only via
// ==/!=.
func compare(op string, left, right celValue) (bool, error) {
	switch op {
	case "==":
		return valuesEqual(left, right), nil
	case "!=":
		return !valuesEqual(left, right), nil
	}
	// ordered comparisons require ordered operands
	ls, lok := left.(string)
	rs, rok := right.(string)
	if lok && rok {
		switch op {
		case "<":
			return ls < rs, nil
		case "<=":
			return ls <= rs, nil
		case ">":
			return ls > rs, nil
		case ">=":
			return ls >= rs, nil
		}
	}
	li, lok := left.(int64)
	ri, rok := right.(int64)
	if lok && rok {
		switch op {
		case "<":
			return li < ri, nil
		case "<=":
			return li <= ri, nil
		case ">":
			return li > ri, nil
		case ">=":
			return li >= ri, nil
		}
	}
	return false, fmt.Errorf("cel: cannot apply %q to %T and %T", op, left, right)
}

// parsePrimary parses the smallest standalone unit: parenthesised
// expressions, literals, and attribute references.
func (p *parser) parsePrimary() (celValue, error) {
	switch p.tok.kind {
	case tokOp:
		if p.tok.text == "(" {
			p.advance()
			v, err := p.parseExpr()
			if err != nil {
				return nil, err
			}
			if p.tok.kind != tokOp || p.tok.text != ")" {
				return nil, fmt.Errorf("cel: expected ')' got %q", p.tok.text)
			}
			p.advance()
			return v, nil
		}
		return nil, fmt.Errorf("cel: unexpected operator %q", p.tok.text)
	case tokInt:
		n, err := strconv.ParseInt(p.tok.text, 10, 64)
		if err != nil {
			return nil, fmt.Errorf("cel: bad int %q: %w", p.tok.text, err)
		}
		p.advance()
		return n, nil
	case tokStr:
		v := p.tok.text
		p.advance()
		return v, nil
	case tokIdent:
		return p.parseAttrRef()
	}
	return nil, fmt.Errorf("cel: unexpected token %q", p.tok.text)
}

// parseAttrRef resolves a bare or `node.<field>` attribute reference
// against the node being evaluated. Bare identifiers (e.g. `region`)
// resolve against the same attribute map as `node.region`; the PRD
// examples use both forms interchangeably (see
// TestParseMarkdown_ConstraintsInlineArray).
func (p *parser) parseAttrRef() (celValue, error) {
	name := p.tok.text
	p.advance()
	// dotted access: node.<field>
	if p.tok.kind == tokOp && p.tok.text == "." {
		if name != "node" {
			return nil, fmt.Errorf("cel: dotted access on non-node: %q", name)
		}
		p.advance()
		if p.tok.kind != tokIdent {
			return nil, fmt.Errorf("cel: expected attribute name after '.', got %q", p.tok.text)
		}
		field := p.tok.text
		p.advance()
		return p.nodeAttr(name + "." + field)
	}
	// bare identifier
	switch name {
	case "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return p.nodeAttr(name)
	}
}

// nodeAttr resolves an attribute name to its value on the parser's
// active node. Mapping (per PRD T2):
//
//	node.hostname -> Hostname (string)
//	node.kind     -> Kind     (string)
//	node.cpus     -> CPU      (int64)
//	node.memory   -> Memory   (int64)
//	node.tags     -> Tags     ([]string -> []celValue)
//	node.runtimes -> Runtimes ([]string -> []celValue)
//
// Bare names (without the `node.` prefix) resolve through the same
// map, so `region == "us"` and `node.region == "us"` are equivalent
// when the attribute exists.
func (p *parser) nodeAttr(name string) (celValue, error) {
	switch name {
	case "node.hostname", "hostname":
		return p.node.Hostname, nil
	case "node.kind", "kind":
		return p.node.Kind, nil
	case "node.cpus", "cpus":
		return p.node.CPU, nil
	case "node.memory", "memory":
		return p.node.Memory, nil
	case "node.tags", "tags":
		return toStringValues(p.node.Tags), nil
	case "node.runtimes", "runtimes":
		return toStringValues(p.node.Runtimes), nil
	}
	return nil, fmt.Errorf("cel: unknown attribute %q", name)
}

// toStringValues converts a []string to []celValue so the membership
// operators can compare element-wise.
func toStringValues(in []string) []celValue {
	out := make([]celValue, len(in))
	for i, s := range in {
		out[i] = s
	}
	return out
}
