package eacl

import (
	"bytes"
	"errors"

	"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
	"github.com/nspcc-dev/neofs-node/pkg/core/container"
	"github.com/nspcc-dev/neofs-node/pkg/util/logger"
	"go.uber.org/zap"
)

// Validator is a tool that calculates
// the action on a request according
// to the extended ACL rule table.
type Validator struct {
	*cfg
}

// Option represents Validator option.
type Option func(*cfg)

type cfg struct {
	logger *logger.Logger

	storage Storage
}

func defaultCfg() *cfg {
	return &cfg{
		logger: zap.L(),
	}
}

// NewValidator creates and initializes a new Validator using options.
func NewValidator(opts ...Option) *Validator {
	cfg := defaultCfg()

	for i := range opts {
		opts[i](cfg)
	}

	return &Validator{
		cfg: cfg,
	}
}

// CalculateAction calculates action on the request according
// to its information represented in ValidationUnit.
//
// The action is calculated according to the application of
// eACL table of rules to the request.
//
// If the eACL table is not available at the time of the call,
// eacl.ActionUnknown is returned.
//
// If no matching table entry is found, ActionAllow is returned.
func (v *Validator) CalculateAction(unit *ValidationUnit) eacl.Action {
	var (
		err   error
		table *eacl.Table
	)

	if unit.bearer != nil {
		table = eacl.NewTableFromV2(unit.bearer.GetBody().GetEACL())
	} else {
		// get eACL table by container ID
		table, err = v.storage.GetEACL(unit.cid)
		if err != nil {
			if errors.Is(err, container.ErrEACLNotFound) {
				return eacl.ActionAllow
			}

			v.logger.Error("could not get eACL table",
				zap.String("error", err.Error()),
			)

			return eacl.ActionUnknown
		}
	}

	return tableAction(unit, table)
}

// calculates action on the request based on the eACL rules.
func tableAction(unit *ValidationUnit, table *eacl.Table) eacl.Action {
	for _, record := range table.Records() {
		// check type of operation
		if record.Operation() != unit.op {
			continue
		}

		// check target
		if !targetMatches(unit, record) {
			continue
		}

		// check headers
		switch val := matchFilters(unit.hdrSrc, record.Filters()); {
		case val < 0:
			// headers of some type could not be composed => allow
			return eacl.ActionAllow
		case val == 0:
			return record.Action()
		}
	}

	return eacl.ActionAllow
}

// returns:
//  - positive value if no matching header is found for at least one filter;
//  - zero if at least one suitable header is found for all filters;
//  - negative value if the headers of at least one filter cannot be obtained.
func matchFilters(hdrSrc TypedHeaderSource, filters []*eacl.Filter) int {
	matched := 0

	for _, filter := range filters {
		headers, ok := hdrSrc.HeadersOfType(filter.From())
		if !ok {
			return -1
		}

		// get headers of filtering type
		for _, header := range headers {
			// prevent NPE
			if header == nil {
				continue
			}

			// check header name
			if header.Key() != filter.Key() {
				continue
			}

			// get match function
			matchFn, ok := mMatchFns[filter.Matcher()]
			if !ok {
				continue
			}

			// check match
			if !matchFn(header, filter) {
				continue
			}

			// increment match counter
			matched++

			break
		}
	}

	return len(filters) - matched
}

// returns true if one of ExtendedACLTarget has
// suitable target OR suitable public key.
func targetMatches(unit *ValidationUnit, record *eacl.Record) bool {
	for _, target := range record.Targets() {
		// check public key match
		for _, key := range target.BinaryKeys() {
			if bytes.Equal(key, unit.key) {
				return true
			}
		}

		// check target group match
		if unit.role == target.Role() {
			return true
		}
	}

	return false
}

// Maps match type to corresponding function.
var mMatchFns = map[eacl.Match]func(Header, *eacl.Filter) bool{
	eacl.MatchStringEqual: func(header Header, filter *eacl.Filter) bool {
		return header.Value() == filter.Value()
	},

	eacl.MatchStringNotEqual: func(header Header, filter *eacl.Filter) bool {
		return header.Value() != filter.Value()
	},
}