package rego

import (
	"fmt"

	"github.com/open-policy-agent/opa/v1/ast"
)

// ResultSet represents a collection of output from Rego evaluation. An empty
// result set represents an undefined query.
type ResultSet []Result

// Vars represents a collection of variable bindings. The keys are the variable
// names and the values are the binding values.
type Vars map[string]any

// WithoutWildcards returns a copy of v with wildcard variables removed.
func (v Vars) WithoutWildcards() Vars {
	n := Vars{}
	for k, v := range v {
		if ast.Var(k).IsWildcard() || ast.Var(k).IsGenerated() {
			continue
		}
		n[k] = v
	}
	return n
}

// Result defines the output of Rego evaluation.
type Result struct {
	Expressions []*ExpressionValue `json:"expressions"`
	Bindings    Vars               `json:"bindings,omitempty"`
}

func newResult() Result {
	return Result{
		Bindings: Vars{},
	}
}

// Location defines a position in a Rego query or module.
type Location struct {
	Row int `json:"row"`
	Col int `json:"col"`
}

// ExpressionValue defines the value of an expression in a Rego query.
type ExpressionValue struct {
	Value    any       `json:"value"`
	Text     string    `json:"text"`
	Location *Location `json:"location"`
}

func newExpressionValue(expr *ast.Expr, value any) *ExpressionValue {
	result := &ExpressionValue{
		Value: value,
	}
	if expr.Location != nil {
		result.Text = string(expr.Location.Text)
		result.Location = &Location{
			Row: expr.Location.Row,
			Col: expr.Location.Col,
		}
	}
	return result
}

func (ev *ExpressionValue) String() string {
	return fmt.Sprint(ev.Value)
}

// Allowed is a helper method that'll return true if all of these conditions hold:
// - the result set only has one element
// - there is only one expression in the result set's only element
// - that expression has the value `true`
// - there are no bindings.
//
// If bindings are present, this will yield `false`: it would be a pitfall to
// return `true` for a query like `data.authz.allow = x`, which always has result
// set element with value true, but could also have a binding `x: false`.
func (rs ResultSet) Allowed() bool {
	x, _ := ResultValue[bool](rs)
	return x
}

// ResultValue is a helper function that'll return a value of type T if all of
// these conditions hold:
// - the result set only has one element
// - there is only one expression in the result set's only element
// - that expression has type T
// - there are no bindings.
func ResultValue[T any](rs ResultSet) (T, bool) {
	var zero T
	if len(rs) == 1 && len(rs[0].Bindings) == 0 {
		if exprs := rs[0].Expressions; len(exprs) == 1 {
			if v, ok := exprs[0].Value.(T); ok {
				return v, true
			}
		}
	}
	return zero, false
}
