package client

import (
	"sync"

	"github.com/nspcc-dev/neo-go/pkg/rpc/client"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"go.uber.org/zap"
)

type multiClient struct {
	cfg cfg

	account *wallet.Account

	sharedNotary *notary // notary config needed for single client construction

	endpoints  []string
	clientsMtx sync.RWMutex
	// lastSuccess is an index in endpoints array relating to a last
	// used endpoint.
	lastSuccess int
	clients     map[string]*Client
}

// createForAddress creates single Client instance using provided endpoint.
func (x *multiClient) createForAddress(addr string) (*Client, error) {
	cli, err := client.New(x.cfg.ctx, addr, client.Options{
		DialTimeout:     x.cfg.dialTimeout,
		MaxConnsPerHost: x.cfg.maxConnPerHost,
	})
	if err != nil {
		return nil, err
	}

	err = cli.Init() // magic number is set there based on RPC node answer
	if err != nil {
		return nil, err
	}

	var c *Client

	x.clientsMtx.Lock()
	// While creating 2 clients in parallel is ok, we don't want to
	// use a client missing from `x.clients` map as it can lead
	// to unexpected bugs.
	if x.clients[addr] == nil {
		sCli := blankSingleClient(cli, x.account, &x.cfg)
		sCli.notary = x.sharedNotary

		c = &Client{
			cache:        newClientCache(),
			singleClient: sCli,
		}
		x.clients[addr] = c
	} else {
		c = x.clients[addr]
	}
	x.clientsMtx.Unlock()

	return c, nil
}

// iterateClients executes f on each client until nil error is returned.
// When nil error is returned, lastSuccess field is updated.
// The iteration order is non-deterministic and shouldn't be relied upon.
func (x *multiClient) iterateClients(f func(*Client) error) error {
	var (
		firstErr error
		err      error
	)

	x.clientsMtx.RLock()
	start := x.lastSuccess
	x.clientsMtx.RUnlock()

	for i := 0; i < len(x.endpoints); i++ {
		index := (start + i) % len(x.endpoints)

		x.clientsMtx.RLock()
		c, cached := x.clients[x.endpoints[index]]
		x.clientsMtx.RUnlock()
		if !cached {
			c, err = x.createForAddress(x.endpoints[index])
		}

		if !cached && err != nil {
			x.cfg.logger.Error("could not open morph client connection",
				zap.String("endpoint", x.endpoints[index]),
				zap.String("err", err.Error()),
			)
		} else {
			err = f(c)
		}

		if err == nil {
			if i != 0 {
				x.clientsMtx.Lock()
				x.lastSuccess = index
				x.clientsMtx.Unlock()
			}
			return nil
		}

		// we dont need to continue the process after the logical error was encountered
		if errNeoFS := unwrapNeoFSError(err); errNeoFS != nil {
			return errNeoFS
		}

		// set first error once
		if firstErr == nil {
			firstErr = err
		}
	}

	return firstErr
}