package util

import (
	"errors"
	"fmt"
	"strings"

	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/flynn-archive/go-shlex"
)

var (
	errInvalidStatementFormat = errors.New("invalid statement format")
	errInvalidConditionFormat = errors.New("invalid condition format")
	errUnknownAction          = errors.New("action is not recognized")
	errUnknownOperation       = errors.New("operation is not recognized")
	errUnknownActionDetail    = errors.New("action detail is not recognized")
	errUnknownBinaryOperator  = errors.New("binary operator is not recognized")
	errUnknownCondObjectType  = errors.New("condition object type is not recognized")
)

// ParseAPEChain parses APE chain rules.
func ParseAPEChain(chain *apechain.Chain, rules []string) error {
	if len(rules) == 0 {
		return errors.New("no APE rules provided")
	}

	for _, rule := range rules {
		r := new(apechain.Rule)
		if err := ParseAPERule(r, rule); err != nil {
			return err
		}
		chain.Rules = append(chain.Rules, *r)
	}

	return nil
}

// ParseAPERule parses access-policy-engine statement from the following form:
// <action>[:action_detail] <operation> [<condition1> ...] <resource>
//
// Examples:
// deny  Object.Put *
// deny:QuotaLimitReached Object.Put *
// allow Object.Put *
// allow Object.Get Object.Resource:Department=HR Object.Request:Actor=ownerA *
//
//nolint:godot
func ParseAPERule(r *apechain.Rule, rule string) error {
	lexemes, err := shlex.Split(rule)
	if err != nil {
		return fmt.Errorf("can't parse rule '%s': %v", rule, err)
	}
	return parseRuleLexemes(r, lexemes)
}

func parseRuleLexemes(r *apechain.Rule, lexemes []string) error {
	if len(lexemes) < 2 {
		return errInvalidStatementFormat
	}

	var err error
	r.Status, err = parseStatus(lexemes[0])
	if err != nil {
		return err
	}

	r.Actions, err = parseAction(lexemes[1])
	if err != nil {
		return err
	}

	r.Condition, err = parseConditions(lexemes[2 : len(lexemes)-1])
	if err != nil {
		return err
	}

	r.Resources, err = parseResource(lexemes[len(lexemes)-1])
	return err
}

func parseStatus(lexeme string) (apechain.Status, error) {
	action, expression, found := strings.Cut(lexeme, ":")
	switch action = strings.ToLower(action); action {
	case "deny":
		if !found {
			return apechain.AccessDenied, nil
		} else if strings.EqualFold(expression, "QuotaLimitReached") {
			return apechain.QuotaLimitReached, nil
		} else {
			return 0, fmt.Errorf("%w: %s", errUnknownActionDetail, expression)
		}
	case "allow":
		if found {
			return 0, errUnknownActionDetail
		}
		return apechain.Allow, nil
	default:
		return 0, errUnknownAction
	}
}

func parseAction(lexeme string) (apechain.Actions, error) {
	switch strings.ToLower(lexeme) {
	case "object.put":
		return apechain.Actions{Names: []string{nativeschema.MethodPutObject}}, nil
	case "object.get":
		return apechain.Actions{Names: []string{nativeschema.MethodGetObject}}, nil
	case "object.head":
		return apechain.Actions{Names: []string{nativeschema.MethodHeadObject}}, nil
	case "object.delete":
		return apechain.Actions{Names: []string{nativeschema.MethodDeleteObject}}, nil
	case "object.search":
		return apechain.Actions{Names: []string{nativeschema.MethodSearchObject}}, nil
	case "object.range":
		return apechain.Actions{Names: []string{nativeschema.MethodRangeObject}}, nil
	case "object.hash":
		return apechain.Actions{Names: []string{nativeschema.MethodHashObject}}, nil
	default:
	}
	return apechain.Actions{}, fmt.Errorf("%w: %s", errUnknownOperation, lexeme)
}

func parseResource(lexeme string) (apechain.Resources, error) {
	if lexeme == "*" {
		return apechain.Resources{Names: []string{nativeschema.ResourceFormatRootObjects}}, nil
	}
	return apechain.Resources{Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, lexeme)}}, nil
}

const (
	ObjectResource = "object.resource"
	ObjectRequest  = "object.request"
)

var typeToCondObject = map[string]apechain.ObjectType{
	ObjectResource: apechain.ObjectResource,
	ObjectRequest:  apechain.ObjectRequest,
}

func parseConditions(lexemes []string) ([]apechain.Condition, error) {
	conds := make([]apechain.Condition, 0)

	for _, lexeme := range lexemes {
		typ, expression, found := strings.Cut(lexeme, ":")
		typ = strings.ToLower(typ)

		objType, ok := typeToCondObject[typ]
		if ok {
			if !found {
				return nil, fmt.Errorf("%w: %s", errInvalidConditionFormat, lexeme)
			}

			var lhs, rhs string
			var binExpFound bool

			var cond apechain.Condition
			cond.Object = objType

			lhs, rhs, binExpFound = strings.Cut(expression, "!=")
			if !binExpFound {
				lhs, rhs, binExpFound = strings.Cut(expression, "=")
				if !binExpFound {
					return nil, fmt.Errorf("%w: %s", errUnknownBinaryOperator, expression)
				}
				cond.Op = apechain.CondStringEquals
			} else {
				cond.Op = apechain.CondStringNotEquals
			}

			cond.Key, cond.Value = lhs, rhs

			conds = append(conds, cond)
		} else {
			return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
		}
	}

	return conds, nil
}