package policy

import (
	"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
	"github.com/nspcc-dev/neo-go/pkg/interop"
	"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
	"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
	"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
	"github.com/nspcc-dev/neo-go/pkg/interop/storage"
)

// Kind represents the object the chain is attached to.
// Currently only namespace and container are supported.
type Kind byte

const (
	Namespace = 'n'
	Container = 'c'
	IAM       = 'i'
)

const (
	ownerKeyPrefix = 'o'
)

const (
	mappingKeyPrefix = 'm'
	counterKey       = "Counter"
)

const (
	// ErrNotAuthorized is returned when the none of the transaction signers
	// belongs to the list of autorized keys.
	ErrNotAuthorized = "none of the signers is authorized to change the contract"
)

// _deploy function sets up initial list of inner ring public keys.
func _deploy(data any, isUpdate bool) {
	if isUpdate {
		args := data.([]any)
		common.CheckVersion(args[len(args)-1].(int))
		return
	}

	args := data.(struct {
		Admin interop.Hash160
	})
	ctx := storage.GetContext()
	if args.Admin != nil {
		if len(args.Admin) != 20 {
			panic("invaliad admin hash length")
		}
		storage.Put(ctx, []byte{ownerKeyPrefix}, args.Admin)
	}
	storage.Put(ctx, counterKey, 0)
}

func checkAuthorization(ctx storage.Context) {
	admin := getAdmin(ctx)
	if admin != nil && runtime.CheckWitness(admin) {
		return
	}
	if runtime.CheckWitness(common.AlphabetAddress()) {
		return
	}

	panic(ErrNotAuthorized)
}

// Version returns the version of the contract.
func Version() int {
	return common.Version
}

// Update method updates contract source code and manifest. It can be invoked
// by committee only.
func Update(script []byte, manifest []byte, data any) {
	if !common.HasUpdateAccess() {
		panic("only committee can update contract")
	}

	management.UpdateWithData(script, manifest, common.AppendVersion(data))
	runtime.Log("policy contract updated")
}

func SetAdmin(addr interop.Hash160) {
	common.CheckAlphabetWitness()

	ctx := storage.GetContext()
	storage.Put(ctx, []byte{ownerKeyPrefix}, addr)
}

func GetAdmin() interop.Hash160 {
	ctx := storage.GetReadOnlyContext()
	return getAdmin(ctx)
}

func getAdmin(ctx storage.Context) interop.Hash160 {
	return storage.Get(ctx, []byte{ownerKeyPrefix}).(interop.Hash160)
}

func storageKey(prefix Kind, counter int, name []byte) []byte {
	key := append([]byte{byte(prefix)}, common.ToFixedWidth64(counter)...)
	return append(key, name...)
}

func mapKey(kind Kind, name []byte) []byte {
	return append([]byte{mappingKeyPrefix, byte(kind)}, name...)
}

// mapToNumeric maps a name to a number. That allows to keep more space in
// a storage key shortening long names. Short entity
// names are also mapped to prevent collisions in the map.
func mapToNumeric(ctx storage.Context, kind Kind, name []byte) (mapped int, mappingExists bool) {
	mKey := mapKey(kind, name)
	numericID := storage.Get(ctx, mKey)
	if numericID == nil {
		return 0, false
	}
	mapped = numericID.(int)
	mappingExists = true
	return
}

// mapToNumericCreateIfNotExists maps a name to a number. That allows to keep
// more space in a storage key shortening long names. Short entity
// names are also mapped to prevent collisions in the map.
// If a mapping cannot be found, then the method creates and returns it.
// mapToNumericCreateIfNotExists is NOT applicable for a read-only context.
func mapToNumericCreateIfNotExists(ctx storage.Context, kind Kind, name []byte) int {
	mKey := mapKey(kind, name)
	numericID := storage.Get(ctx, mKey)
	if numericID == nil {
		counter := storage.Get(ctx, counterKey).(int)
		counter++
		storage.Put(ctx, counterKey, counter)
		storage.Put(ctx, mKey, counter)
		return counter
	}
	return numericID.(int)
}

func AddChain(entity Kind, entityName string, name []byte, chain []byte) {
	ctx := storage.GetContext()
	checkAuthorization(ctx)

	entityNameBytes := mapToNumericCreateIfNotExists(ctx, entity, []byte(entityName))
	key := storageKey(entity, entityNameBytes, name)
	storage.Put(ctx, key, chain)
}

func GetChain(entity Kind, entityName string, name []byte) []byte {
	ctx := storage.GetReadOnlyContext()

	entityNameBytes, exists := mapToNumeric(ctx, entity, []byte(entityName))
	if !exists {
		panic("not found")
	}

	key := storageKey(entity, entityNameBytes, name)
	data := storage.Get(ctx, key).([]byte)
	if data == nil {
		panic("not found")
	}

	return data
}

func RemoveChain(entity Kind, entityName string, name []byte) {
	ctx := storage.GetContext()
	checkAuthorization(ctx)

	entityNameNum, exists := mapToNumeric(ctx, entity, []byte(entityName))
	if !exists {
		return
	}

	key := storageKey(entity, entityNameNum, name)
	storage.Delete(ctx, key)

	// If no chains are left for the target, then remove the mapping.
	prefix := append([]byte{byte(entity)}, common.ToFixedWidth64(entityNameNum)...)
	it := storage.Find(ctx, prefix, storage.KeysOnly)
	if !iterator.Next(it) {
		storage.Delete(ctx, mapKey(entity, []byte(entityName)))
	}
}

func RemoveChainsByPrefix(entity Kind, entityName string, name []byte) {
	ctx := storage.GetContext()
	checkAuthorization(ctx)

	entityNameNum, exists := mapToNumeric(ctx, entity, []byte(entityName))
	if !exists {
		return
	}

	key := storageKey(entity, entityNameNum, name)
	it := storage.Find(ctx, key, storage.KeysOnly)
	for iterator.Next(it) {
		storage.Delete(ctx, iterator.Value(it).([]byte))
	}

	// If no chains are left for the target, then remove the mapping.
	prefix := append([]byte{byte(entity)}, common.ToFixedWidth64(entityNameNum)...)
	it = storage.Find(ctx, prefix, storage.KeysOnly)
	if !iterator.Next(it) {
		storage.Delete(ctx, mapKey(entity, []byte(entityName)))
	}
}

// ListChains lists all chains for the namespace by prefix.
// container may be empty.
func ListChains(namespace, container string, name []byte) [][]byte {
	result := ListChainsByPrefix(Namespace, namespace, name)

	if container != "" {
		result = append(result, ListChainsByPrefix(Container, container, name)...)
	}

	return result
}

// ListChainsByPrefix list all chains for the provided kind and entity by prefix.
func ListChainsByPrefix(entity Kind, entityName string, prefix []byte) [][]byte {
	ctx := storage.GetReadOnlyContext()

	result := [][]byte{}

	entityNameBytes, exists := mapToNumeric(ctx, entity, []byte(entityName))
	if !exists {
		return result
	}

	keyPrefix := storageKey(entity, entityNameBytes, prefix)
	it := storage.Find(ctx, keyPrefix, storage.ValuesOnly)
	for iterator.Next(it) {
		result = append(result, iterator.Value(it).([]byte))
	}

	return result
}

func IteratorChainsByPrefix(entity Kind, entityName string, prefix []byte) iterator.Iterator {
	ctx := storage.GetReadOnlyContext()
	id, _ := mapToNumeric(ctx, entity, []byte(entityName))
	keyPrefix := storageKey(entity, id, prefix)
	return storage.Find(ctx, keyPrefix, storage.ValuesOnly)
}

// ListTargets iterates over targets for which rules are defined.
func ListTargets(entity Kind) iterator.Iterator {
	ctx := storage.GetReadOnlyContext()
	mKey := mapKey(entity, []byte{})
	return storage.Find(ctx, mKey, storage.KeysOnly|storage.RemovePrefix)
}