package policy

import (
	"fmt"

	policycontract "git.frostfs.info/TrueCloudLab/frostfs-contract/policy"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"go.uber.org/zap"
)

type MorphRuleChainStorage struct {
	contract Contract
	cache    *cache.MorphPolicyCache
	log      *zap.Logger
}

type MorphRuleChainStorageConfig struct {
	Contract Contract
	Cache    *cache.MorphPolicyCache
	Log      *zap.Logger
}

var _ engine.MorphRuleChainStorageReader = (*MorphRuleChainStorage)(nil)

const bucketPolicyPrefix = 'b'

func NewMorphRuleChainStorage(config *MorphRuleChainStorageConfig) *MorphRuleChainStorage {
	return &MorphRuleChainStorage{
		contract: config.Contract,
		cache:    config.Cache,
		log:      config.Log,
	}
}

func (c *MorphRuleChainStorage) GetAdmin() (util.Uint160, error) {
	panic("should never be called")
}

func (c *MorphRuleChainStorage) ListTargetsIterator(engine.TargetType) (uuid.UUID, result.Iterator, error) {
	panic("should never be called")
}

func (c *MorphRuleChainStorage) ListMorphRuleChains(name chain.Name, target engine.Target) ([]*chain.Chain, error) {
	key := cache.MorphPolicyCacheKey{Target: target, Name: name}
	list := c.cache.Get(key)
	if list != nil {
		return list, nil
	}

	listChains, err := c.contract.ListChains(getKind(target), target.Name, []byte(name))
	if err != nil {
		return nil, err
	}

	list = make([]*chain.Chain, len(listChains))
	for i, listChain := range listChains {
		var item chain.Chain
		if err = item.DecodeBytes(listChain); err != nil {
			return nil, fmt.Errorf("unmarshal chain: %w", err)
		}
		list[i] = &item
	}

	if err = c.cache.Put(key, list); err != nil {
		c.log.Warn(logs.CouldntCacheListPolicyChains)
	}

	return list, nil
}

func (c *MorphRuleChainStorage) PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error {
	c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cnrID.EncodeToString()), Name: chain.S3})

	tx := c.contract.StartTx()
	tx.AddChain(policycontract.IAM, ns, getBucketPolicyName(cnrID), policy)

	for i := range chains {
		tx.AddChain(policycontract.Container, cnrID.EncodeToString(), chains[i].ID, chains[i].Bytes())
	}

	return c.contract.SendTx(tx)
}

func (c *MorphRuleChainStorage) DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error {
	c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cnrID.EncodeToString()), Name: chain.S3})

	tx := c.contract.StartTx()
	for _, chainID := range chainIDs {
		tx.RemoveChain(policycontract.Container, cnrID.EncodeToString(), chainID)
	}
	tx.RemoveChain(policycontract.IAM, ns, getBucketPolicyName(cnrID))

	return c.contract.SendTx(tx)
}

func (c *MorphRuleChainStorage) GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error) {
	return c.contract.GetChain(policycontract.IAM, ns, getBucketPolicyName(cnrID))
}

func (c *MorphRuleChainStorage) SaveACLChains(cid string, chains []*chain.Chain) error {
	c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cid), Name: chain.S3})
	c.cache.Delete(cache.MorphPolicyCacheKey{Target: engine.ContainerTarget(cid), Name: chain.Ingress})

	tx := c.contract.StartTx()
	for i := range chains {
		tx.AddChain(policycontract.Container, cid, chains[i].ID, chains[i].Bytes())
	}

	return c.contract.SendTx(tx)
}

func getKind(target engine.Target) policycontract.Kind {
	switch target.Type {
	case engine.Container:
		return policycontract.Container
	case engine.User:
		return 'u'
	case engine.Group:
		return 'g'
	default:
		return policycontract.Namespace
	}
}

func getBucketPolicyName(cnrID cid.ID) []byte {
	return append([]byte{bucketPolicyPrefix}, cnrID[:]...)
}