package policyengine

import (
	"encoding/json"
	"fmt"
	"strings"
)

// Engine ...
type Engine interface {
	// IsAllowed returns status for the operation after all checks.
	// The second return value signifies whether a matching rule was found.
	IsAllowed(name Name, namespace string, r Request) (Status, bool)
}

// ChainID is the ID of rule chain.
type ChainID string

type Chain struct {
	ID ChainID

	Rules []Rule
}

func (c *Chain) Bytes() []byte {
	data, err := json.Marshal(c)
	if err != nil {
		panic(err)
	}
	return data
}

func (c *Chain) DecodeBytes(b []byte) error {
	return json.Unmarshal(b, c)
}

type Rule struct {
	Status Status
	// Actions the operation is applied to.
	Action []string
	// List of the resources the operation is applied to.
	Resource []string
	// True iff individual conditions must be combined with the logical OR.
	// By default AND is used, so _each_ condition must pass.
	Any       bool
	Condition []Condition
}

type Condition struct {
	Op     ConditionType
	Object ObjectType
	Key    string
	Value  string
}

type ObjectType byte

const (
	ObjectResource ObjectType = iota
	ObjectRequest
	ObjectActor
)

type ConditionType byte

// TODO @fyrchik: reduce the number of conditions.
// Everything from here should be expressable, but we do not need them all.
// https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
const (
	// String condition operators.
	CondStringEquals ConditionType = iota
	CondStringNotEquals
	CondStringEqualsIgnoreCase
	CondStringNotEqualsIgnoreCase
	CondStringLike
	CondStringNotLike
	CondStringLessThan
	CondStringLessThanEquals
	CondStringGreaterThan
	CondStringGreaterThanEquals

	// Numeric condition operators.
	CondNumericEquals
	CondNumericNotEquals
	CondNumericLessThan
	CondNumericLessThanEquals
	CondNumericGreaterThan
	CondNumericGreaterThanEquals
)

func (c *Condition) Match(req Request) bool {
	var val string
	switch c.Object {
	case ObjectResource:
		val = req.Resource().Property(c.Key)
	case ObjectRequest:
		val = req.Property(c.Key)
	default:
		panic(fmt.Sprintf("unknown condition type: %d", c.Object))
	}

	switch c.Op {
	default:
		panic(fmt.Sprintf("unimplemented: %s", c.Op))
	case CondStringEquals:
		return val == c.Value
	case CondStringNotEquals:
		return val != c.Value
	case CondStringEqualsIgnoreCase:
		return strings.EqualFold(val, c.Value)
	case CondStringNotEqualsIgnoreCase:
		return !strings.EqualFold(val, c.Value)
	case CondStringLike:
		return globMatch(val, c.Value)
	case CondStringNotLike:
		return !globMatch(val, c.Value)
	case CondStringLessThan:
		return val < c.Value
	case CondStringLessThanEquals:
		return val <= c.Value
	case CondStringGreaterThan:
		return val > c.Value
	case CondStringGreaterThanEquals:
		return val >= c.Value
	}
}

func (r *Rule) Match(req Request) (status Status, matched bool) {
	found := len(r.Resource) == 0
	for i := range r.Resource {
		if globMatch(req.Resource().Name(), r.Resource[i]) {
			found = true
			break
		}
	}
	if !found {
		return NoRuleFound, false
	}
	for i := range r.Action {
		if globMatch(req.Operation(), r.Action[i]) {
			return r.matchCondition(req)
		}
	}
	return NoRuleFound, false
}

func (r *Rule) matchCondition(obj Request) (status Status, matched bool) {
	if r.Any {
		return r.matchAny(obj)
	}
	return r.matchAll(obj)
}
func (r *Rule) matchAny(obj Request) (status Status, matched bool) {
	for i := range r.Condition {
		if r.Condition[i].Match(obj) {

			return r.Status, true
		}
	}
	return NoRuleFound, false
}
func (r *Rule) matchAll(obj Request) (status Status, matched bool) {
	for i := range r.Condition {
		if !r.Condition[i].Match(obj) {
			return NoRuleFound, false
		}
	}
	return r.Status, true
}

func (c *Chain) Match(req Request) (status Status, matched bool) {
	for i := range c.Rules {
		status, matched := c.Rules[i].Match(req)
		if matched {
			return status, true
		}
	}
	return NoRuleFound, false
}