package chain

import (
	"fmt"
	"net/netip"
	"strings"

	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
	"git.frostfs.info/TrueCloudLab/policy-engine/util"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	"golang.org/x/exp/slices"
)

// ID is the ID of rule chain.
type ID []byte

// MatchType is the match type for chain rules.
type MatchType uint8

const (
	// MatchTypeDenyPriority rejects the request if any `Deny` is specified.
	MatchTypeDenyPriority MatchType = 0
	// MatchTypeFirstMatch returns the first rule action matched to the request.
	MatchTypeFirstMatch MatchType = 1
)

//easyjson:json
type Chain struct {
	ID ID

	Rules []Rule

	MatchType MatchType
}

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

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

type Rule struct {
	Status Status
	// Actions the operation is applied to.
	Actions Actions
	// List of the resources the operation is applied to.
	Resources Resources
	// 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 Actions struct {
	Inverted bool
	Names    []string
}

type Resources struct {
	Inverted bool
	Names    []string
}

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

type ObjectType byte

const (
	ObjectResource ObjectType = iota
	ObjectRequest
	ContainerResource
	ContainerRequest
)

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

	CondSliceContains

	CondIPAddress
	CondNotIPAddress
)

var condToStr = []struct {
	ct  ConditionType
	str string
}{
	{CondStringEquals, "StringEquals"},
	{CondStringNotEquals, "StringNotEquals"},
	{CondStringEqualsIgnoreCase, "StringEqualsIgnoreCase"},
	{CondStringNotEqualsIgnoreCase, "StringNotEqualsIgnoreCase"},
	{CondStringLike, "StringLike"},
	{CondStringNotLike, "StringNotLike"},
	{CondStringLessThan, "StringLessThan"},
	{CondStringLessThanEquals, "StringLessThanEquals"},
	{CondStringGreaterThan, "StringGreaterThan"},
	{CondStringGreaterThanEquals, "StringGreaterThanEquals"},
	{CondNumericEquals, "NumericEquals"},
	{CondNumericNotEquals, "NumericNotEquals"},
	{CondNumericLessThan, "NumericLessThan"},
	{CondNumericLessThanEquals, "NumericLessThanEquals"},
	{CondNumericGreaterThan, "NumericGreaterThan"},
	{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
	{CondSliceContains, "SliceContains"},
	{CondIPAddress, "IPAddress"},
	{CondNotIPAddress, "NotIPAddress"},
}

func (c ConditionType) String() string {
	for _, v := range condToStr {
		if v.ct == c {
			return v.str
		}
	}
	return "unknown condition type"
}

const condSliceContainsDelimiter = "\x00"

// FormCondSliceContainsValue builds value for ObjectResource or ObjectRequest property
// that can be matched by CondSliceContains condition.
func FormCondSliceContainsValue(values []string) string {
	return strings.Join(values, condSliceContainsDelimiter)
}

func (c *Condition) Match(req resource.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: %d", 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 util.GlobMatch(val, c.Value)
	case CondStringNotLike:
		return !util.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
	case CondSliceContains:
		return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
	case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
		CondNumericGreaterThanEquals:
		return c.matchNumeric(val)
	case CondIPAddress, CondNotIPAddress:
		return c.matchIP(val)
	}
}

func (c *Condition) matchNumeric(val string) bool {
	valDecimal, err := fixedn.Fixed8FromString(val)
	if err != nil {
		return c.Op == CondNumericNotEquals
	}

	condVal, err := fixedn.Fixed8FromString(c.Value)
	if err != nil {
		return c.Op == CondNumericNotEquals
	}

	switch c.Op {
	default:
		panic(fmt.Sprintf("unimplemented: %d", c.Op))
	case CondNumericEquals:
		return valDecimal.Equal(condVal)
	case CondNumericNotEquals:
		return !valDecimal.Equal(condVal)
	case CondNumericLessThan:
		return valDecimal.LessThan(condVal)
	case CondNumericLessThanEquals:
		return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
	case CondNumericGreaterThan:
		return valDecimal.GreaterThan(condVal)
	case CondNumericGreaterThanEquals:
		return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal)
	}
}

func (c *Condition) matchIP(val string) bool {
	ipAddr, err := netip.ParseAddr(val)
	if err != nil {
		return false
	}

	prefix, err := netip.ParsePrefix(c.Value)
	if err != nil {
		return false
	}

	switch c.Op {
	default:
		panic(fmt.Sprintf("unimplemented: %d", c.Op))
	case CondIPAddress:
		return prefix.Contains(ipAddr)
	case CondNotIPAddress:
		return !prefix.Contains(ipAddr)
	}
}

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

func (r *Rule) matchCondition(obj resource.Request) (status Status, matched bool) {
	if r.Any {
		return r.matchAny(obj)
	}
	return r.matchAll(obj)
}

func (r *Rule) matchAny(obj resource.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 resource.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 resource.Request) (status Status, matched bool) {
	switch c.MatchType {
	case MatchTypeDenyPriority:
		return c.denyPriority(req)
	case MatchTypeFirstMatch:
		return c.firstMatch(req)
	default:
		panic(fmt.Sprintf("unknown MatchType %d", c.MatchType))
	}
}

func (c *Chain) firstMatch(req resource.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
}

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