package iam

import (
	"fmt"
	"strings"

	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)

const PropertyKeyFilePath = "FilePath"

var actionToNativeOpMap = map[string][]string{
	s3ActionAbortMultipartUpload:             {native.MethodGetContainer, native.MethodDeleteObject, native.MethodHeadObject},
	s3ActionCreateBucket:                     {native.MethodGetContainer, native.MethodPutContainer, native.MethodSetContainerEACL},
	s3ActionDeleteBucket:                     {native.MethodGetContainer, native.MethodDeleteContainer, native.MethodSearchObject, native.MethodHeadObject},
	s3ActionDeleteBucketPolicy:               {native.MethodGetContainer},
	s3ActionDeleteObject:                     {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
	s3ActionDeleteObjectTagging:              {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionDeleteObjectVersion:              {native.MethodGetContainer, native.MethodDeleteObject, native.MethodPutObject, native.MethodHeadObject, native.MethodGetObject, native.MethodRangeObject},
	s3ActionDeleteObjectVersionTagging:       {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionGetBucketACL:                     {native.MethodGetContainer, native.MethodGetContainerEACL},
	s3ActionGetBucketCORS:                    {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetBucketLocation:                {native.MethodGetContainer},
	s3ActionGetBucketNotification:            {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetBucketObjectLockConfiguration: {native.MethodGetContainer},
	s3ActionGetBucketPolicy:                  {native.MethodGetContainer},
	s3ActionGetBucketPolicyStatus:            {native.MethodGetContainer},
	s3ActionGetBucketTagging:                 {native.MethodGetContainer},
	s3ActionGetBucketVersioning:              {native.MethodGetContainer},
	s3ActionGetLifecycleConfiguration:        { /*not implemented yet*/ },
	s3ActionGetObject:                        {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	s3ActionGetObjectACL:                     {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetObjectAttributes:              {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetObjectLegalHold:               {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionGetObjectRetention:               {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionGetObjectTagging:                 {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionGetObjectVersion:                 {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	s3ActionGetObjectVersionACL:              {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetObjectVersionAttributes:       {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject},
	s3ActionGetObjectVersionTagging:          {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionListAllMyBuckets:                 {native.MethodListContainers, native.MethodGetContainer},
	s3ActionListBucket:                       {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	s3ActionListBucketMultipartUploads:       {native.MethodGetContainer},
	s3ActionListBucketVersions:               {native.MethodGetContainer, native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	s3ActionListMultipartUploadParts:         {native.MethodGetContainer},
	s3ActionPutBucketACL:                     {native.MethodGetContainer, native.MethodSetContainerEACL},
	s3ActionPutBucketCORS:                    {native.MethodGetContainer},
	s3ActionPutBucketNotification:            {native.MethodGetContainer, native.MethodHeadObject, native.MethodDeleteObject, native.MethodHeadObject},
	s3ActionPutBucketObjectLockConfiguration: {native.MethodGetContainer},
	s3ActionPutBucketPolicy:                  {native.MethodGetContainer},
	s3ActionPutBucketTagging:                 {native.MethodGetContainer},
	s3ActionPutBucketVersioning:              {native.MethodGetContainer},
	s3ActionPutLifecycleConfiguration:        { /*not implemented yet*/ },
	s3ActionPutObject:                        {native.MethodGetContainer, native.MethodPutObject},
	s3ActionPutObjectACL:                     {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
	s3ActionPutObjectLegalHold:               {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionPutObjectRetention:               {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionPutObjectTagging:                 {native.MethodGetContainer, native.MethodHeadObject},
	s3ActionPutObjectVersionACL:              {native.MethodGetContainer, native.MethodGetContainerEACL, native.MethodSetContainerEACL, native.MethodGetObject, native.MethodHeadObject},
	s3ActionPutObjectVersionTagging:          {native.MethodGetContainer, native.MethodHeadObject},
}

var containerNativeOperations = map[string]struct{}{
	native.MethodPutContainer:     {},
	native.MethodDeleteContainer:  {},
	native.MethodGetContainer:     {},
	native.MethodListContainers:   {},
	native.MethodSetContainerEACL: {},
	native.MethodGetContainerEACL: {},
}

var objectNativeOperations = map[string]struct{}{
	native.MethodGetObject:    {},
	native.MethodPutObject:    {},
	native.MethodHeadObject:   {},
	native.MethodDeleteObject: {},
	native.MethodSearchObject: {},
	native.MethodRangeObject:  {},
	native.MethodHashObject:   {},
}

type NativeResolver interface {
	GetUserKey(account, name string) (string, error)
	GetBucketInfo(bucket string) (*BucketInfo, error)
}

type BucketInfo struct {
	Namespace string
	Container string
}

func ConvertToNativeChain(p Policy, resolver NativeResolver) (*chain.Chain, error) {
	if err := p.Validate(ResourceBasedPolicyType); err != nil {
		return nil, err
	}

	var engineChain chain.Chain

	for _, statement := range p.Statement {
		status := formStatus(statement)
		if status != chain.Allow {
			// Most s3 methods share the same native operations. Deny rules must not affect shared native operations,
			// therefore this code skips all deny rules for native protocol. Deny is applied for s3 protocol only, in this case.
			continue
		}

		action, actionInverted := statement.action()
		nativeActions, err := formNativeActionNames(action)
		if err != nil {
			return nil, err
		}
		ruleAction := chain.Actions{Inverted: actionInverted, Names: nativeActions}
		if len(ruleAction.Names) == 0 {
			continue
		}

		resource, resourceInverted := statement.resource()
		groupedResources, err := formNativeResourceNamesAndConditions(resource, resolver, getActionTypes(nativeActions))
		if err != nil {
			return nil, err
		}

		groupedConditions, err := convertToNativeChainCondition(statement.Conditions, resolver)
		if err != nil {
			return nil, err
		}
		splitConditions := splitGroupedConditions(groupedConditions)

		principals, principalCondFn, err := getNativePrincipalsAndConditionFunc(statement, resolver)
		if err != nil {
			return nil, err
		}

		for _, groupedResource := range groupedResources {
			for _, principal := range principals {
				for _, conditions := range splitConditions {
					var principalCondition []chain.Condition
					if principal != Wildcard {
						principalCondition = []chain.Condition{principalCondFn(principal)}
					}

					ruleConditions := append(principalCondition, groupedResource.Conditions...)

					r := chain.Rule{
						Status:  status,
						Actions: ruleAction,
						Resources: chain.Resources{
							Inverted: resourceInverted,
							Names:    groupedResource.Names,
						},
						Condition: append(ruleConditions, conditions...),
					}
					engineChain.Rules = append(engineChain.Rules, r)
				}
			}
		}
	}

	if len(engineChain.Rules) == 0 {
		return nil, ErrActionsNotApplicable
	}

	return &engineChain, nil
}

func getActionTypes(nativeActions []string) ActionTypes {
	var res ActionTypes
	for _, action := range nativeActions {
		if res.Object && res.Container {
			break
		}

		_, isObj := objectNativeOperations[action]
		_, isCnr := containerNativeOperations[action]

		res.Object = res.Object || isObj || action == Wildcard
		res.Container = res.Container || isCnr || action == Wildcard
	}

	return res
}

func getNativePrincipalsAndConditionFunc(statement Statement, resolver NativeResolver) ([]string, formPrincipalConditionFunc, error) {
	var principals []string
	var op chain.ConditionType
	statementPrincipal, inverted := statement.principal()
	if _, ok := statementPrincipal[Wildcard]; ok { // this can be true only if 'inverted' false
		principals = []string{Wildcard}
		op = chain.CondStringLike
	} else {
		for principalType, principal := range statementPrincipal {
			if principalType != AWSPrincipalType {
				return nil, nil, fmt.Errorf("unsupported principal type '%s'", principalType)
			}
			parsedPrincipal, err := formNativePrincipal(principal, resolver)
			if err != nil {
				return nil, nil, fmt.Errorf("parse principal: %w", err)
			}
			principals = append(principals, parsedPrincipal...)
		}

		op = chain.CondStringEquals
		if inverted {
			op = chain.CondStringNotEquals
		}
	}

	return principals, func(principal string) chain.Condition {
		return chain.Condition{
			Op:     op,
			Object: chain.ObjectRequest,
			Key:    native.PropertyKeyActorPublicKey,
			Value:  principal,
		}
	}, nil
}

func convertToNativeChainCondition(c Conditions, resolver NativeResolver) ([]GroupedConditions, error) {
	return convertToChainConditions(c, func(gr GroupedConditions) (GroupedConditions, error) {
		for i := range gr.Conditions {
			if gr.Conditions[i].Key == condKeyAWSPrincipalARN {
				gr.Conditions[i].Key = native.PropertyKeyActorPublicKey
				val, err := formPrincipalKey(gr.Conditions[i].Value, resolver)
				if err != nil {
					return GroupedConditions{}, err
				}
				gr.Conditions[i].Value = val
			}
		}

		return gr, nil
	})
}

type GroupedResources struct {
	Names      []string
	Conditions []chain.Condition
}

type ActionTypes struct {
	Object    bool
	Container bool
}

func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver, actionTypes ActionTypes) ([]GroupedResources, error) {
	if !actionTypes.Object && !actionTypes.Container {
		return nil, ErrActionsNotApplicable
	}

	res := make([]GroupedResources, 0, len(names))

	combined := make(map[string]struct{})

	for _, resource := range names {
		if err := validateResource(resource); err != nil {
			return nil, err
		}

		if resource == Wildcard {
			res = res[:0]
			return append(res, formWildcardNativeResource(actionTypes)), nil
		}

		if !strings.HasPrefix(resource, s3ResourcePrefix) {
			continue
		}

		var bkt, obj string
		s3Resource := strings.TrimPrefix(resource, s3ResourcePrefix)
		if s3Resource == Wildcard {
			res = res[:0]
			return append(res, formWildcardNativeResource(actionTypes)), nil
		}

		if sepIndex := strings.Index(s3Resource, "/"); sepIndex < 0 {
			bkt = s3Resource
		} else {
			bkt = s3Resource[:sepIndex]
			obj = s3Resource[sepIndex+1:]
			if len(obj) == 0 {
				obj = Wildcard
			}
		}

		bktInfo, err := resolver.GetBucketInfo(bkt)
		if err != nil {
			return nil, err
		}

		if obj == Wildcard && actionTypes.Object { // this corresponds to arn:aws:s3:::BUCKET/ or arn:aws:s3:::BUCKET/*
			combined[fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
			combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
			continue
		}
		if obj == "" && actionTypes.Container { // this corresponds to arn:aws:s3:::BUCKET
			combined[fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container)] = struct{}{}
			continue
		}

		res = append(res, GroupedResources{
			Names: []string{
				fmt.Sprintf(native.ResourceFormatNamespaceContainerObjects, bktInfo.Namespace, bktInfo.Container),
				fmt.Sprintf(native.ResourceFormatNamespaceContainer, bktInfo.Namespace, bktInfo.Container),
			},
			Conditions: []chain.Condition{
				{
					Op:     chain.CondStringLike,
					Object: chain.ObjectResource,
					Key:    PropertyKeyFilePath,
					Value:  obj,
				},
			},
		})
	}

	if len(combined) != 0 {
		gr := GroupedResources{Names: make([]string, 0, len(combined))}
		for key := range combined {
			gr.Names = append(gr.Names, key)
		}

		res = append(res, gr)
	}

	return res, nil
}

func formWildcardNativeResource(actionTypes ActionTypes) GroupedResources {
	groupedNames := make([]string, 0, 2)
	if actionTypes.Object {
		groupedNames = append(groupedNames, native.ResourceFormatAllObjects)
	}
	if actionTypes.Container {
		groupedNames = append(groupedNames, native.ResourceFormatAllContainers)
	}

	return GroupedResources{Names: groupedNames}
}

func formNativePrincipal(principal []string, resolver NativeResolver) ([]string, error) {
	res := make([]string, len(principal))

	var err error
	for i := range principal {
		if res[i], err = formPrincipalKey(principal[i], resolver); err != nil {
			return nil, err
		}
	}

	return res, nil
}

func formPrincipalKey(principal string, resolver NativeResolver) (string, error) {
	account, user, err := parsePrincipalAsIAMUser(principal)
	if err != nil {
		return "", err
	}

	key, err := resolver.GetUserKey(account, user)
	if err != nil {
		return "", fmt.Errorf("get user key: %w", err)
	}

	return key, nil
}

func formNativeActionNames(names []string) ([]string, error) {
	uniqueActions := make(map[string]struct{}, len(names))

	for _, action := range names {
		if action == Wildcard {
			return []string{Wildcard}, nil
		}

		isIAM, err := validateAction(action)
		if err != nil {
			return nil, err
		}

		if isIAM {
			continue
		}

		if action[len(s3ActionPrefix):] == Wildcard {
			return []string{Wildcard}, nil
		}

		nativeActions := actionToNativeOpMap[action]
		if len(nativeActions) == 0 {
			return nil, ErrActionsNotApplicable
		}

		for _, nativeAction := range nativeActions {
			uniqueActions[nativeAction] = struct{}{}
		}
	}

	res := make([]string, 0, len(uniqueActions))
	for key := range uniqueActions {
		res = append(res, key)
	}

	return res, nil
}