package frostfsid

import (
	"errors"
	"fmt"
	"sort"

	frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	morphUtil "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/util"
	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	"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/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/spf13/cobra"
	"github.com/spf13/viper"
)

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

	frostfsIDAdminConfigKey  = "frostfsid.admin"
	rootNamespacePlaceholder = "<root>"
)

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(morphUtil.AlphabetWalletsFlag, cmd.Flags().Lookup(morphUtil.AlphabetWalletsFlag))
			_ = viper.BindPFlag(morphUtil.EndpointFlag, cmd.Flags().Lookup(morphUtil.EndpointFlag))
		},
		Run: frostfsidCreateNamespace,
	}

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

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

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

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

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

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

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

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

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

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

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

func initFrostfsIDListNamespacesCmd() {
	Cmd.AddCommand(frostfsidListNamespacesCmd)
	frostfsidListNamespacesCmd.Flags().StringP(morphUtil.EndpointFlag, morphUtil.EndpointFlagShort, "", morphUtil.EndpointFlagDesc)
	frostfsidListNamespacesCmd.Flags().String(morphUtil.AlphabetWalletsFlag, "", morphUtil.AlphabetWalletsFlagDesc)
}

func initFrostfsIDCreateSubjectCmd() {
	Cmd.AddCommand(frostfsidCreateSubjectCmd)
	frostfsidCreateSubjectCmd.Flags().StringP(morphUtil.EndpointFlag, morphUtil.EndpointFlagShort, "", morphUtil.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(morphUtil.AlphabetWalletsFlag, "", morphUtil.AlphabetWalletsFlagDesc)
}

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

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

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

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

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

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

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

func initFrostfsIDListGroupSubjectsCmd() {
	Cmd.AddCommand(frostfsidListGroupSubjectsCmd)
	frostfsidListGroupSubjectsCmd.Flags().StringP(morphUtil.EndpointFlag, morphUtil.EndpointFlagShort, "", morphUtil.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)")
	frostfsidListGroupSubjectsCmd.Flags().String(morphUtil.AlphabetWalletsFlag, "", morphUtil.AlphabetWalletsFlagDesc)
}

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) {
	ffsid, err := newFrostfsIDClient(cmd)
	commonCmd.ExitOnErr(cmd, "init contract invoker: %w", err)

	namespaces, err := ffsid.roCli.ListNamespaces()
	commonCmd.ExitOnErr(cmd, "list namespaces: %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) {
	ns := getFrostfsIDNamespace(cmd)
	includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)

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

	subAddresses, err := ffsid.roCli.ListNamespaceSubjects(ns)
	commonCmd.ExitOnErr(cmd, "list subjects: %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
		}

		subj, err := ffsid.roCli.GetSubject(addr)
		commonCmd.ExitOnErr(cmd, "get 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) {
	ns := getFrostfsIDNamespace(cmd)

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

	groups, err := ffsid.roCli.ListGroups(ns)
	commonCmd.ExitOnErr(cmd, "list 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 frostfsidListGroupSubjects(cmd *cobra.Command, _ []string) {
	ns := getFrostfsIDNamespace(cmd)
	groupID := getFrostfsIDGroupID(cmd)
	includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)

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

	subjects, err := ffsid.roCli.ListGroupSubjects(ns, groupID)
	commonCmd.ExitOnErr(cmd, "list group subjects: %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
		}

		subj, err := ffsid.roCli.GetSubject(subjAddr)
		commonCmd.ExitOnErr(cmd, "get 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         *morphUtil.InitializeContext
}

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

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

	ffsidHash, err := morphUtil.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, morphUtil.DomainOf(morphUtil.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()

	if len(f.wCtx.SentTxs) == 0 {
		return nil, errors.New("no transactions to wait")
	}

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