package subnet

import (
	"github.com/nspcc-dev/neo-go/pkg/interop"
	"github.com/nspcc-dev/neo-go/pkg/interop/contract"
	"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"
	"github.com/nspcc-dev/neofs-contract/common"
)

const (
	// ErrInvalidSubnetID is thrown when subnet id is not a slice of 5 bytes.
	ErrInvalidSubnetID = "invalid subnet ID"
	// ErrInvalidGroupID is thrown when group id is not a slice of 5 bytes.
	ErrInvalidGroupID = "invalid group ID"
	// ErrInvalidOwner is thrown when owner has invalid format.
	ErrInvalidOwner = "invalid owner"
	// ErrInvalidAdmin is thrown when admin has invalid format.
	ErrInvalidAdmin = "invalid administrator"
	// ErrAlreadyExists is thrown when id already exists.
	ErrAlreadyExists = "subnet id already exists"
	// ErrNotExist is thrown when id doesn't exist.
	ErrNotExist = "subnet id doesn't exist"
	// ErrInvalidUser is thrown when user has invalid format.
	ErrInvalidUser = "invalid user"
	// ErrInvalidNode is thrown when node has invalid format.
	ErrInvalidNode = "invalid node key"
	// ErrNodeAdmNotExist is thrown when node admin is not found.
	ErrNodeAdmNotExist = "node admin not found"
	// ErrClientAdmNotExist is thrown when client admin is not found.
	ErrClientAdmNotExist = "client admin not found"
	// ErrNodeNotExist is thrown when node is not found.
	ErrNodeNotExist = "node not found"
	// ErrUserNotExist is thrown when user is not found.
	ErrUserNotExist = "user not found"
	// ErrAccessDenied is thrown when operation is denied for caller.
	ErrAccessDenied = "access denied"
)

const (
	nodeAdminPrefix   = 'a'
	infoPrefix        = 'i'
	clientAdminPrefix = 'm'
	nodePrefix        = 'n'
	ownerPrefix       = 'o'
	userPrefix        = 'u'
	notaryDisabledKey = 'z'
)

const (
	userIDSize   = 27
	subnetIDSize = 5
	groupIDSize  = 5
)

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

	args := data.(struct {
		notaryDisabled bool
	})

	ctx := storage.GetContext()
	storage.Put(ctx, []byte{notaryDisabledKey}, args.notaryDisabled)
}

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

	contract.Call(interop.Hash160(management.Hash), "update", contract.All,
		script, manifest, common.AppendVersion(data))
	runtime.Log("subnet contract updated")
}

// Put creates new subnet with the specified owner and info.
func Put(id []byte, ownerKey interop.PublicKey, info []byte) {
	// V2 format
	if len(id) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}
	if len(ownerKey) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidOwner)
	}

	ctx := storage.GetContext()
	stKey := append([]byte{ownerPrefix}, id...)
	if storage.Get(ctx, stKey) != nil {
		panic(ErrAlreadyExists)
	}

	notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
	if notaryDisabled {
		alphabet := common.AlphabetNodes()
		nodeKey := common.InnerRingInvoker(alphabet)
		if len(nodeKey) == 0 {
			common.CheckWitness(ownerKey)
			runtime.Notify("Put", id, ownerKey, info)
			return
		}

		threshold := len(alphabet)*2/3 + 1
		id := common.InvokeID([]interface{}{ownerKey, info}, []byte("put"))
		n := common.Vote(ctx, id, nodeKey)
		if n < threshold {
			return
		}

		common.RemoveVotes(ctx, id)
	} else {
		common.CheckOwnerWitness(ownerKey)

		multiaddr := common.AlphabetAddress()
		common.CheckAlphabetWitness(multiaddr)
	}

	storage.Put(ctx, stKey, ownerKey)
	stKey[0] = infoPrefix
	storage.Put(ctx, stKey, info)
}

// Get returns info about subnet with the specified id.
func Get(id []byte) []byte {
	// V2 format
	if len(id) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	ctx := storage.GetReadOnlyContext()
	key := append([]byte{infoPrefix}, id...)
	raw := storage.Get(ctx, key)
	if raw == nil {
		panic(ErrNotExist)
	}
	return raw.([]byte)
}

// Delete deletes subnet with the specified id.
func Delete(id []byte) {
	// V2 format
	if len(id) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	ctx := storage.GetContext()
	key := append([]byte{ownerPrefix}, id...)
	raw := storage.Get(ctx, key)
	if raw == nil {
		return
	}

	owner := raw.([]byte)
	common.CheckOwnerWitness(owner)

	storage.Delete(ctx, key)

	key[0] = infoPrefix
	storage.Delete(ctx, key)

	key[0] = nodeAdminPrefix
	deleteByPrefix(ctx, key)

	key[0] = nodePrefix
	deleteByPrefix(ctx, key)

	key[0] = clientAdminPrefix
	deleteByPrefix(ctx, key)

	key[0] = userPrefix
	deleteByPrefix(ctx, key)

	runtime.Notify("Delete", id)
}

// AddNodeAdmin adds new node administrator to the specified subnetwork.
func AddNodeAdmin(subnetID []byte, adminKey interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	if len(adminKey) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidAdmin)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	owner := rawOwner.([]byte)
	common.CheckOwnerWitness(owner)

	stKey[0] = nodeAdminPrefix

	if keyInList(ctx, adminKey, stKey) {
		return
	}

	putKeyInList(ctx, adminKey, stKey)
}

// RemoveNodeAdmin removes node administrator from the specified subnetwork.
// Must be called by subnet owner only.
func RemoveNodeAdmin(subnetID []byte, adminKey interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	if len(adminKey) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidAdmin)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	owner := rawOwner.([]byte)
	common.CheckOwnerWitness(owner)

	stKey[0] = nodeAdminPrefix

	if !keyInList(ctx, adminKey, stKey) {
		return
	}

	deleteKeyFromList(ctx, adminKey, stKey)
}

// AddNode adds node to the specified subnetwork.
// Must be called by subnet's owner or node administrator
// only.
func AddNode(subnetID []byte, node interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	if len(node) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidNode)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	stKey[0] = nodeAdminPrefix

	owner := rawOwner.([]byte)

	if !calledByOwnerOrAdmin(ctx, owner, stKey) {
		panic(ErrAccessDenied)
	}

	stKey[0] = nodePrefix

	if keyInList(ctx, node, stKey) {
		return
	}

	putKeyInList(ctx, node, stKey)
}

// RemoveNode removes node from the specified subnetwork.
// Must be called by subnet's owner or node administrator
// only.
func RemoveNode(subnetID []byte, node interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	if len(node) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidNode)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	stKey[0] = nodeAdminPrefix

	owner := rawOwner.([]byte)

	if !calledByOwnerOrAdmin(ctx, owner, stKey) {
		panic(ErrAccessDenied)
	}

	stKey[0] = nodePrefix

	if !keyInList(ctx, node, stKey) {
		return
	}

	storage.Delete(ctx, append(stKey, node...))

	runtime.Notify("RemoveNode", subnetID, node)
}

// NodeAllowed checks if node is included in the
// specified subnet or not.
func NodeAllowed(subnetID []byte, node interop.PublicKey) bool {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	if len(node) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidNode)
	}

	ctx := storage.GetReadOnlyContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	stKey[0] = nodePrefix

	return storage.Get(ctx, append(stKey, node...)) != nil
}

// AddClientAdmin adds new client administrator of the specified group in the specified subnetwork.
// Must be called by owner only.
func AddClientAdmin(subnetID []byte, groupID []byte, adminPublicKey interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	// V2 format
	if len(groupID) != groupIDSize {
		panic(ErrInvalidGroupID)
	}

	if len(adminPublicKey) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidAdmin)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	owner := rawOwner.([]byte)
	common.CheckOwnerWitness(owner)

	stKey[0] = clientAdminPrefix
	stKey = append(stKey, groupID...)

	if keyInList(ctx, adminPublicKey, stKey) {
		return
	}

	putKeyInList(ctx, adminPublicKey, stKey)
}

// RemoveClientAdmin removes client administrator from the
// specified group in the specified subnetwork.
// Must be called by owner only.
func RemoveClientAdmin(subnetID []byte, groupID []byte, adminPublicKey interop.PublicKey) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	// V2 format
	if len(groupID) != groupIDSize {
		panic(ErrInvalidGroupID)
	}

	if len(adminPublicKey) != interop.PublicKeyCompressedLen {
		panic(ErrInvalidAdmin)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	owner := rawOwner.([]byte)
	common.CheckOwnerWitness(owner)

	stKey[0] = clientAdminPrefix
	stKey = append(stKey, groupID...)

	if !keyInList(ctx, adminPublicKey, stKey) {
		return
	}

	deleteKeyFromList(ctx, adminPublicKey, stKey)
}

// AddUser adds user to the specified subnetwork and group.
// Must be called by the owner or the group's admin only.
func AddUser(subnetID []byte, groupID []byte, userID []byte) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	// V2 format
	if len(userID) != userIDSize {
		panic(ErrInvalidUser)
	}

	// V2 format
	if len(groupID) != groupIDSize {
		panic(ErrInvalidGroupID)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	stKey[0] = clientAdminPrefix
	stKey = append(stKey, groupID...)

	owner := rawOwner.([]byte)

	if !calledByOwnerOrAdmin(ctx, owner, stKey) {
		panic(ErrAccessDenied)
	}

	stKey[0] = userPrefix

	if keyInList(ctx, userID, stKey) {
		return
	}

	putKeyInList(ctx, userID, stKey)
}

// RemoveUser removes user from the specified subnetwork and group.
// Must be called by the owner or the group's admin only.
func RemoveUser(subnetID []byte, groupID []byte, userID []byte) {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	// V2 format
	if len(groupID) != groupIDSize {
		panic(ErrInvalidGroupID)
	}

	// V2 format
	if len(userID) != userIDSize {
		panic(ErrInvalidUser)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)

	rawOwner := storage.Get(ctx, stKey)
	if rawOwner == nil {
		panic(ErrNotExist)
	}

	stKey[0] = clientAdminPrefix
	stKey = append(stKey, groupID...)

	owner := rawOwner.([]byte)

	if !calledByOwnerOrAdmin(ctx, owner, stKey) {
		panic(ErrAccessDenied)
	}

	stKey[0] = userPrefix

	if !keyInList(ctx, userID, stKey) {
		return
	}

	deleteKeyFromList(ctx, userID, stKey)
}

// UserAllowed returns bool that indicates if node is included in the
// specified subnet or not.
func UserAllowed(subnetID []byte, user []byte) bool {
	// V2 format
	if len(subnetID) != subnetIDSize {
		panic(ErrInvalidSubnetID)
	}

	ctx := storage.GetContext()

	stKey := append([]byte{ownerPrefix}, subnetID...)
	if storage.Get(ctx, stKey) == nil {
		panic(ErrNotExist)
	}

	stKey[0] = userPrefix
	prefixLen := len(stKey) + groupIDSize

	iter := storage.Find(ctx, stKey, storage.KeysOnly)
	for iterator.Next(iter) {
		key := iterator.Value(iter).([]byte)
		if common.BytesEqual(user, key[prefixLen:]) {
			return true
		}
	}

	return false
}

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

func keyInList(ctx storage.Context, searchedKey interop.PublicKey, prefix []byte) bool {
	return storage.Get(ctx, append(prefix, searchedKey...)) != nil
}

func putKeyInList(ctx storage.Context, keyToPut interop.PublicKey, prefix []byte) {
	storage.Put(ctx, append(prefix, keyToPut...), []byte{1})
}

func deleteKeyFromList(ctx storage.Context, keyToDelete interop.PublicKey, prefix []byte) {
	storage.Delete(ctx, append(prefix, keyToDelete...))
}

func deleteByPrefix(ctx storage.Context, prefix []byte) {
	iter := storage.Find(ctx, prefix, storage.KeysOnly)
	for iterator.Next(iter) {
		k := iterator.Value(iter).([]byte)
		storage.Delete(ctx, k)
	}
}

func calledByOwnerOrAdmin(ctx storage.Context, owner []byte, adminPrefix []byte) bool {
	if runtime.CheckWitness(owner) {
		return true
	}

	iter := storage.Find(ctx, adminPrefix, storage.KeysOnly|storage.RemovePrefix)
	for iterator.Next(iter) {
		key := iterator.Value(iter).([]byte)
		if runtime.CheckWitness(key) {
			return true
		}
	}

	return false
}