package helper

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	io2 "io"
	"os"
	"path/filepath"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"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/smartcontract/context"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
	"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/nspcc-dev/neo-go/pkg/vm/vmstate"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

var (
	errNegativeDuration = errors.New("epoch duration must be positive")
	errNegativeSize     = errors.New("max object size must be positive")
)

type ContractState struct {
	NEF         *nef.File
	RawNEF      []byte
	Manifest    *manifest.Manifest
	RawManifest []byte
	Hash        util.Uint160
}

type Cache struct {
	NNSCs    *state.Contract
	GroupKey *keys.PublicKey
}

type InitializeContext struct {
	ClientContext
	Cache
	// CommitteeAcc is used for retrieving the committee address and the verification script.
	CommitteeAcc *wallet.Account
	// ConsensusAcc is used for retrieving the committee address and the verification script.
	ConsensusAcc *wallet.Account
	Wallets      []*wallet.Wallet
	// ContractWallet is a wallet for providing the contract group signature.
	ContractWallet *wallet.Wallet
	// Accounts contains simple signature accounts in the same order as in Wallets.
	Accounts     []*wallet.Account
	Contracts    map[string]*ContractState
	Command      *cobra.Command
	ContractPath string
	ContractURL  string
}

func (cs *ContractState) Parse() error {
	nf, err := nef.FileFromBytes(cs.RawNEF)
	if err != nil {
		return fmt.Errorf("can't parse NEF file: %w", err)
	}

	m := new(manifest.Manifest)
	if err := json.Unmarshal(cs.RawManifest, m); err != nil {
		return fmt.Errorf("can't parse manifest file: %w", err)
	}

	cs.NEF = &nf
	cs.Manifest = m
	return nil
}

func NewInitializeContext(cmd *cobra.Command, v *viper.Viper) (*InitializeContext, error) {
	walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
	wallets, err := GetAlphabetWallets(v, walletDir)
	if err != nil {
		return nil, err
	}

	needContracts := cmd.Name() == "update-contracts" || cmd.Name() == "init"

	var w *wallet.Wallet
	w, err = getWallet(cmd, v, needContracts, walletDir)
	if err != nil {
		return nil, err
	}

	c, err := createClient(cmd, v, wallets)
	if err != nil {
		return nil, err
	}

	committeeAcc, err := GetWalletAccount(wallets[0], constants.CommitteeAccountName)
	if err != nil {
		return nil, fmt.Errorf("can't find committee account: %w", err)
	}

	consensusAcc, err := GetWalletAccount(wallets[0], constants.ConsensusAccountName)
	if err != nil {
		return nil, fmt.Errorf("can't find consensus account: %w", err)
	}

	if err := validateInit(cmd); err != nil {
		return nil, err
	}

	ctrPath, err := getContractsPath(cmd, needContracts)
	if err != nil {
		return nil, err
	}

	var ctrURL string
	if needContracts {
		ctrURL, _ = cmd.Flags().GetString(commonflags.ContractsURLFlag)
	}

	if err := CheckNotaryEnabled(c); err != nil {
		return nil, err
	}

	accounts, err := createWalletAccounts(wallets)
	if err != nil {
		return nil, err
	}

	cliCtx, err := DefaultClientContext(c, committeeAcc)
	if err != nil {
		return nil, fmt.Errorf("client context: %w", err)
	}

	initCtx := &InitializeContext{
		ClientContext:  *cliCtx,
		ConsensusAcc:   consensusAcc,
		CommitteeAcc:   committeeAcc,
		ContractWallet: w,
		Wallets:        wallets,
		Accounts:       accounts,
		Command:        cmd,
		Contracts:      make(map[string]*ContractState),
		ContractPath:   ctrPath,
		ContractURL:    ctrURL,
	}

	if needContracts {
		err := readContracts(initCtx, constants.FullContractList)
		if err != nil {
			return nil, err
		}
	}

	return initCtx, nil
}

func validateInit(cmd *cobra.Command) error {
	if cmd.Name() != "init" {
		return nil
	}
	if viper.GetInt64(commonflags.EpochDurationInitFlag) <= 0 {
		return errNegativeDuration
	}

	if viper.GetInt64(commonflags.MaxObjectSizeInitFlag) <= 0 {
		return errNegativeSize
	}

	return nil
}

func createClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (Client, error) {
	var c Client
	var err error
	if ldf := cmd.Flags().Lookup(commonflags.LocalDumpFlag); ldf != nil && ldf.Changed {
		if cmd.Flags().Changed(commonflags.EndpointFlag) {
			return nil, fmt.Errorf("`%s` and `%s` flags are mutually exclusive", commonflags.EndpointFlag, commonflags.LocalDumpFlag)
		}
		c, err = NewLocalClient(cmd, v, wallets, ldf.Value.String())
	} else {
		c, err = GetN3Client(v)
	}
	if err != nil {
		return nil, fmt.Errorf("can't create N3 client: %w", err)
	}
	return c, nil
}

func getContractsPath(cmd *cobra.Command, needContracts bool) (string, error) {
	if !needContracts {
		return "", nil
	}

	ctrPath, err := cmd.Flags().GetString(commonflags.ContractsInitFlag)
	if err != nil {
		return "", fmt.Errorf("invalid contracts path: %w", err)
	}
	return ctrPath, nil
}

func createWalletAccounts(wallets []*wallet.Wallet) ([]*wallet.Account, error) {
	accounts := make([]*wallet.Account, len(wallets))
	for i, w := range wallets {
		acc, err := GetWalletAccount(w, constants.SingleAccountName)
		if err != nil {
			return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err)
		}
		accounts[i] = acc
	}
	return accounts, nil
}

func readContracts(c *InitializeContext, names []string) error {
	var (
		fi  os.FileInfo
		err error
	)
	if c.ContractPath != "" {
		fi, err = os.Stat(c.ContractPath)
		if err != nil {
			return fmt.Errorf("invalid contracts path: %w", err)
		}
	}

	if c.ContractPath != "" && fi.IsDir() {
		for _, ctrName := range names {
			cs, err := ReadContract(filepath.Join(c.ContractPath, ctrName), ctrName)
			if err != nil {
				return err
			}
			c.Contracts[ctrName] = cs
		}
	} else {
		var r io2.ReadCloser
		if c.ContractPath != "" {
			r, err = os.Open(c.ContractPath)
		} else if c.ContractURL != "" {
			r, err = downloadContracts(c.Command, c.ContractURL)
		} else {
			r, err = downloadContractsFromRepository(c.Command)
		}
		if err != nil {
			return fmt.Errorf("can't open contracts archive: %w", err)
		}
		defer r.Close()

		m, err := readContractsFromArchive(r, names)
		if err != nil {
			return err
		}
		for _, name := range names {
			if err := m[name].Parse(); err != nil {
				return err
			}
			c.Contracts[name] = m[name]
		}
	}

	for _, ctrName := range names {
		if ctrName != constants.AlphabetContract {
			cs := c.Contracts[ctrName]
			cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(),
				cs.NEF.Checksum, cs.Manifest.Name)
		}
	}
	return nil
}

func (c *InitializeContext) Close() {
	if local, ok := c.Client.(*LocalClient); ok {
		err := local.Dump()
		if err != nil {
			c.Command.PrintErrf("Can't write dump: %v\n", err)
			os.Exit(1)
		}
	}
}

func (c *InitializeContext) AwaitTx() error {
	return c.ClientContext.AwaitTx(c.Command)
}

func (c *InitializeContext) NNSContractState() (*state.Contract, error) {
	if c.NNSCs != nil {
		return c.NNSCs, nil
	}

	r := management.NewReader(c.ReadOnlyInvoker)
	cs, err := r.GetContractByID(1)
	if err != nil {
		return nil, err
	}

	c.NNSCs = cs
	return cs, nil
}

func (c *InitializeContext) GetSigner(tryGroup bool, acc *wallet.Account) transaction.Signer {
	if tryGroup && c.GroupKey != nil {
		return transaction.Signer{
			Account:       acc.Contract.ScriptHash(),
			Scopes:        transaction.CustomGroups,
			AllowedGroups: keys.PublicKeys{c.GroupKey},
		}
	}

	signer := transaction.Signer{
		Account: acc.Contract.ScriptHash(),
		Scopes:  transaction.Global, // Scope is important, as we have nested call to container contract.
	}

	if !tryGroup {
		return signer
	}

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

	groupKey, err := NNSResolveKey(c.ReadOnlyInvoker, nnsCs.Hash, client.NNSGroupKeyName)
	if err == nil {
		c.GroupKey = groupKey

		signer.Scopes = transaction.CustomGroups
		signer.AllowedGroups = keys.PublicKeys{groupKey}
	}
	return signer
}

// SendCommitteeTx creates transaction from script, signs it by committee nodes and sends it to RPC.
// If tryGroup is false, global scope is used for the signer (useful when
// working with native contracts).
func (c *InitializeContext) SendCommitteeTx(script []byte, tryGroup bool) error {
	return c.sendMultiTx(script, tryGroup, false)
}

// SendConsensusTx creates transaction from script, signs it by alphabet nodes and sends it to RPC.
// Not that because this is used only after the contracts were initialized and deployed,
// we always try to have a group scope.
func (c *InitializeContext) SendConsensusTx(script []byte) error {
	return c.sendMultiTx(script, true, true)
}

func (c *InitializeContext) sendMultiTx(script []byte, tryGroup bool, withConsensus bool) error {
	var act *actor.Actor
	var err error

	withConsensus = withConsensus && !c.ConsensusAcc.Contract.ScriptHash().Equals(c.CommitteeAcc.ScriptHash())
	if tryGroup {
		// Even for consensus signatures we need the committee to pay.
		signers := make([]actor.SignerAccount, 1, 2)
		signers[0] = actor.SignerAccount{
			Signer:  c.GetSigner(tryGroup, c.CommitteeAcc),
			Account: c.CommitteeAcc,
		}
		if withConsensus {
			signers = append(signers, actor.SignerAccount{
				Signer:  c.GetSigner(tryGroup, c.ConsensusAcc),
				Account: c.ConsensusAcc,
			})
		}
		act, err = actor.New(c.Client, signers)
	} else {
		if withConsensus {
			panic("BUG: should never happen")
		}
		act, err = c.CommitteeAct, nil
	}
	if err != nil {
		return fmt.Errorf("could not create actor: %w", err)
	}

	tx, err := act.MakeUnsignedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}})
	if err != nil {
		return fmt.Errorf("could not perform test invocation: %w", err)
	}

	if err := c.MultiSign(tx, constants.CommitteeAccountName); err != nil {
		return err
	}
	if withConsensus {
		if err := c.MultiSign(tx, constants.ConsensusAccountName); err != nil {
			return err
		}
	}

	return c.SendTx(tx, c.Command, false)
}

func (c *InitializeContext) MultiSignAndSend(tx *transaction.Transaction, accType string) error {
	if err := c.MultiSign(tx, accType); err != nil {
		return err
	}

	return c.SendTx(tx, c.Command, false)
}

func (c *InitializeContext) MultiSign(tx *transaction.Transaction, accType string) error {
	version, err := c.Client.GetVersion()
	if err != nil {
		// error appears only if client
		// has not been initialized
		panic(err)
	}
	network := version.Protocol.Network

	// Use parameter context to avoid dealing with signature order.
	pc := context.NewParameterContext("", network, tx)
	h := c.CommitteeAcc.Contract.ScriptHash()
	if accType == constants.ConsensusAccountName {
		h = c.ConsensusAcc.Contract.ScriptHash()
	}
	for _, w := range c.Wallets {
		acc, err := GetWalletAccount(w, accType)
		if err != nil {
			return fmt.Errorf("can't find %s wallet account: %w", accType, err)
		}

		priv := acc.PrivateKey()
		sign := priv.SignHashable(uint32(network), tx)
		if err := pc.AddSignature(h, acc.Contract, priv.PublicKey(), sign); err != nil {
			return fmt.Errorf("can't add signature: %w", err)
		}
		if len(pc.Items[h].Signatures) == len(acc.Contract.Parameters) {
			break
		}
	}

	w, err := pc.GetWitness(h)
	if err != nil {
		return fmt.Errorf("incomplete signature: %w", err)
	}

	for i := range tx.Signers {
		if tx.Signers[i].Account == h {
			if i < len(tx.Scripts) {
				tx.Scripts[i] = *w
			} else if i == len(tx.Scripts) {
				tx.Scripts = append(tx.Scripts, *w)
			} else {
				panic("BUG: invalid signing order")
			}
			return nil
		}
	}

	return fmt.Errorf("%s account was not found among transaction signers", accType)
}

// EmitUpdateNNSGroupScript emits script for updating group key stored in NNS.
// First return value is true iff the key is already there and nothing should be done.
// Second return value is true iff a domain registration code was emitted.
func (c *InitializeContext) EmitUpdateNNSGroupScript(bw *io.BufBinWriter, nnsHash util.Uint160, pub *keys.PublicKey) (bool, bool, error) {
	isAvail, err := NNSIsAvailable(c.Client, nnsHash, client.NNSGroupKeyName)
	if err != nil {
		return false, false, err
	}

	if !isAvail {
		currentPub, err := NNSResolveKey(c.ReadOnlyInvoker, nnsHash, client.NNSGroupKeyName)
		if err != nil {
			return false, false, err
		}

		if pub.Equal(currentPub) {
			return true, false, nil
		}
	}

	if isAvail {
		emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
			client.NNSGroupKeyName, c.CommitteeAcc.Contract.ScriptHash(),
			constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
			int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
		emit.Opcodes(bw.BinWriter, opcode.ASSERT)
	}

	emit.AppCall(bw.BinWriter, nnsHash, "deleteRecords", callflag.All, "group.frostfs", int64(nns.TXT))
	emit.AppCall(bw.BinWriter, nnsHash, "addRecord", callflag.All,
		"group.frostfs", int64(nns.TXT), hex.EncodeToString(pub.Bytes()))

	return false, isAvail, nil
}

func (c *InitializeContext) NNSRegisterDomainScript(nnsHash, expectedHash util.Uint160, domain string) ([]byte, bool, error) {
	ok, err := NNSIsAvailable(c.Client, nnsHash, domain)
	if err != nil {
		return nil, false, err
	}

	if ok {
		bw := io.NewBufBinWriter()
		emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
			domain, c.CommitteeAcc.Contract.ScriptHash(),
			constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
			int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
		emit.Opcodes(bw.BinWriter, opcode.ASSERT)

		if bw.Err != nil {
			panic(bw.Err)
		}
		return bw.Bytes(), false, nil
	}

	s, err := NNSResolveHash(c.ReadOnlyInvoker, nnsHash, domain)
	if err != nil {
		return nil, false, err
	}
	return nil, s == expectedHash, nil
}

func (c *InitializeContext) NNSRootRegistered(nnsHash util.Uint160, zone string) (bool, error) {
	res, err := c.CommitteeAct.Call(nnsHash, "isAvailable", "name."+zone)
	if err != nil {
		return false, err
	}

	return res.State == vmstate.Halt.String(), nil
}

func (c *InitializeContext) IsUpdated(ctrHash util.Uint160, cs *ContractState) bool {
	r := management.NewReader(c.ReadOnlyInvoker)
	realCs, err := r.GetContract(ctrHash)
	return err == nil && realCs != nil && realCs.NEF.Checksum == cs.NEF.Checksum
}

func (c *InitializeContext) GetContract(ctrName string) *ContractState {
	return c.Contracts[ctrName]
}

func (c *InitializeContext) GetAlphabetDeployItems(i, n int) []any {
	items := make([]any, 5)
	items[0] = c.Contracts[constants.NetmapContract].Hash
	items[1] = c.Contracts[constants.ProxyContract].Hash
	items[2] = innerring.GlagoliticLetter(i).String()
	items[3] = int64(i)
	items[4] = int64(n)
	return items
}