package service

import (
	"context"
	"errors"
	"github.com/ethereum/go-ethereum/ethclient"
	"github.com/fbsobreira/gotron-sdk/pkg/client"
	logging "github.com/ipfs/go-log/v2"
	"google.golang.org/grpc"
	"regexp"
	"strconv"
	"time"
)

var log = logging.Logger("server")

type Node struct {
	ethRpcAddrs    []string
	ethBlockNumber uint64
	ethClient      *ethclient.Client

	tronRpcAddrs    []string
	tronBlockNumber int64
	tronClient      *client.GrpcClient
}

func NewNode(ethRpcAddrs []string, tronRpcAddrs []string) (*Node, error) {
	ethC, ethBlockNumber, err := getBestEthNode(ethRpcAddrs)
	if err != nil {
		return nil, err
	}
	tronC, tronBlockNumber, err := getBestTronNode(tronRpcAddrs)
	if err != nil {
		return nil, err
	}

	n := &Node{
		ethRpcAddrs:     ethRpcAddrs,
		ethBlockNumber:  ethBlockNumber,
		ethClient:       ethC,
		tronRpcAddrs:    tronRpcAddrs,
		tronBlockNumber: tronBlockNumber,
		tronClient:      tronC,
	}

	go n.monitor()

	return n, nil
}

func (n *Node) monitor() {
	ticker := time.NewTicker(time.Second * 60)
	for {
		select {
		case <-ticker.C:
			ethBlockNumber := n.keepEthClientConnect()
			tronBlockNumber := n.keepTronClientConnect()
			log.Infow("node monitor", "ethBlockNumber", ethBlockNumber, "tronBlockNumber", tronBlockNumber)
		}
	}
}

func (n *Node) keepEthClientConnect() uint64 {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()
	number, err := n.ethClient.BlockNumber(ctx)
	if err != nil || n.ethBlockNumber > number {
		ethC, ethBlockNumber, err := getBestEthNode(n.ethRpcAddrs)
		if err != nil {
			log.Errorw("keepEthClientConnect: getBestEthNode", "err", err)
			return 0
		}
		n.ethClient.Close()
		n.ethBlockNumber = ethBlockNumber
		n.ethClient = ethC
	}

	return n.ethBlockNumber
}

func (n *Node) keepTronClientConnect() int64 {
	num, err := getTronBlockNumber(n.tronClient)
	if err != nil || n.tronBlockNumber > num {
		tronC, tronBlockNumber, err := getBestTronNode(n.tronRpcAddrs)
		if err != nil {
			log.Errorw("keepTronClientConnect: getBestTronNode", "err", err)
			return 0
		}
		n.tronClient.Stop()
		n.tronBlockNumber = tronBlockNumber
		n.tronClient = tronC
	}

	return n.tronBlockNumber
}

func getBestEthNode(ethRpcAddrs []string) (*ethclient.Client, uint64, error) {
	var cs = make([]*ethclient.Client, 0, len(ethRpcAddrs))
	for _, addr := range ethRpcAddrs {
		c, err := ethclient.Dial(addr)
		if err != nil {
			log.Warnw("getBestEthNode: bad rpc", "rpc", addr)
			continue
		}
		tem := *c
		cs = append(cs, &tem)
	}

	if len(cs) == 0 {
		return nil, 0, errors.New("no active eth node")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	blockNumber := uint64(0)
	bestIndex := -1

	for i, c := range cs {
		b, err := c.BlockNumber(ctx)
		if err != nil {
			log.Warnw("getBestEthNode: bad client", "err", err)
			continue
		}

		if b > blockNumber {
			bestIndex = i
			blockNumber = b
		}
	}

	if bestIndex == -1 {
		return nil, 0, errors.New("no active eth node")
	}

	for i, c := range cs {
		if i == bestIndex {
			continue
		}
		c.Close()
	}

	return cs[bestIndex], blockNumber, nil
}

func getBestTronNode(tronRpcAddrs []string) (*client.GrpcClient, int64, error) {
	var cs = make([]*client.GrpcClient, 0, len(tronRpcAddrs))
	for _, addr := range tronRpcAddrs {
		c := client.NewGrpcClient(addr)
		err := c.Start(grpc.WithInsecure())
		if err != nil {
			log.Warnw("getBestTronNode: bad rpc", "rpc", addr)
			continue
		}

		tem := *c
		cs = append(cs, &tem)
	}

	if len(cs) == 0 {
		return nil, 0, errors.New("no active tron node")
	}

	blockNumber := int64(0)
	bestIndex := -1

	for i, c := range cs {
		num, err := getTronBlockNumber(c)
		if err != nil {
			log.Warnw("getBestTronNode: bad client", "err", err)
			continue
		}

		if num > blockNumber {
			bestIndex = i
			blockNumber = num
		}
	}

	if bestIndex == -1 {
		return nil, 0, errors.New("no active tron node")
	}

	for i, c := range cs {
		if i == bestIndex {
			continue
		}
		c.Stop()
	}

	return cs[bestIndex], blockNumber, nil
}

func getTronBlockNumber(c *client.GrpcClient) (int64, error) {
	b, err := c.GetNodeInfo()
	if err != nil {
		return 0, err
	}

	re := regexp.MustCompile(`Num:(\d+)`)
	match := re.FindStringSubmatch(b.Block)
	if len(match) < 2 {
		return 0, err
	}
	num, err := strconv.ParseInt(match[1], 10, 64)
	if err != nil {
		return 0, err
	}

	return num, nil
}
