package contract

import (
	"encoding/hex"
	"errors"
	"fmt"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
	"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
	morphUtil "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/util"
	morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	io2 "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"
	neoUtil "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/opcode"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var errMissingNNSRecord = errors.New("missing NNS record")

func updateContracts(cmd *cobra.Command, _ []string) error {
	wCtx, err := morphUtil.NewInitializeContext(cmd, viper.GetViper())
	if err != nil {
		return fmt.Errorf("initialization error: %w", err)
	}

	if err := morphUtil.DeployNNS(wCtx, morphUtil.UpdateMethodName); err != nil {
		return err
	}

	return updateContractsInternal(wCtx)
}

func updateContractsInternal(c *morphUtil.InitializeContext) error {
	alphaCs := c.GetContract(morphUtil.AlphabetContract)

	nnsCs, err := c.NNSContractState()
	if err != nil {
		return err
	}
	nnsHash := nnsCs.Hash

	w := io2.NewBufBinWriter()

	// Update script size for a single-node committee is close to the maximum allowed size of 65535.
	// Because of this we want to reuse alphabet contract NEF and manifest for different updates.
	// The generated script is as following.
	// 1. Initialize static slot for alphabet NEF.
	// 2. Store NEF into the static slot.
	// 3. Push parameters for each alphabet contract on stack.
	// 4. Add contract group to the manifest.
	// 5. For each alphabet contract, invoke `update` using parameters on stack and
	//    NEF from step 2 and manifest from step 4.
	emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
	emit.Bytes(w.BinWriter, alphaCs.RawNEF)
	emit.Opcodes(w.BinWriter, opcode.STSFLD0)

	keysParam, err := deployAlphabetAccounts(c, nnsHash, w, alphaCs)
	if err != nil {
		return err
	}

	w.Reset()

	if err = deployOrUpdateContracts(c, w, nnsHash, keysParam); err != nil {
		return err
	}

	groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
	_, _, err = c.EmitUpdateNNSGroupScript(w, nnsHash, groupKey)
	if err != nil {
		return err
	}
	c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))

	emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
	emit.Int(w.BinWriter, 1)
	emit.Opcodes(w.BinWriter, opcode.PACK)
	emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)

	if err := c.SendCommitteeTx(w.Bytes(), false); err != nil {
		return err
	}
	return c.AwaitTx()
}

func deployAlphabetAccounts(c *morphUtil.InitializeContext, nnsHash neoUtil.Uint160, w *io2.BufBinWriter, alphaCs *morphUtil.ContractState) ([]any, error) {
	var keysParam []any

	baseGroups := alphaCs.Manifest.Groups

	// alphabet contracts should be deployed by individual nodes to get different hashes.
	for i, acc := range c.Accounts {
		ctrHash, err := morphUtil.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, morphUtil.GetAlphabetNNSDomain(i))
		if err != nil {
			return nil, fmt.Errorf("can't resolve hash for contract update: %w", err)
		}

		keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())

		params := c.GetAlphabetDeployItems(i, len(c.Wallets))
		emit.Array(w.BinWriter, params...)

		alphaCs.Manifest.Groups = baseGroups
		err = morphUtil.AddManifestGroup(c.ContractWallet, ctrHash, alphaCs)
		if err != nil {
			return nil, fmt.Errorf("can't sign manifest group: %v", err)
		}

		emit.Bytes(w.BinWriter, alphaCs.RawManifest)
		emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
		emit.Int(w.BinWriter, 3)
		emit.Opcodes(w.BinWriter, opcode.PACK)
		emit.AppCallNoArgs(w.BinWriter, ctrHash, morphUtil.UpdateMethodName, callflag.All)
	}
	if err := c.SendCommitteeTx(w.Bytes(), false); err != nil {
		if !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
			return nil, err
		}
		c.Command.Println("Alphabet contracts are already updated.")
	}

	return keysParam, nil
}

func deployOrUpdateContracts(c *morphUtil.InitializeContext, w *io2.BufBinWriter, nnsHash neoUtil.Uint160, keysParam []any) error {
	emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
	emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
	emit.Opcodes(w.BinWriter, opcode.STSFLD0)
	emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)

	for _, ctrName := range morphUtil.ContractList {
		cs := c.GetContract(ctrName)

		method := morphUtil.UpdateMethodName
		ctrHash, err := morphUtil.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, morphUtil.DomainOf(ctrName))
		if err != nil {
			if errors.Is(err, errMissingNNSRecord) {
				// if contract not found we deploy it instead of update
				method = morphUtil.DeployMethodName
			} else {
				return fmt.Errorf("can't resolve hash for contract update: %w", err)
			}
		}

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

		invokeHash := management.Hash
		if method == morphUtil.UpdateMethodName {
			invokeHash = ctrHash
		}

		args, err := morphUtil.GetContractDeployData(c, ctrName, keysParam, morphUtil.UpdateMethodName)
		if err != nil {
			return fmt.Errorf("%s: getting update params: %v", ctrName, err)
		}
		params := morphUtil.GetContractDeployParameters(cs, args)
		res, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
		if err != nil {
			if method != morphUtil.UpdateMethodName || !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
				return fmt.Errorf("deploy contract: %w", err)
			}
			c.Command.Printf("%s contract is already updated.\n", ctrName)
			continue
		}

		w.WriteBytes(res.Script)

		if method == morphUtil.DeployMethodName {
			// same actions are done in InitializeContext.setNNS, can be unified
			domain := ctrName + ".frostfs"
			script, ok, err := c.NNSRegisterDomainScript(nnsHash, cs.Hash, domain)
			if err != nil {
				return err
			}
			if !ok {
				w.WriteBytes(script)
				emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
				emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
					domain, int64(nns.TXT), cs.Hash.StringLE())
				emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
					domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
			}
			c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
		}
	}
	return nil
}