package approvalir

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

// CriteriaMode selects the structured neutral representation of a predicate.
// Product decoders resolve product-specific source precedence before returning
// this execution IR. Formula source is a separate representation whose grammar
// must be validated through FormulaValidator at the decoder boundary.
type CriteriaMode string

const (
	CriteriaModeItems CriteriaMode = "items"
)

// Valid reports whether m is a defined CriteriaMode.
func (m CriteriaMode) Valid() bool {
	return m == CriteriaModeItems
}

// CriteriaOperator is the closed comparison vocabulary understood by neutral
// approval engines. Product decoders translate wire tokens into these values.
type CriteriaOperator string

const (
	CriteriaOperatorGreaterOrEqual CriteriaOperator = "greater_or_equal"
	CriteriaOperatorLessOrEqual    CriteriaOperator = "less_or_equal"
	CriteriaOperatorGreaterThan    CriteriaOperator = "greater_than"
	CriteriaOperatorLessThan       CriteriaOperator = "less_than"
	CriteriaOperatorNotEqual       CriteriaOperator = "not_equal"
	CriteriaOperatorNotContain     CriteriaOperator = "not_contain"
	CriteriaOperatorStartsWith     CriteriaOperator = "starts_with"
	CriteriaOperatorContains       CriteriaOperator = "contains"
	CriteriaOperatorExcludes       CriteriaOperator = "excludes"
	CriteriaOperatorIncludes       CriteriaOperator = "includes"
	CriteriaOperatorEquals         CriteriaOperator = "equals"
)

// Valid reports whether o is a defined CriteriaOperator.
func (o CriteriaOperator) Valid() bool {
	switch o {
	case CriteriaOperatorGreaterOrEqual, CriteriaOperatorLessOrEqual,
		CriteriaOperatorGreaterThan, CriteriaOperatorLessThan,
		CriteriaOperatorNotEqual, CriteriaOperatorNotContain,
		CriteriaOperatorStartsWith, CriteriaOperatorContains,
		CriteriaOperatorExcludes, CriteriaOperatorIncludes,
		CriteriaOperatorEquals:
		return true
	default:
		return false
	}
}

// FormulaValidator is the app-blind syntax and executable-surface gate for
// formula source. The host binds this interface to its single formula-language
// authority; product decoders must not implement a second parser.
type FormulaValidator interface {
	ValidateFormula(string) error
}

// FormulaValidatorFunc adapts a formula validation function.
type FormulaValidatorFunc func(string) error

// ValidateFormula calls f and rejects a nil validator.
func (f FormulaValidatorFunc) ValidateFormula(source string) error {
	if f == nil {
		return fmt.Errorf("formula validator is nil")
	}
	return f(source)
}

// Criteria is a resolved neutral predicate. Formula is generic formula-engine
// source text; when present it is the complete predicate and Mode must be
// empty. Formula source is opaque to this stdlib-only package, so Validate
// rejects it unless a DefinitionDecoder explicitly supplies FormulaValidator.
// Otherwise Items are implicitly conjoined when BooleanFilter is empty.
type Criteria struct {
	Mode          CriteriaMode
	Formula       string
	BooleanFilter string
	Items         []CriteriaItem
}

// CriteriaItem is one structured approval predicate term.
type CriteriaItem struct {
	Field     string
	Operation CriteriaOperator
	Value     string
}

// Validate rejects unknown modes and operators, incomplete item predicates,
// and malformed item-selection logic.
func (c *Criteria) Validate() error {
	return c.validate(nil)
}

func (c *Criteria) validate(formulaValidator FormulaValidator) error {
	if c == nil {
		return fmt.Errorf("criteria is nil")
	}
	if strings.TrimSpace(c.Formula) != "" {
		if c.Mode != "" {
			return fmt.Errorf("formula criteria must not set a structured mode")
		}
		if strings.TrimSpace(c.BooleanFilter) != "" || len(c.Items) != 0 {
			return fmt.Errorf("formula criteria must not include structured items or a boolean filter")
		}
		if formulaValidator == nil {
			return fmt.Errorf("formula criteria require a formula validator at the decoder boundary")
		}
		if err := formulaValidator.ValidateFormula(c.Formula); err != nil {
			return fmt.Errorf("formula criteria are invalid: %w", err)
		}
		return nil
	}
	if !c.Mode.Valid() {
		return fmt.Errorf("invalid mode %q", c.Mode)
	}
	if len(c.Items) == 0 {
		return fmt.Errorf("items mode requires at least one item")
	}
	for index, item := range c.Items {
		if strings.TrimSpace(item.Field) == "" {
			return fmt.Errorf("item %d field is required", index)
		}
		if !item.Operation.Valid() {
			return fmt.Errorf("item %d has invalid operation %q", index, item.Operation)
		}
	}
	if err := validateCriteriaBooleanFilter(c.BooleanFilter, len(c.Items)); err != nil {
		return err
	}
	return nil
}

func validateCriteriaBooleanFilter(filter string, itemCount int) error {
	parser := criteriaBooleanFilterParser{
		input:      strings.TrimSpace(filter),
		itemCount:  itemCount,
		referenced: make([]bool, itemCount),
	}
	if parser.input == "" {
		return nil
	}
	if err := parser.parseExpression(); err != nil {
		return fmt.Errorf("boolean filter: %w", err)
	}
	parser.skipSpace()
	if !parser.eof() {
		return fmt.Errorf("boolean filter: unexpected token %q at offset %d", parser.remaining(), parser.pos)
	}
	for index, referenced := range parser.referenced {
		if !referenced {
			return fmt.Errorf("boolean filter: item reference %d is unused", index+1)
		}
	}
	return nil
}

type criteriaBooleanFilterParser struct {
	input      string
	pos        int
	itemCount  int
	referenced []bool
}

func (p *criteriaBooleanFilterParser) parseExpression() error {
	return p.parseOr()
}

func (p *criteriaBooleanFilterParser) parseOr() error {
	if err := p.parseAnd(); err != nil {
		return err
	}
	for p.consumeWord("OR") {
		if err := p.parseAnd(); err != nil {
			return err
		}
	}
	return nil
}

func (p *criteriaBooleanFilterParser) parseAnd() error {
	if err := p.parseUnary(); err != nil {
		return err
	}
	for p.consumeWord("AND") {
		if err := p.parseUnary(); err != nil {
			return err
		}
	}
	return nil
}

func (p *criteriaBooleanFilterParser) parseUnary() error {
	if p.consumeWord("NOT") {
		return p.parseUnary()
	}
	return p.parsePrimary()
}

func (p *criteriaBooleanFilterParser) parsePrimary() error {
	p.skipSpace()
	if p.eof() {
		return fmt.Errorf("expected item reference or '(' at end")
	}
	if p.consumeByte('(') {
		if err := p.parseExpression(); err != nil {
			return err
		}
		if !p.consumeByte(')') {
			return fmt.Errorf("expected ')' at offset %d", p.pos)
		}
		return nil
	}
	if p.currentIsDigit() {
		return p.parseReference()
	}
	return fmt.Errorf("expected item reference or '(' at offset %d", p.pos)
}

func (p *criteriaBooleanFilterParser) parseReference() error {
	start := p.pos
	for !p.eof() && p.currentIsDigit() {
		p.pos++
	}
	raw := p.input[start:p.pos]
	reference, err := strconv.Atoi(raw)
	if err != nil || reference < 1 {
		return fmt.Errorf("invalid item reference %q", raw)
	}
	if reference > p.itemCount {
		return fmt.Errorf("item reference %d out of range 1..%d", reference, p.itemCount)
	}
	p.referenced[reference-1] = true
	return nil
}

func (p *criteriaBooleanFilterParser) consumeWord(word string) bool {
	p.skipSpace()
	if p.pos > 0 && isCriteriaFilterWordByte(p.input[p.pos-1]) {
		return false
	}
	end := p.pos + len(word)
	if end > len(p.input) || !strings.EqualFold(p.input[p.pos:end], word) {
		return false
	}
	if end < len(p.input) && isCriteriaFilterWordByte(p.input[end]) {
		return false
	}
	p.pos = end
	return true
}

func (p *criteriaBooleanFilterParser) consumeByte(ch byte) bool {
	p.skipSpace()
	if p.eof() || p.input[p.pos] != ch {
		return false
	}
	p.pos++
	return true
}

func (p *criteriaBooleanFilterParser) skipSpace() {
	for !p.eof() && strings.ContainsRune(" \t\n\r", rune(p.input[p.pos])) {
		p.pos++
	}
}

func (p *criteriaBooleanFilterParser) eof() bool {
	return p.pos >= len(p.input)
}

func (p *criteriaBooleanFilterParser) currentIsDigit() bool {
	return !p.eof() && p.input[p.pos] >= '0' && p.input[p.pos] <= '9'
}

func (p *criteriaBooleanFilterParser) remaining() string {
	if p.eof() {
		return ""
	}
	return p.input[p.pos:]
}

func isCriteriaFilterWordByte(ch byte) bool {
	return ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9' || ch == '_'
}
