package helper

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
	"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/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// Client represents N3 client interface capable of test-invoking scripts
// and sending signed transactions to chain.
type Client interface {
	invoker.RPCInvoke

	GetBlockCount() (uint32, error)
	GetNativeContracts() ([]state.Contract, error)
	GetApplicationLog(util.Uint256, *trigger.Type) (*result.ApplicationLog, error)
	GetVersion() (*result.Version, error)
	SendRawTransaction(*transaction.Transaction) (util.Uint256, error)
	GetCommittee() (keys.PublicKeys, error)
	CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
}

type HashVUBPair struct {
	Hash util.Uint256
	Vub  uint32
}

type ClientContext struct {
	Client          Client           // a raw neo-go client OR a local chain implementation
	CommitteeAct    *actor.Actor     // committee actor with the Global witness scope
	ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
	SentTxs         []HashVUBPair
}

func GetN3Client(v *viper.Viper) (Client, error) {
	// number of opened connections
	// by neo-go client per one host
	const (
		maxConnsPerHost = 10
		requestTimeout  = time.Second * 10
	)

	ctx := context.Background()
	endpoint := v.GetString(commonflags.EndpointFlag)
	if endpoint == "" {
		return nil, errors.New("missing endpoint")
	}

	var cfg *tls.Config
	if rootCAs := v.GetStringSlice("tls.trusted_ca_list"); len(rootCAs) != 0 {
		certFile := v.GetString("tls.certificate")
		keyFile := v.GetString("tls.key")

		tlsConfig, err := rpcclient.TLSClientConfig(rootCAs, certFile, keyFile)
		if err != nil {
			return nil, err
		}

		cfg = tlsConfig
	}
	c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
		MaxConnsPerHost: maxConnsPerHost,
		RequestTimeout:  requestTimeout,
		TLSClientConfig: cfg,
	})
	if err != nil {
		return nil, err
	}
	if err := c.Init(); err != nil {
		return nil, err
	}
	return c, nil
}

func DefaultClientContext(c Client, committeeAcc *wallet.Account) (*ClientContext, error) {
	commAct, err := NewActor(c, committeeAcc)
	if err != nil {
		return nil, err
	}

	return &ClientContext{
		Client:          c,
		CommitteeAct:    commAct,
		ReadOnlyInvoker: invoker.New(c, nil),
	}, nil
}

func (c *ClientContext) SendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
	h, err := c.Client.SendRawTransaction(tx)
	if err != nil {
		return err
	}

	if h != tx.Hash() {
		return fmt.Errorf("sent and actual tx hashes mismatch:\n\tsent: %v\n\tactual: %v", tx.Hash().StringLE(), h.StringLE())
	}

	c.SentTxs = append(c.SentTxs, HashVUBPair{Hash: h, Vub: tx.ValidUntilBlock})

	if await {
		return c.AwaitTx(cmd)
	}
	return nil
}

func (c *ClientContext) AwaitTx(cmd *cobra.Command) error {
	if len(c.SentTxs) == 0 {
		return nil
	}

	if local, ok := c.Client.(*LocalClient); ok {
		if err := local.putTransactions(); err != nil {
			return fmt.Errorf("can't persist transactions: %w", err)
		}
	}

	err := AwaitTx(cmd, c.Client, c.SentTxs)
	c.SentTxs = c.SentTxs[:0]

	return err
}