package iam

import (
	"errors"
	"fmt"

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

const PropertyKeyFilePath = "FilePath"

// ErrActionsNotApplicable occurs when failed to convert any actions.
var ErrActionsNotApplicable = errors.New("actions not applicable")

var actionToOpMap = map[string][]string{
	supportedS3ActionDeleteObject: {native.MethodDeleteObject},
	supportedS3ActionGetObject:    {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	supportedS3ActionHeadObject:   {native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
	supportedS3ActionPutObject:    {native.MethodPutObject},
	supportedS3ActionListBucket:   {native.MethodGetObject, native.MethodHeadObject, native.MethodSearchObject, native.MethodRangeObject, native.MethodHashObject},
}

const (
	supportedS3ActionDeleteObject = "DeleteObject"
	supportedS3ActionGetObject    = "GetObject"
	supportedS3ActionHeadObject   = "HeadObject"
	supportedS3ActionPutObject    = "PutObject"
	supportedS3ActionListBucket   = "ListBucket"
)

type NativeResolver interface {
	GetUserKey(account, name string) (string, error)
	GetBucketCID(bucket string) (string, error)
}

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)

		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)
		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 {
					ruleConditions := append([]chain.Condition{principalCondFn(principal)}, 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 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
}

func formNativeResourceNamesAndConditions(names []string, resolver NativeResolver) ([]GroupedResources, error) {
	res := make([]GroupedResources, 0, len(names))

	var combined []string

	for i := range names {
		bkt, obj, err := parseResourceAsS3ARN(names[i])
		if err != nil {
			return nil, err
		}

		if bkt == Wildcard {
			res = res[:0]
			return append(res, GroupedResources{Names: []string{native.ResourceFormatAllObjects}}), nil
		}

		cnrID, err := resolver.GetBucketCID(bkt)
		if err != nil {
			return nil, err
		}
		resource := fmt.Sprintf(native.ResourceFormatRootContainerObjects, cnrID)

		if obj == Wildcard || obj == "" {
			combined = append(combined, resource)
			continue
		}

		res = append(res, GroupedResources{
			Names: []string{resource},
			Conditions: []chain.Condition{
				{
					Op:     chain.CondStringLike,
					Object: chain.ObjectResource,
					Key:    PropertyKeyFilePath,
					Value:  obj,
				},
			},
		})
	}

	if len(combined) != 0 {
		res = append(res, GroupedResources{Names: combined})
	}

	return res, nil
}

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) {
	res := make([]string, 0, len(names))

	for i := range names {
		action, err := parseActionAsS3Action(names[i])
		if err != nil {
			return nil, err
		}
		if action == Wildcard {
			return []string{Wildcard}, nil
		}
		res = append(res, actionToOpMap[action]...)
	}

	return res, nil
}