package helper

import (
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
	"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/rpcclient/unwrap"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"github.com/spf13/viper"
)

func getFrostfsIDAdminFromContract(roInvoker *invoker.Invoker) (util.Uint160, bool, error) {
	r := management.NewReader(roInvoker)
	cs, err := r.GetContractByID(1)
	if err != nil {
		return util.Uint160{}, false, fmt.Errorf("get nns contract: %w", err)
	}
	fidHash, err := NNSResolveHash(roInvoker, cs.Hash, DomainOf(constants.FrostfsIDContract))
	if err != nil {
		return util.Uint160{}, false, fmt.Errorf("resolve frostfsid contract hash: %w", err)
	}
	item, err := unwrap.Item(roInvoker.Call(fidHash, "getAdmin"))
	if err != nil {
		return util.Uint160{}, false, fmt.Errorf("getAdmin: %w", err)
	}
	if _, ok := item.(stackitem.Null); ok {
		return util.Uint160{}, false, nil
	}

	bs, err := item.TryBytes()
	if err != nil {
		return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err)
	}
	h, err := util.Uint160DecodeBytesBE(bs)
	if err != nil {
		return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err)
	}
	return h, true, nil
}

func GetContractDeployData(c *InitializeContext, ctrName string, keysParam []any, method string) ([]any, error) {
	items := make([]any, 0, 6)

	switch ctrName {
	case constants.FrostfsContract:
		items = append(items,
			c.Contracts[constants.ProcessingContract].Hash,
			keysParam,
			smartcontract.Parameter{})
	case constants.ProcessingContract:
		items = append(items, c.Contracts[constants.FrostfsContract].Hash)
		return items[1:], nil // no notary info
	case constants.BalanceContract:
		items = append(items,
			c.Contracts[constants.NetmapContract].Hash,
			c.Contracts[constants.ContainerContract].Hash)
	case constants.ContainerContract:
		// In case if NNS is updated multiple times, we can't calculate
		// it's actual hash based on local data, thus query chain.
		r := management.NewReader(c.ReadOnlyInvoker)
		nnsCs, err := r.GetContractByID(1)
		if err != nil {
			return nil, fmt.Errorf("get nns contract: %w", err)
		}
		items = append(items,
			c.Contracts[constants.NetmapContract].Hash,
			c.Contracts[constants.BalanceContract].Hash,
			c.Contracts[constants.FrostfsIDContract].Hash,
			nnsCs.Hash,
			"container")
	case constants.FrostfsIDContract:
		var (
			h     util.Uint160
			found bool
			err   error
		)
		if method == constants.UpdateMethodName {
			h, found, err = getFrostfsIDAdminFromContract(c.ReadOnlyInvoker)
		}
		if method != constants.UpdateMethodName || err == nil && !found {
			h, found, err = GetFrostfsIDAdmin(viper.GetViper())
		}
		if err != nil {
			return nil, err
		}

		if found {
			items = append(items, h)
		} else {
			items = append(items, c.Contracts[constants.ProxyContract].Hash)
		}
	case constants.NetmapContract:
		md := GetDefaultNetmapContractConfigMap()
		if method == constants.UpdateMethodName {
			if err := MergeNetmapConfig(c.ReadOnlyInvoker, md); err != nil {
				return nil, err
			}
		}

		var configParam []any
		for k, v := range md {
			configParam = append(configParam, k, v)
		}

		items = append(items,
			c.Contracts[constants.BalanceContract].Hash,
			c.Contracts[constants.ContainerContract].Hash,
			keysParam,
			configParam)
	case constants.ProxyContract:
		items = nil
	case constants.PolicyContract:
		items = append(items, c.Contracts[constants.ProxyContract].Hash)
	default:
		panic("invalid contract name: " + ctrName)
	}
	return items, nil
}

func GetContractDeployParameters(cs *ContractState, deployData []any) []any {
	return []any{cs.RawNEF, cs.RawManifest, deployData}
}

func DeployNNS(c *InitializeContext, method string) error {
	cs := c.GetContract(constants.NNSContract)
	h := cs.Hash

	nnsCs, err := c.NNSContractState()
	if err != nil {
		return err
	}
	if nnsCs != nil {
		if nnsCs.NEF.Checksum == cs.NEF.Checksum {
			if method == constants.DeployMethodName {
				c.Command.Println("NNS contract is already deployed.")
			} else {
				c.Command.Println("NNS contract is already updated.")
			}
			return nil
		}
		h = nnsCs.Hash
	}

	err = AddManifestGroup(c.ContractWallet, h, cs)
	if err != nil {
		return fmt.Errorf("can't sign manifest group: %v", err)
	}

	params := GetContractDeployParameters(cs, nil)

	invokeHash := management.Hash
	if method == constants.UpdateMethodName {
		invokeHash = nnsCs.Hash
	}

	tx, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
	if err != nil {
		return fmt.Errorf("failed to create deploy tx for %s: %w", constants.NNSContract, err)
	}

	if err := c.MultiSignAndSend(tx, constants.CommitteeAccountName); err != nil {
		return fmt.Errorf("can't send deploy transaction: %w", err)
	}

	return c.AwaitTx()
}