package client

import (
	"context"
	"sort"
	"time"

	"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
	"github.com/nspcc-dev/neo-go/pkg/core/block"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/neorpc"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"go.uber.org/zap"
)

// Endpoint represents morph endpoint together with its priority.
type Endpoint struct {
	Address  string
	Priority int
}

type endpoints struct {
	curr int
	list []Endpoint
}

func (e *endpoints) init(ee []Endpoint) {
	sort.SliceStable(ee, func(i, j int) bool {
		return ee[i].Priority < ee[j].Priority
	})

	e.curr = 0
	e.list = ee
}

func (c *Client) switchRPC(ctx context.Context) bool {
	c.switchLock.Lock()
	defer c.switchLock.Unlock()

	c.client.Close()

	// Iterate endpoints in the order of decreasing priority.
	for c.endpoints.curr = range c.endpoints.list {
		newEndpoint := c.endpoints.list[c.endpoints.curr].Address
		cli, act, err := c.newCli(ctx, newEndpoint)
		if err != nil {
			c.logger.Warn(logs.ClientCouldNotEstablishConnectionToTheSwitchedRPCNode,
				zap.String("endpoint", newEndpoint),
				zap.Error(err),
			)

			continue
		}

		c.cache.invalidate()

		c.logger.Info(logs.ClientConnectionToTheNewRPCNodeHasBeenEstablished,
			zap.String("endpoint", newEndpoint))

		subs, ok := c.restoreSubscriptions(ctx, cli, newEndpoint, false)
		if !ok {
			// new WS client does not allow
			// restoring subscription, client
			// could not work correctly =>
			// closing connection to RPC node
			// to switch to another one
			cli.Close()
			continue
		}

		c.client = cli
		c.setActor(act)
		c.subsInfo = subs

		if c.cfg.switchInterval != 0 && !c.switchIsActive.Load() &&
			c.endpoints.list[c.endpoints.curr].Priority != c.endpoints.list[0].Priority {
			c.switchIsActive.Store(true)
			go c.switchToMostPrioritized(ctx)
		}

		return true
	}

	return false
}

func (c *Client) notificationLoop(ctx context.Context) {
	var e any
	var ok bool

	for {
		c.switchLock.RLock()
		bChan := c.blockRcv
		nChan := c.notificationRcv
		nrChan := c.notaryReqRcv
		c.switchLock.RUnlock()

		select {
		case <-ctx.Done():
			_ = c.UnsubscribeAll()
			c.close()

			return
		case <-c.closeChan:
			_ = c.UnsubscribeAll()
			c.close()

			return
		case e, ok = <-bChan:
		case e, ok = <-nChan:
		case e, ok = <-nrChan:
		}

		if ok {
			c.routeEvent(ctx, e)
			continue
		}

		if !c.reconnect(ctx) {
			return
		}
	}
}

func (c *Client) routeEvent(ctx context.Context, e any) {
	typedNotification := rpcclient.Notification{Value: e}

	switch e.(type) {
	case *block.Block:
		typedNotification.Type = neorpc.BlockEventID
	case *state.ContainedNotificationEvent:
		typedNotification.Type = neorpc.NotificationEventID
	case *result.NotaryRequestEvent:
		typedNotification.Type = neorpc.NotaryRequestEventID
	}

	select {
	case c.notifications <- typedNotification:
	case <-ctx.Done():
		_ = c.UnsubscribeAll()
		c.close()
	case <-c.closeChan:
		_ = c.UnsubscribeAll()
		c.close()
	}
}

func (c *Client) reconnect(ctx context.Context) bool {
	if closeErr := c.client.GetError(); closeErr != nil {
		c.logger.Warn(logs.ClientSwitchingToTheNextRPCNode,
			zap.String("reason", closeErr.Error()),
		)
	} else {
		// neo-go client was closed by calling `Close`
		// method, that happens only when a client has
		// switched to the more prioritized RPC
		return true
	}

	if !c.switchRPC(ctx) {
		c.logger.Error(logs.ClientCouldNotEstablishConnectionToAnyRPCNode)

		// could not connect to all endpoints =>
		// switch client to inactive mode
		c.inactiveMode()

		return false
	}

	// TODO(@carpawell): call here some callback retrieved in constructor
	// of the client to allow checking chain state since during switch
	// process some notification could be lost

	return true
}

func (c *Client) switchToMostPrioritized(ctx context.Context) {
	t := time.NewTicker(c.cfg.switchInterval)
	defer t.Stop()
	defer c.switchIsActive.Store(false)

mainLoop:
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			c.switchLock.RLock()

			endpointsCopy := make([]Endpoint, len(c.endpoints.list))
			copy(endpointsCopy, c.endpoints.list)
			currPriority := c.endpoints.list[c.endpoints.curr].Priority
			highestPriority := c.endpoints.list[0].Priority

			c.switchLock.RUnlock()

			if currPriority == highestPriority {
				// already connected to
				// the most prioritized
				return
			}

			for i, e := range endpointsCopy {
				if currPriority == e.Priority {
					// a switch will not increase the priority
					continue mainLoop
				}

				tryE := e.Address

				cli, act, err := c.newCli(ctx, tryE)
				if err != nil {
					c.logger.Warn(logs.ClientCouldNotCreateClientToTheHigherPriorityNode,
						zap.String("endpoint", tryE),
						zap.Error(err),
					)
					continue
				}

				if subs, ok := c.restoreSubscriptions(ctx, cli, tryE, true); ok {
					c.switchLock.Lock()

					// higher priority node could have been
					// connected in the other goroutine
					if e.Priority >= c.endpoints.list[c.endpoints.curr].Priority {
						cli.Close()
						c.switchLock.Unlock()
						return
					}

					c.client.Close()
					c.cache.invalidate()
					c.client = cli
					c.setActor(act)
					c.subsInfo = subs
					c.endpoints.curr = i

					c.switchLock.Unlock()

					c.logger.Info(logs.ClientSwitchedToTheHigherPriorityRPC,
						zap.String("endpoint", tryE))

					return
				}

				c.logger.Warn(logs.ClientCouldNotRestoreSideChainSubscriptionsUsingNode,
					zap.String("endpoint", tryE),
					zap.Error(err),
				)
			}
		}
	}
}

// close closes notification channel and wrapped WS client.
func (c *Client) close() {
	close(c.notifications)
	c.client.Close()
}