package monitor

import (
	"context"
	"fmt"
	"log"
	"math/big"
	"strings"
	"time"

	ethereum "github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

type backendIntf interface {
	FetchFinedNodes(chan node)
	NodeSet() []node
	BalanceFromAddress(*ethclient.Client, common.Address) *big.Int
}

// BlockchainBackend is the blockchain backend object for fetching blockchain info.
type BlockchainBackend struct {
	networkID int
}

// NodeSet returns the registered nodes.
func (b *BlockchainBackend) NodeSet() []node {
	nc := NetworkConfigMap[int64(b.networkID)]
	conn, err := ethclient.Dial(nc.HTTPEndpoint)
	if err != nil {
		log.Println("EthClient Dial failed: ", err)
		return []node{}
	}
	g, err := NewGovernance(GovAddress, conn)
	if err != nil {
		panic(err)
	}
	txOps := &bind.CallOpts{}
	nodeLen, err := g.NodesLength(txOps)
	if err != nil {
		panic(err)
	}
	minStake, err := g.MinStake(txOps)
	if err != nil {
		panic(err)
	}
	nodes := []node{}
	for i := uint64(0); i < nodeLen.Uint64(); i++ {
		n, err := g.Nodes(txOps, big.NewInt(int64(i)))
		if n.Staked.Cmp(minStake) < 0 {
			continue
		}
		if err != nil {
			panic(err)
		}
		publicKey, err := crypto.UnmarshalPubkey(n.PublicKey)
		if err != nil {
			log.Println("Unmarshal publicKey fail: ", err)
			continue
		}
		address := crypto.PubkeyToAddress(*publicKey)
		nodes = append(nodes, node{
			owner:          n.Owner,
			email:          n.Email,
			fined:          n.Fined,
			name:           n.Name,
			publicKey:      n.PublicKey,
			nodeKeyAddress: address,
		})
	}
	return nodes
}

// BalanceFromAddress return address's balance.
func (b *BlockchainBackend) BalanceFromAddress(conn *ethclient.Client,
	address common.Address) *big.Int {
	balance, err := conn.BalanceAt(context.Background(),
		address, nil)
	if err != nil {
		log.Println("Get balance fail: ", err)
		return big.NewInt(0)
	}
	return balance
}

func (b *BlockchainBackend) subscribe(
	logs chan types.Log,
	headers chan *types.Header,
) (ethereum.Subscription, *ethclient.Client, error) {

	nc := NetworkConfigMap[int64(b.networkID)]
	conn, err := ethclient.Dial(nc.WSEndpoint)
	if err != nil {
		log.Println("EthClient Dial failed: ", err)
		return nil, conn, err
	}
	abiObject, err := abi.JSON(strings.NewReader(GovernanceABI))
	query := ethereum.FilterQuery{
		Addresses: []common.Address{GovAddress},
		Topics: [][]common.Hash{
			[]common.Hash{
				abiObject.Events["Fined"].Id(),
			},
		},
	}
	sub, err := conn.SubscribeFilterLogs(context.Background(), query, logs)
	if err != nil {
		log.Println("Subscribe logs failed: ", err)
		return nil, conn, err
	}

	// Subscribe chain head to keep ws from timeout
	_, err = conn.SubscribeNewHead(context.Background(), headers)
	if err != nil {
		log.Println("Subscribe chain head failed: ", err)
	}
	return sub, conn, nil
}

// FetchFinedNodes fetches nodes' information from blockchain.
func (b *BlockchainBackend) FetchFinedNodes(nodeChan chan node) {
	logs := make(chan types.Log)
	headers := make(chan *types.Header)
	sub, conn, err := b.subscribe(logs, headers)
	if err != nil {
		panic(err)
	}
	for {
		select {
		case err := <-sub.Err():
			log.Println("sub.Err()", err)
			// retry
			for err != nil {
				sub, conn, err = b.subscribe(logs, headers)
				time.Sleep(30000)
			}
		case vLog := <-logs:
			nodeAddress := common.BytesToAddress(vLog.Topics[1].Bytes())
			log.Println("Notify node: ", nodeAddress.Hex())
			nodeChan <- b.getNodeByAddress(nodeAddress, conn)
		}
	}
}

func (b *BlockchainBackend) getNodeByAddress(
	nodeAddress common.Address, conn *ethclient.Client) node {

	g, err := NewGovernance(GovAddress, conn)
	if err != nil {
		panic(err)
	}
	txOps := &bind.CallOpts{}
	offset, err := g.NodesOffsetByAddress(txOps, nodeAddress)
	if err != nil {
		panic(err)
	}
	n, err := g.Nodes(txOps, offset)
	if err != nil {
		panic(err)
	}
	return node{
		owner: n.Owner,
		email: n.Email,
		fined: n.Fined,
		name:  n.Name,
	}
}

func printNodes(nodes []node) {
	for i := range nodes {
		fmt.Println("Owner", nodes[i].owner.Hex())
		fmt.Println("Email", nodes[i].email)
		fmt.Println("Fined", nodes[i].fined.Uint64())
	}
}

// NewBlockchainBackend is the blockchain backend constructor.
func NewBlockchainBackend(networkID int) *BlockchainBackend {
	return &BlockchainBackend{networkID: networkID}
}