package frostfsid

import (
	"errors"
	"fmt"
	"math/big"
	"sort"

	frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	frostfsidrpclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/frostfsid"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const iteratorBatchSize = 1

const (
	namespaceFlag      = "namespace"
	subjectNameFlag    = "subject-name"
	subjectKeyFlag     = "subject-key"
	subjectAddressFlag = "subject-address"
	includeNamesFlag   = "include-names"
	groupNameFlag      = "group-name"
	groupIDFlag        = "group-id"

	rootNamespacePlaceholder = "<root>"

	keyFlag       = "key"
	keyDescFlag   = "Key for storing a value in the subject's KV storage"
	valueFlag     = "value"
	valueDescFlag = "Value to be stored in the subject's KV storage"
)

var (
	Cmd = &cobra.Command{
		Use:   "frostfsid",
		Short: "Section for frostfsid interactions commands",
	}

	frostfsidCreateNamespaceCmd = &cobra.Command{
		Use:   "create-namespace",
		Short: "Create new namespace in frostfsid contract",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidCreateNamespace,
	}

	frostfsidListNamespacesCmd = &cobra.Command{
		Use:   "list-namespaces",
		Short: "List all namespaces in frostfsid",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidListNamespaces,
	}

	frostfsidCreateSubjectCmd = &cobra.Command{
		Use:   "create-subject",
		Short: "Create subject in frostfsid contract",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidCreateSubject,
	}

	frostfsidDeleteSubjectCmd = &cobra.Command{
		Use:   "delete-subject",
		Short: "Delete subject from frostfsid contract",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidDeleteSubject,
	}

	frostfsidListSubjectsCmd = &cobra.Command{
		Use:   "list-subjects",
		Short: "List subjects in namespace",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidListSubjects,
	}

	frostfsidCreateGroupCmd = &cobra.Command{
		Use:   "create-group",
		Short: "Create group in frostfsid contract",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidCreateGroup,
	}

	frostfsidDeleteGroupCmd = &cobra.Command{
		Use:   "delete-group",
		Short: "Delete group from frostfsid contract",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidDeleteGroup,
	}

	frostfsidListGroupsCmd = &cobra.Command{
		Use:   "list-groups",
		Short: "List groups in namespace",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidListGroups,
	}

	frostfsidAddSubjectToGroupCmd = &cobra.Command{
		Use:   "add-subject-to-group",
		Short: "Add subject to group",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidAddSubjectToGroup,
	}

	frostfsidRemoveSubjectFromGroupCmd = &cobra.Command{
		Use:   "remove-subject-from-group",
		Short: "Remove subject from group",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidRemoveSubjectFromGroup,
	}

	frostfsidListGroupSubjectsCmd = &cobra.Command{
		Use:   "list-group-subjects",
		Short: "List subjects in group",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidListGroupSubjects,
	}

	frostfsidSetKVCmd = &cobra.Command{
		Use:   "set-kv",
		Short: "Store a key-value pair in the subject's KV storage",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidSetKV,
	}
	frostfsidDeleteKVCmd = &cobra.Command{
		Use:   "delete-kv",
		Short: "Delete a value from the subject's KV storage",
		PreRun: func(cmd *cobra.Command, _ []string) {
			_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
		},
		Run: frostfsidDeleteKV,
	}
)

func initFrostfsIDCreateNamespaceCmd() {
	Cmd.AddCommand(frostfsidCreateNamespaceCmd)
	frostfsidCreateNamespaceCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidCreateNamespaceCmd.Flags().String(namespaceFlag, "", "Namespace name to create")
	frostfsidCreateNamespaceCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
	_ = frostfsidCreateNamespaceCmd.MarkFlagRequired(namespaceFlag)
}

func initFrostfsIDListNamespacesCmd() {
	Cmd.AddCommand(frostfsidListNamespacesCmd)
	frostfsidListNamespacesCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
}

func initFrostfsIDCreateSubjectCmd() {
	Cmd.AddCommand(frostfsidCreateSubjectCmd)
	frostfsidCreateSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidCreateSubjectCmd.Flags().String(namespaceFlag, "", "Namespace where create subject")
	frostfsidCreateSubjectCmd.Flags().String(subjectNameFlag, "", "Subject name, must be unique in namespace")
	frostfsidCreateSubjectCmd.Flags().String(subjectKeyFlag, "", "Subject hex-encoded public key")
	frostfsidCreateSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
}

func initFrostfsIDDeleteSubjectCmd() {
	Cmd.AddCommand(frostfsidDeleteSubjectCmd)
	frostfsidDeleteSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidDeleteSubjectCmd.Flags().String(subjectAddressFlag, "", "Subject address")
	frostfsidDeleteSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
}

func initFrostfsIDListSubjectsCmd() {
	Cmd.AddCommand(frostfsidListSubjectsCmd)
	frostfsidListSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidListSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace to list subjects")
	frostfsidListSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
}

func initFrostfsIDCreateGroupCmd() {
	Cmd.AddCommand(frostfsidCreateGroupCmd)
	frostfsidCreateGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidCreateGroupCmd.Flags().String(namespaceFlag, "", "Namespace where create group")
	frostfsidCreateGroupCmd.Flags().String(groupNameFlag, "", "Group name, must be unique in namespace")
	frostfsidCreateGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
	_ = frostfsidCreateGroupCmd.MarkFlagRequired(groupNameFlag)
}

func initFrostfsIDDeleteGroupCmd() {
	Cmd.AddCommand(frostfsidDeleteGroupCmd)
	frostfsidDeleteGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidDeleteGroupCmd.Flags().String(namespaceFlag, "", "Namespace to delete group")
	frostfsidDeleteGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
	frostfsidDeleteGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
}

func initFrostfsIDListGroupsCmd() {
	Cmd.AddCommand(frostfsidListGroupsCmd)
	frostfsidListGroupsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidListGroupsCmd.Flags().String(namespaceFlag, "", "Namespace to list groups")
}

func initFrostfsIDAddSubjectToGroupCmd() {
	Cmd.AddCommand(frostfsidAddSubjectToGroupCmd)
	frostfsidAddSubjectToGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidAddSubjectToGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
	frostfsidAddSubjectToGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
	frostfsidAddSubjectToGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
}

func initFrostfsIDRemoveSubjectFromGroupCmd() {
	Cmd.AddCommand(frostfsidRemoveSubjectFromGroupCmd)
	frostfsidRemoveSubjectFromGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidRemoveSubjectFromGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
	frostfsidRemoveSubjectFromGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
	frostfsidRemoveSubjectFromGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
}

func initFrostfsIDListGroupSubjectsCmd() {
	Cmd.AddCommand(frostfsidListGroupSubjectsCmd)
	frostfsidListGroupSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidListGroupSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace name")
	frostfsidListGroupSubjectsCmd.Flags().Int64(groupIDFlag, 0, "Group id")
	frostfsidListGroupSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
}

func initFrostfsIDSetKVCmd() {
	Cmd.AddCommand(frostfsidSetKVCmd)
	frostfsidSetKVCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidSetKVCmd.Flags().String(subjectAddressFlag, "", "Subject address")
	frostfsidSetKVCmd.Flags().String(keyFlag, "", keyDescFlag)
	frostfsidSetKVCmd.Flags().String(valueFlag, "", valueDescFlag)
}

func initFrostfsIDDeleteKVCmd() {
	Cmd.AddCommand(frostfsidDeleteKVCmd)
	frostfsidDeleteKVCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
	frostfsidDeleteKVCmd.Flags().String(subjectAddressFlag, "", "Subject address")
	frostfsidDeleteKVCmd.Flags().String(keyFlag, "", keyDescFlag)
}

func frostfsidCreateNamespace(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.CreateNamespaceCall(ns))

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "create namespace error: %w", err)
}

func frostfsidListNamespaces(cmd *cobra.Command, _ []string) {
	inv, _, hash := initInvoker(cmd)
	reader := frostfsidrpclient.NewReader(inv, hash)
	sessionID, it, err := reader.ListNamespaces()
	commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)
	items, err := readIterator(inv, &it, sessionID)
	commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)

	namespaces, err := frostfsidclient.ParseNamespaces(items)
	commonCmd.ExitOnErr(cmd, "can't parse namespace: %w", err)
	sort.Slice(namespaces, func(i, j int) bool { return namespaces[i].Name < namespaces[j].Name })

	for _, namespace := range namespaces {
		if namespace.Name == "" {
			namespace.Name = rootNamespacePlaceholder
		}
		cmd.Printf("%s\n", namespace.Name)
	}
}

func frostfsidCreateSubject(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)
	subjName := getFrostfsIDSubjectName(cmd)
	subjKey := getFrostfsIDSubjectKey(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.CreateSubjectCall(ns, subjKey))
	if subjName != "" {
		ffsid.addCall(ffsid.roCli.SetSubjectNameCall(subjKey.GetScriptHash(), subjName))
	}

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "create subject: %w", err)
}

func frostfsidDeleteSubject(cmd *cobra.Command, _ []string) {
	subjectAddress := getFrostfsIDSubjectAddress(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.DeleteSubjectCall(subjectAddress))

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "delete subject error: %w", err)
}

func frostfsidListSubjects(cmd *cobra.Command, _ []string) {
	includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)
	ns := getFrostfsIDNamespace(cmd)
	inv, _, hash := initInvoker(cmd)
	reader := frostfsidrpclient.NewReader(inv, hash)
	sessionID, it, err := reader.ListNamespaceSubjects(ns)
	commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)

	subAddresses, err := frostfsidclient.UnwrapArrayOfUint160(readIterator(inv, &it, sessionID))
	commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err)

	sort.Slice(subAddresses, func(i, j int) bool { return subAddresses[i].Less(subAddresses[j]) })

	for _, addr := range subAddresses {
		if !includeNames {
			cmd.Println(address.Uint160ToString(addr))
			continue
		}

		sessionID, it, err := reader.ListSubjects()
		commonCmd.ExitOnErr(cmd, "can't get subject: %w", err)

		items, err := readIterator(inv, &it, sessionID)
		commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)

		subj, err := frostfsidclient.ParseSubject(items)
		commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err)

		cmd.Printf("%s (%s)\n", address.Uint160ToString(addr), subj.Name)
	}
}

func frostfsidCreateGroup(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)
	groupName := getFrostfsIDGroupName(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.CreateGroupCall(ns, groupName))

	groupID, err := ffsid.roCli.ParseGroupID(ffsid.sendWaitRes())
	commonCmd.ExitOnErr(cmd, "create group: %w", err)

	cmd.Printf("group '%s' created with id: %d\n", groupName, groupID)
}

func frostfsidDeleteGroup(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)
	groupID := getFrostfsIDGroupID(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.DeleteGroupCall(ns, groupID))

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "delete group error: %w", err)
}

func frostfsidListGroups(cmd *cobra.Command, _ []string) {
	inv, _, hash := initInvoker(cmd)
	ns := getFrostfsIDNamespace(cmd)

	reader := frostfsidrpclient.NewReader(inv, hash)
	sessionID, it, err := reader.ListGroups(ns)
	commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)

	items, err := readIterator(inv, &it, sessionID)
	commonCmd.ExitOnErr(cmd, "can't list groups: %w", err)
	groups, err := frostfsidclient.ParseGroups(items)
	commonCmd.ExitOnErr(cmd, "can't parse groups: %w", err)

	sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })

	for _, group := range groups {
		cmd.Printf("%s (%d)\n", group.Name, group.ID)
	}
}

func frostfsidAddSubjectToGroup(cmd *cobra.Command, _ []string) {
	subjectAddress := getFrostfsIDSubjectAddress(cmd)
	groupID := getFrostfsIDGroupID(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.AddSubjectToGroupCall(subjectAddress, groupID))

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "add subject to group error: %w", err)
}

func frostfsidRemoveSubjectFromGroup(cmd *cobra.Command, _ []string) {
	subjectAddress := getFrostfsIDSubjectAddress(cmd)
	groupID := getFrostfsIDGroupID(cmd)

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	ffsid.addCall(ffsid.roCli.RemoveSubjectFromGroupCall(subjectAddress, groupID))

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "remove subject from group error: %w", err)
}

func frostfsidSetKV(cmd *cobra.Command, _ []string) {
	subjectAddress := getFrostfsIDSubjectAddress(cmd)
	key, _ := cmd.Flags().GetString(keyFlag)
	value, _ := cmd.Flags().GetString(valueFlag)

	if key == "" {
		commonCmd.ExitOnErr(cmd, "", errors.New("key can't be empty"))
	}

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	method, args := ffsid.roCli.SetSubjectKVCall(subjectAddress, key, value)

	ffsid.addCall(method, args)

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "set KV: %w", err)
}

func frostfsidDeleteKV(cmd *cobra.Command, _ []string) {
	subjectAddress := getFrostfsIDSubjectAddress(cmd)
	key, _ := cmd.Flags().GetString(keyFlag)

	if key == "" {
		commonCmd.ExitOnErr(cmd, "", errors.New("key can't be empty"))
	}

	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract client: %w", err)

	method, args := ffsid.roCli.DeleteSubjectKVCall(subjectAddress, key)

	ffsid.addCall(method, args)

	err = ffsid.sendWait()
	commonCmd.ExitOnErr(cmd, "delete KV: %w", err)
}

func frostfsidListGroupSubjects(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)
	groupID := getFrostfsIDGroupID(cmd)
	includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)
	inv, cs, hash := initInvoker(cmd)
	_, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
	commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err)

	reader := frostfsidrpclient.NewReader(inv, hash)
	sessionID, it, err := reader.ListGroupSubjects(ns, big.NewInt(groupID))
	commonCmd.ExitOnErr(cmd, "can't list groups: %w", err)

	items, err := readIterator(inv, &it, sessionID)
	commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)

	subjects, err := frostfsidclient.UnwrapArrayOfUint160(items, err)
	commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err)

	sort.Slice(subjects, func(i, j int) bool { return subjects[i].Less(subjects[j]) })

	for _, subjAddr := range subjects {
		if !includeNames {
			cmd.Println(address.Uint160ToString(subjAddr))
			continue
		}

		items, err := reader.GetSubject(subjAddr)
		commonCmd.ExitOnErr(cmd, "can't get subject: %w", err)
		subj, err := frostfsidclient.ParseSubject(items)
		commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err)
		cmd.Printf("%s (%s)\n", address.Uint160ToString(subjAddr), subj.Name)
	}
}

type frostfsidClient struct {
	bw           *io.BufBinWriter
	contractHash util.Uint160
	roCli        *frostfsidclient.Client // client can be used only for waiting tx, parsing and forming method params
	wCtx         *helper.InitializeContext
}

func newFrostfsIDClient(cmd *cobra.Command) (*frostfsidClient, error) {
	wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
	if err != nil {
		return nil, fmt.Errorf("can't initialize context: %w", err)
	}

	r := management.NewReader(wCtx.ReadOnlyInvoker)
	cs, err := helper.GetContractByID(r, 1)
	if err != nil {
		return nil, fmt.Errorf("can't get NNS contract info: %w", err)
	}

	ffsidHash, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
	if err != nil {
		return nil, fmt.Errorf("can't get proxy contract hash: %w", err)
	}

	return &frostfsidClient{
		bw:           io.NewBufBinWriter(),
		contractHash: ffsidHash,
		roCli:        frostfsidclient.NewSimple(wCtx.CommitteeAct, ffsidHash),
		wCtx:         wCtx,
	}, nil
}

func (f *frostfsidClient) addCall(method string, args []any) {
	emit.AppCall(f.bw.BinWriter, f.contractHash, method, callflag.All, args...)
}

func (f *frostfsidClient) sendWait() error {
	if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil {
		return err
	}
	f.bw.Reset()

	return f.wCtx.AwaitTx()
}

func (f *frostfsidClient) sendWaitRes() (*state.AppExecResult, error) {
	if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil {
		return nil, err
	}
	f.bw.Reset()

	f.wCtx.Command.Println("Waiting for transactions to persist...")
	return f.roCli.Wait(f.wCtx.SentTxs[0].Hash, f.wCtx.SentTxs[0].Vub, nil)
}

func readIterator(inv *invoker.Invoker, iter *result.Iterator, sessionID uuid.UUID) ([]stackitem.Item, error) {
	var shouldStop bool
	res := make([]stackitem.Item, 0)
	for !shouldStop {
		items, err := inv.TraverseIterator(sessionID, iter, iteratorBatchSize)
		if err != nil {
			return nil, err
		}

		res = append(res, items...)
		shouldStop = len(items) < iteratorBatchSize
	}

	return res, nil
}

func initInvoker(cmd *cobra.Command) (*invoker.Invoker, *state.Contract, util.Uint160) {
	c, err := helper.NewRemoteClient(viper.GetViper())
	commonCmd.ExitOnErr(cmd, "can't create N3 client: %w", err)

	inv := invoker.New(c, nil)
	r := management.NewReader(inv)

	cs, err := r.GetContractByID(1)
	commonCmd.ExitOnErr(cmd, "can't get NNS contract info: %w", err)

	nmHash, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
	commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err)

	return inv, cs, nmHash
}