package util

import (
	"errors"
	"fmt"
	"os"
	"strconv"
	"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"
	"github.com/spf13/cobra"
)

var (
	errInvalidStatementFormat    = errors.New("invalid statement format")
	errInvalidConditionFormat    = errors.New("invalid condition format")
	errUnknownStatus             = errors.New("status is not recognized")
	errUnknownStatusDetail       = errors.New("status detail is not recognized")
	errUnknownAction             = errors.New("action is not recognized")
	errUnknownBinaryOperator     = errors.New("binary operator is not recognized")
	errUnknownCondObjectType     = errors.New("condition object type is not recognized")
	errMixedTypesInRule          = errors.New("found mixed type of actions in rule")
	errNoActionsInRule           = errors.New("there are no actions in rule")
	errUnsupportedResourceFormat = errors.New("unsupported resource format")
	errFailedToParseAllAny       = errors.New("any/all is not parsed")
)

// PrintHumanReadableAPEChain print APE chain rules.
func PrintHumanReadableAPEChain(cmd *cobra.Command, chain *apechain.Chain) {
	cmd.Println("Chain ID: " + string(chain.ID))
	cmd.Printf("     HEX: %x\n", chain.ID)
	cmd.Println("Rules:")
	for _, rule := range chain.Rules {
		cmd.Println("\n\tStatus: " + rule.Status.String())
		cmd.Println("\tAny: " + strconv.FormatBool(rule.Any))
		cmd.Println("\tConditions:")
		for _, c := range rule.Condition {
			var ot string
			switch c.Kind {
			case apechain.KindResource:
				ot = "Resource"
			case apechain.KindRequest:
				ot = "Request"
			default:
				panic("unknown object type")
			}
			cmd.Println(fmt.Sprintf("\t\t%s %s %s %s", ot, c.Key, c.Op, c.Value))
		}
		cmd.Println("\tActions:\tInverted:" + strconv.FormatBool(rule.Actions.Inverted))
		for _, name := range rule.Actions.Names {
			cmd.Println("\t\t" + name)
		}
		cmd.Println("\tResources:\tInverted:" + strconv.FormatBool(rule.Resources.Inverted))
		for _, name := range rule.Resources.Names {
			cmd.Println("\t\t" + name)
		}
	}
}

func ParseAPEChainBinaryOrJSON(chain *apechain.Chain, path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read file <%s>: %w", path, err)
	}

	err = chain.UnmarshalBinary(data)
	if err != nil {
		err = chain.UnmarshalJSON(data)
		if err != nil {
			return fmt.Errorf("invalid format: %w", err)
		}
	}

	return nil
}

// 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:
// <status>[:status_detail] <action>... [<condition>...] <resource>...
//
// Examples:
// deny  Object.Put *
// deny:QuotaLimitReached Object.Put *
// allow Object.Put *
// allow Object.Get ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.Get any ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.Get all ResourceCondition:Department=HR RequestCondition:Actor=ownerA *
// allow Object.* *
// allow Container.* *
//
//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 unique(inputSlice []string) []string {
	uniqueSlice := make([]string, 0, len(inputSlice))
	seen := make(map[string]bool, len(inputSlice))
	for _, element := range inputSlice {
		if !seen[element] {
			uniqueSlice = append(uniqueSlice, element)
			seen[element] = true
		}
	}
	return uniqueSlice
}

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
	}

	var objectTargeted bool
	var containerTargeted bool

	for i, lexeme := range lexemes[1:] {
		anyExpr, anyErr := parseAnyAll(lexeme)
		if anyErr == nil {
			r.Any = anyExpr
			continue
		}

		var names []string
		var actionType bool
		names, actionType, err = parseAction(lexeme)
		if err != nil {
			condition, errCond := parseCondition(lexeme)
			if errCond != nil {
				err = fmt.Errorf("%w:%w", err, errCond)
				lexemes = lexemes[i+1:]
				break
			}
			r.Condition = append(r.Condition, *condition)
		} else {
			if actionType {
				objectTargeted = true
			} else {
				containerTargeted = true
			}
			if objectTargeted && containerTargeted {
				// Actually, APE chain allows to define rules for several resources, for example, if
				// chain target is namespace, but the parser primitevly compiles verbs,
				// conditions and resources in one rule. So, for the parser, one rule relates only to
				// one resource type - object or container.
				return errMixedTypesInRule
			}

			r.Actions.Names = append(r.Actions.Names, names...)
		}
	}
	r.Actions.Names = unique(r.Actions.Names)
	if len(r.Actions.Names) == 0 {
		return fmt.Errorf("%w:%w", err, errNoActionsInRule)
	}
	for _, lexeme := range lexemes {
		resource, errRes := parseResource(lexeme, objectTargeted)
		if errRes != nil {
			return fmt.Errorf("%w:%w", err, errRes)
		}
		r.Resources.Names = append(r.Resources.Names, resource)
	}

	return nil
}

func parseAnyAll(lexeme string) (bool, error) {
	switch strings.ToLower(lexeme) {
	case "any":
		return true, nil
	case "all":
		return false, nil
	default:
		return false, errFailedToParseAllAny
	}
}

func parseStatus(lexeme string) (apechain.Status, error) {
	action, expression, found := strings.Cut(lexeme, ":")
	switch strings.ToLower(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", errUnknownStatusDetail, expression)
		}
	case "allow":
		if found {
			return 0, errUnknownStatusDetail
		}
		return apechain.Allow, nil
	default:
		return 0, errUnknownStatus
	}
}

func parseAction(lexeme string) ([]string, bool, error) {
	switch strings.ToLower(lexeme) {
	case "object.put":
		return []string{nativeschema.MethodPutObject}, true, nil
	case "object.get":
		return []string{nativeschema.MethodGetObject}, true, nil
	case "object.head":
		return []string{nativeschema.MethodHeadObject}, true, nil
	case "object.delete":
		return []string{nativeschema.MethodDeleteObject}, true, nil
	case "object.search":
		return []string{nativeschema.MethodSearchObject}, true, nil
	case "object.range":
		return []string{nativeschema.MethodRangeObject}, true, nil
	case "object.hash":
		return []string{nativeschema.MethodHashObject}, true, nil
	case "object.*":
		return []string{
			nativeschema.MethodPutObject,
			nativeschema.MethodGetObject,
			nativeschema.MethodHeadObject,
			nativeschema.MethodDeleteObject,
			nativeschema.MethodSearchObject,
			nativeschema.MethodHashObject,
		}, true, nil
	case "container.put":
		return []string{nativeschema.MethodPutContainer}, false, nil
	case "container.delete":
		return []string{nativeschema.MethodDeleteContainer}, false, nil
	case "container.get":
		return []string{nativeschema.MethodGetContainer}, false, nil
	case "container.setcontainereacl":
		return []string{nativeschema.MethodSetContainerEACL}, false, nil
	case "container.getcontainereacl":
		return []string{nativeschema.MethodGetContainerEACL}, false, nil
	case "container.list":
		return []string{nativeschema.MethodListContainers}, false, nil
	case "container.*":
		return []string{
			nativeschema.MethodPutContainer,
			nativeschema.MethodDeleteContainer,
			nativeschema.MethodGetContainer,
			nativeschema.MethodSetContainerEACL,
			nativeschema.MethodGetContainerEACL,
			nativeschema.MethodListContainers,
		}, false, nil
	default:
	}
	return nil, false, fmt.Errorf("%w: %s", errUnknownAction, lexeme)
}

func parseResource(lexeme string, isObj bool) (string, error) {
	if len(lexeme) > 0 && !strings.HasSuffix(lexeme, "/") {
		if isObj {
			if lexeme == "*" {
				return nativeschema.ResourceFormatAllObjects, nil
			} else if lexeme == "/*" {
				return nativeschema.ResourceFormatRootObjects, nil
			} else if strings.HasPrefix(lexeme, "/") {
				lexeme = lexeme[1:]
				delimCount := strings.Count(lexeme, "/")
				if delimCount == 1 && len(lexeme) >= 3 { // container/object
					return nativeschema.ObjectPrefix + "//" + lexeme, nil
				}
			} else {
				delimCount := strings.Count(lexeme, "/")
				if delimCount == 1 && len(lexeme) >= 3 ||
					delimCount == 2 && len(lexeme) >= 5 { // namespace/container/object
					return nativeschema.ObjectPrefix + "/" + lexeme, nil
				}
			}
		} else {
			if lexeme == "*" {
				return nativeschema.ResourceFormatAllContainers, nil
			} else if lexeme == "/*" {
				return nativeschema.ResourceFormatRootContainers, nil
			} else if strings.HasPrefix(lexeme, "/") && len(lexeme) > 1 {
				lexeme = lexeme[1:]
				delimCount := strings.Count(lexeme, "/")
				if delimCount == 0 {
					return nativeschema.ContainerPrefix + "//" + lexeme, nil
				}
			} else {
				delimCount := strings.Count(lexeme, "/")
				if delimCount == 1 && len(lexeme) > 3 { // namespace/container
					return nativeschema.ContainerPrefix + "/" + lexeme, nil
				}
			}
		}
	}
	return "", errUnsupportedResourceFormat
}

const (
	ResourceCondition = "resourcecondition"
	RequestCondition  = "requestcondition"
)

var typeToCondKindType = map[string]apechain.ConditionKindType{
	ResourceCondition: apechain.KindResource,
	RequestCondition:  apechain.KindRequest,
}

func parseCondition(lexeme string) (*apechain.Condition, error) {
	typ, expression, found := strings.Cut(lexeme, ":")
	typ = strings.ToLower(typ)

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

		var cond apechain.Condition
		cond.Kind = condKindType

		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
		return &cond, nil
	}
	return nil, fmt.Errorf("%w: %s", errUnknownCondObjectType, typ)
}