package rule

import (
	"context"

	"github.com/expr-lang/expr"
	"github.com/expr-lang/expr/vm"
	"github.com/pkg/errors"
)

type Engine[V any] struct {
	rules []*vm.Program
}

func (e *Engine[V]) Apply(ctx context.Context, vars V) ([]any, error) {
	type Env[V any] struct {
		Context context.Context `expr:"ctx"`
		Vars    V               `expr:"vars"`
	}

	env := Env[V]{
		Context: ctx,
		Vars:    vars,
	}

	results := make([]any, 0, len(e.rules))
	for i, r := range e.rules {
		result, err := expr.Run(r, env)
		if err != nil {
			return nil, errors.Wrapf(err, "could not run rule #%d", i)
		}

		results = append(results, result)
	}

	return results, nil
}

func NewEngine[E any](funcs ...OptionFunc) (*Engine[E], error) {
	opts := NewOptions(funcs...)

	engine := &Engine[E]{
		rules: make([]*vm.Program, 0, len(opts.Rules)),
	}

	for i, r := range opts.Rules {
		program, err := expr.Compile(r, opts.Expr...)
		if err != nil {
			return nil, errors.Wrapf(err, "could not compile rule #%d", i)
		}

		engine.rules = append(engine.rules, program)
	}

	return engine, nil
}

func Context[T any](ctx context.Context, key any) (T, bool) {
	raw := ctx.Value(key)
	if raw == nil {
		return *new(T), false
	}

	value, err := Assert[T](raw)
	if err != nil {
		return *new(T), false
	}

	return value, true
}

func Assert[T any](raw any) (T, error) {
	value, ok := raw.(T)
	if !ok {
		return *new(T), errors.Errorf("unexpected value '%T'", value)
	}

	return value, nil
}