package helper

import (
	"errors"
	"fmt"
	"time"

	"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"
	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	nns2 "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
	"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 ErrTooManyAlphabetNodes = fmt.Errorf("too many alphabet nodes (maximum allowed is %d)", constants.MaxAlphabetNodes)

func AwaitTx(cmd *cobra.Command, c Client, txs []HashVUBPair) error {
	cmd.Println("Waiting for transactions to persist...")

	at := trigger.Application

	var retErr error

loop:
	for i := range txs {
		var it int
		var pollInterval time.Duration
		var pollIntervalChanged bool
		for {
			// We must fetch current height before application log, to avoid race condition.
			currBlock, err := c.GetBlockCount()
			if err != nil {
				return fmt.Errorf("can't fetch current block height: %w", err)
			}
			res, err := c.GetApplicationLog(txs[i].Hash, &at)
			if err == nil {
				if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
					retErr = fmt.Errorf("tx %d persisted in %s state: %s",
						i, res.Executions[0].VMState, res.Executions[0].FaultException)
				}
				continue loop
			}
			if txs[i].Vub < currBlock {
				return fmt.Errorf("tx was not persisted: Vub=%d, height=%d", txs[i].Vub, currBlock)
			}

			pollInterval, pollIntervalChanged = NextPollInterval(it, pollInterval)
			if pollIntervalChanged && viper.GetBool(commonflags.Verbose) {
				cmd.Printf("Pool interval to check transaction persistence changed: %s\n", pollInterval.String())
			}

			timer := time.NewTimer(pollInterval)
			select {
			case <-cmd.Context().Done():
				return cmd.Context().Err()
			case <-timer.C:
			}

			it++
		}
	}

	return retErr
}

func NextPollInterval(it int, previous time.Duration) (time.Duration, bool) {
	const minPollInterval = 1 * time.Second
	const maxPollInterval = 16 * time.Second
	const changeAfter = 5
	if it == 0 {
		return minPollInterval, true
	}
	if it%changeAfter != 0 {
		return previous, false
	}
	nextInterval := previous * 2
	if nextInterval > maxPollInterval {
		return maxPollInterval, previous != maxPollInterval
	}
	return nextInterval, true
}

func GetWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) {
	for i := range w.Accounts {
		if w.Accounts[i].Label == typ {
			return w.Accounts[i], nil
		}
	}
	return nil, fmt.Errorf("account for '%s' not found", typ)
}

func GetComitteAcc(cmd *cobra.Command, v *viper.Viper) *wallet.Account {
	walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
	wallets, err := GetAlphabetWallets(v, walletDir)
	commonCmd.ExitOnErr(cmd, "unable to get alphabet wallets: %w", err)

	committeeAcc, err := GetWalletAccount(wallets[0], constants.CommitteeAccountName)
	commonCmd.ExitOnErr(cmd, "can't find committee account: %w", err)
	return committeeAcc
}

func NNSResolve(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
	return unwrap.Item(inv.Call(nnsHash, "resolve", domain, int64(nns.TXT)))
}

// ParseNNSResolveResult parses the result of resolving NNS record.
// It works with multiple formats (corresponding to multiple NNS versions).
// If array of hashes is provided, it returns only the first one.
func ParseNNSResolveResult(res stackitem.Item) (util.Uint160, error) {
	arr, ok := res.Value().([]stackitem.Item)
	if !ok {
		arr = []stackitem.Item{res}
	}
	if _, ok := res.Value().(stackitem.Null); ok || len(arr) == 0 {
		return util.Uint160{}, errors.New("NNS record is missing")
	}
	for i := range arr {
		bs, err := arr[i].TryBytes()
		if err != nil {
			continue
		}

		// We support several formats for hash encoding, this logic should be maintained in sync
		// with NNSResolve from pkg/morph/client/nns.go
		h, err := util.Uint160DecodeStringLE(string(bs))
		if err == nil {
			return h, nil
		}

		h, err = address.StringToUint160(string(bs))
		if err == nil {
			return h, nil
		}
	}
	return util.Uint160{}, errors.New("no valid hashes are found")
}

// NNSResolveHash Returns errMissingNNSRecord if invocation fault exception contains "token not found".
func NNSResolveHash(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (util.Uint160, error) {
	item, err := NNSResolve(inv, nnsHash, domain)
	if err != nil {
		return util.Uint160{}, err
	}
	return ParseNNSResolveResult(item)
}

func DomainOf(contract string) string {
	return contract + ".frostfs"
}

func NNSResolveKey(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (*keys.PublicKey, error) {
	res, err := NNSResolve(inv, nnsHash, domain)
	if err != nil {
		return nil, err
	}
	if _, ok := res.Value().(stackitem.Null); ok {
		return nil, errors.New("NNS record is missing")
	}
	arr, ok := res.Value().([]stackitem.Item)
	if !ok {
		return nil, errors.New("API of the NNS contract method `resolve` has changed")
	}
	for i := range arr {
		var bs []byte
		bs, err = arr[i].TryBytes()
		if err != nil {
			continue
		}

		return keys.NewPublicKeyFromString(string(bs))
	}
	return nil, errors.New("no valid keys are found")
}

func NNSIsAvailable(c Client, nnsHash util.Uint160, name string) (bool, error) {
	switch c.(type) {
	case *rpcclient.Client:
		inv := invoker.New(c, nil)
		reader := nns2.NewReader(inv, nnsHash)
		return reader.IsAvailable(name)
	default:
		b, err := unwrap.Bool(InvokeFunction(c, nnsHash, "isAvailable", []any{name}, nil))
		if err != nil {
			return false, fmt.Errorf("`isAvailable`: invalid response: %w", err)
		}

		return b, nil
	}
}

func CheckNotaryEnabled(c Client) error {
	ns, err := c.GetNativeContracts()
	if err != nil {
		return fmt.Errorf("can't get native contract hashes: %w", err)
	}

	notaryEnabled := false
	nativeHashes := make(map[string]util.Uint160, len(ns))
	for i := range ns {
		if ns[i].Manifest.Name == nativenames.Notary {
			notaryEnabled = true
		}
		nativeHashes[ns[i].Manifest.Name] = ns[i].Hash
	}
	if !notaryEnabled {
		return errors.New("notary contract must be enabled")
	}
	return nil
}