package ape

import (
	"encoding/hex"
	"fmt"

	v2acl "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/acl"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
)

type ConvertEACLError struct {
	nested error
}

func (e *ConvertEACLError) Error() string {
	if e == nil {
		return ""
	}
	return "failed to convert eACL table to policy engine chain: " + e.nested.Error()
}

func (e *ConvertEACLError) Unwrap() error {
	if e == nil {
		return nil
	}
	return e.nested
}

// ConvertEACLToAPE converts eacl.Table to apechain.Chain.
func ConvertEACLToAPE(eaclTable *eacl.Table) (*apechain.Chain, error) {
	if eaclTable == nil {
		return nil, nil
	}
	res := &apechain.Chain{
		MatchType: apechain.MatchTypeFirstMatch,
	}

	resource := getResource(eaclTable)

	for _, eaclRecord := range eaclTable.Records() {
		if len(eaclRecord.Targets()) == 0 {
			// see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101
			// and https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L36
			// such record doesn't have any effect
			continue
		}

		st, err := actionToStatus(eaclRecord.Action())
		if err != nil {
			return nil, err
		}
		act, err := operationToAction(eaclRecord.Operation())
		if err != nil {
			return nil, err
		}

		if len(eaclRecord.Filters()) == 0 {
			res.Rules = appendTargetsOnly(res.Rules, st, act, resource, eaclRecord.Targets())
		} else {
			res.Rules, err = appendTargetsAndFilters(res.Rules, st, act, resource, eaclRecord.Targets(), eaclRecord.Filters())
			if err != nil {
				return nil, err
			}
		}
	}

	return res, nil
}

func apeRoleConds(role eacl.Role) (res []apechain.Condition) {
	switch role {
	case eacl.RoleSystem:
		res = append(res,
			apechain.Condition{
				Op:    apechain.CondStringEquals,
				Kind:  apechain.KindRequest,
				Key:   nativeschema.PropertyKeyActorRole,
				Value: nativeschema.PropertyValueContainerRoleContainer,
			},
		)
		res = append(res,
			apechain.Condition{
				Op:    apechain.CondStringEquals,
				Kind:  apechain.KindRequest,
				Key:   nativeschema.PropertyKeyActorRole,
				Value: nativeschema.PropertyValueContainerRoleIR,
			},
		)
	case eacl.RoleOthers:
		res = append(res,
			apechain.Condition{
				Op:    apechain.CondStringEquals,
				Kind:  apechain.KindRequest,
				Key:   nativeschema.PropertyKeyActorRole,
				Value: nativeschema.PropertyValueContainerRoleOthers,
			},
		)
	case eacl.RoleUser:
		res = append(res,
			apechain.Condition{
				Op:    apechain.CondStringEquals,
				Kind:  apechain.KindRequest,
				Key:   nativeschema.PropertyKeyActorRole,
				Value: nativeschema.PropertyValueContainerRoleOwner,
			},
		)
	case eacl.RoleUnknown:
		// such condition has no effect
	default:
	}
	return
}

func appendTargetsOnly(source []apechain.Rule, st apechain.Status, act apechain.Actions, res apechain.Resources, targets []eacl.Target) []apechain.Rule {
	// see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101
	// role OR public key must be equal
	rule := apechain.Rule{
		Status:    st,
		Actions:   act,
		Resources: res,
		Any:       true,
	}
	for _, target := range targets {
		rule.Condition = append(rule.Condition, apeRoleConds(target.Role())...)
		for _, binKey := range target.BinaryKeys() {
			var pubKeyCondition apechain.Condition
			pubKeyCondition.Kind = apechain.KindRequest
			pubKeyCondition.Key = nativeschema.PropertyKeyActorPublicKey
			pubKeyCondition.Value = hex.EncodeToString(binKey)
			pubKeyCondition.Op = apechain.CondStringEquals
			rule.Condition = append(rule.Condition, pubKeyCondition)
		}
	}
	return append(source, rule)
}

func appendTargetsAndFilters(source []apechain.Rule, st apechain.Status, act apechain.Actions, res apechain.Resources,
	targets []eacl.Target, filters []eacl.Filter,
) ([]apechain.Rule, error) {
	// see https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/ab75edd70939564421936d207ef80d6c1398b51b/eacl/validator.go#L101
	// role OR public key must be equal
	// so filters are repeated for each role and public key
	var err error
	for _, target := range targets {
		rule := apechain.Rule{
			Status:    st,
			Actions:   act,
			Resources: res,
		}
		rule.Condition = append(rule.Condition, apeRoleConds(target.Role())...)
		rule.Condition, err = appendFilters(rule.Condition, filters)
		if err != nil {
			return nil, err
		}

		source = append(source, rule)

		for _, binKey := range target.BinaryKeys() {
			rule := apechain.Rule{
				Status:    st,
				Actions:   act,
				Resources: res,
			}
			var pubKeyCondition apechain.Condition
			pubKeyCondition.Kind = apechain.KindRequest
			pubKeyCondition.Key = nativeschema.PropertyKeyActorPublicKey
			pubKeyCondition.Value = hex.EncodeToString(binKey)
			pubKeyCondition.Op = apechain.CondStringEquals

			rule.Condition = append(rule.Condition, pubKeyCondition)
			rule.Condition, err = appendFilters(rule.Condition, filters)
			if err != nil {
				return nil, err
			}

			source = append(source, rule)
		}
	}

	return source, nil
}

func appendFilters(source []apechain.Condition, filters []eacl.Filter) ([]apechain.Condition, error) {
	for _, filter := range filters {
		var cond apechain.Condition
		var isObject bool
		if filter.From() == eacl.HeaderFromObject {
			cond.Kind = apechain.KindResource
			isObject = true
		} else if filter.From() == eacl.HeaderFromRequest {
			cond.Kind = apechain.KindRequest
		} else {
			return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter from: %d", filter.From())}
		}

		if filter.Matcher() == eacl.MatchStringEqual {
			cond.Op = apechain.CondStringEquals
		} else if filter.Matcher() == eacl.MatchStringNotEqual {
			cond.Op = apechain.CondStringNotEquals
		} else {
			return nil, &ConvertEACLError{nested: fmt.Errorf("unknown filter matcher: %d", filter.Matcher())}
		}

		cond.Key = eaclKeyToAPEKey(filter.Key(), isObject)
		cond.Value = filter.Value()

		source = append(source, cond)
	}
	return source, nil
}

func eaclKeyToAPEKey(key string, isObject bool) string {
	if !isObject {
		return key
	}
	switch key {
	default:
		return key
	case v2acl.FilterObjectVersion:
		return nativeschema.PropertyKeyObjectVersion
	case v2acl.FilterObjectID:
		return nativeschema.PropertyKeyObjectID
	case v2acl.FilterObjectContainerID:
		return nativeschema.PropertyKeyObjectContainerID
	case v2acl.FilterObjectOwnerID:
		return nativeschema.PropertyKeyObjectOwnerID
	case v2acl.FilterObjectCreationEpoch:
		return nativeschema.PropertyKeyObjectCreationEpoch
	case v2acl.FilterObjectPayloadLength:
		return nativeschema.PropertyKeyObjectPayloadLength
	case v2acl.FilterObjectPayloadHash:
		return nativeschema.PropertyKeyObjectPayloadHash
	case v2acl.FilterObjectType:
		return nativeschema.PropertyKeyObjectType
	case v2acl.FilterObjectHomomorphicHash:
		return nativeschema.PropertyKeyObjectHomomorphicHash
	}
}

func getResource(eaclTable *eacl.Table) apechain.Resources {
	cnrID, isSet := eaclTable.CID()
	if isSet {
		return apechain.Resources{
			Names: []string{fmt.Sprintf(nativeschema.ResourceFormatRootContainerObjects, cnrID.EncodeToString())},
		}
	}
	return apechain.Resources{
		Names: []string{nativeschema.ResourceFormatRootObjects},
	}
}

func actionToStatus(a eacl.Action) (apechain.Status, error) {
	switch a {
	case eacl.ActionAllow:
		return apechain.Allow, nil
	case eacl.ActionDeny:
		return apechain.AccessDenied, nil
	default:
		return apechain.NoRuleFound, &ConvertEACLError{nested: fmt.Errorf("unknown action: %d", a)}
	}
}

var eaclOperationToEngineAction = map[eacl.Operation]apechain.Actions{
	eacl.OperationGet:       {Names: []string{nativeschema.MethodGetObject}},
	eacl.OperationHead:      {Names: []string{nativeschema.MethodHeadObject}},
	eacl.OperationPut:       {Names: []string{nativeschema.MethodPutObject}},
	eacl.OperationDelete:    {Names: []string{nativeschema.MethodDeleteObject}},
	eacl.OperationSearch:    {Names: []string{nativeschema.MethodSearchObject}},
	eacl.OperationRange:     {Names: []string{nativeschema.MethodRangeObject}},
	eacl.OperationRangeHash: {Names: []string{nativeschema.MethodHashObject}},
}

func operationToAction(op eacl.Operation) (apechain.Actions, error) {
	if v, ok := eaclOperationToEngineAction[op]; ok {
		return v, nil
	}
	return apechain.Actions{}, &ConvertEACLError{nested: fmt.Errorf("unknown operation: %d", op)}
}