package connectors

import (
	"context"
	"time"

	ethAbi "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi"

	ethRpc "github.com/ethereum/go-ethereum/rpc"

	ethereum "github.com/ethereum/go-ethereum"
	ethBind "github.com/ethereum/go-ethereum/accounts/abi/bind"
	ethCommon "github.com/ethereum/go-ethereum/common"
	ethTypes "github.com/ethereum/go-ethereum/core/types"
	ethClient "github.com/ethereum/go-ethereum/ethclient"
	ethEvent "github.com/ethereum/go-ethereum/event"

	"go.uber.org/zap"
)

// EthereumConnector implements EVM network query capabilities for go-ethereum based networks and networks supporting
// the standard web3 rpc.
type EthereumConnector struct {
	networkName string
	address     ethCommon.Address
	logger      *zap.Logger
	client      *ethClient.Client
	rawClient   *ethRpc.Client
	filterer    *ethAbi.AbiFilterer
	caller      *ethAbi.AbiCaller
}

func NewEthereumConnector(ctx context.Context, networkName, rawUrl string, address ethCommon.Address, logger *zap.Logger) (*EthereumConnector, error) {
	rawClient, err := ethRpc.DialContext(ctx, rawUrl)
	if err != nil {
		return nil, err
	}

	client := ethClient.NewClient(rawClient)

	filterer, err := ethAbi.NewAbiFilterer(ethCommon.BytesToAddress(address.Bytes()), client)
	if err != nil {
		panic(err)
	}
	caller, err := ethAbi.NewAbiCaller(ethCommon.BytesToAddress(address.Bytes()), client)
	if err != nil {
		panic(err)
	}

	return &EthereumConnector{
		networkName: networkName,
		address:     address,
		logger:      logger.With(zap.String("eth_network", networkName)),
		client:      client,
		filterer:    filterer,
		caller:      caller,
		rawClient:   rawClient,
	}, nil
}

func (e *EthereumConnector) NetworkName() string {
	return e.networkName
}

func (e *EthereumConnector) ContractAddress() ethCommon.Address {
	return e.address
}

func (e *EthereumConnector) GetCurrentGuardianSetIndex(ctx context.Context) (uint32, error) {
	return e.caller.GetCurrentGuardianSetIndex(&ethBind.CallOpts{Context: ctx})
}

func (e *EthereumConnector) GetGuardianSet(ctx context.Context, index uint32) (ethAbi.StructsGuardianSet, error) {
	return e.caller.GetGuardianSet(&ethBind.CallOpts{Context: ctx}, index)
}

func (e *EthereumConnector) WatchLogMessagePublished(ctx context.Context, sink chan<- *ethAbi.AbiLogMessagePublished) (ethEvent.Subscription, error) {
	timeout, cancel := context.WithTimeout(ctx, 15*time.Second)
	defer cancel()
	return e.filterer.WatchLogMessagePublished(&ethBind.WatchOpts{Context: timeout}, sink, nil)
}

func (e *EthereumConnector) TransactionReceipt(ctx context.Context, txHash ethCommon.Hash) (*ethTypes.Receipt, error) {
	return e.client.TransactionReceipt(ctx, txHash)
}

func (e *EthereumConnector) TimeOfBlockByHash(ctx context.Context, hash ethCommon.Hash) (uint64, error) {
	block, err := e.client.BlockByHash(ctx, hash)
	if err != nil {
		return 0, err
	}

	return block.Time(), err
}

func (e *EthereumConnector) ParseLogMessagePublished(log ethTypes.Log) (*ethAbi.AbiLogMessagePublished, error) {
	return e.filterer.ParseLogMessagePublished(log)
}

func (e *EthereumConnector) SubscribeForBlocks(ctx context.Context, sink chan<- *NewBlock) (ethereum.Subscription, error) {
	headSink := make(chan *ethTypes.Header, 2)
	headerSubscription, err := e.client.SubscribeNewHead(ctx, headSink)
	if err != nil {
		return nil, err
	}

	// The purpose of this is to map events from the geth event channel to the new block event channel.
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			case ev := <-headSink:
				if ev == nil {
					e.logger.Error("new header event is nil")
					continue
				}
				if ev.Number == nil {
					e.logger.Error("new header block number is nil")
					continue
				}
				sink <- &NewBlock{
					Number: ev.Number,
					Hash:   ev.Hash(),
				}
			}
		}
	}()

	return headerSubscription, err
}

func (e *EthereumConnector) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error {
	return e.rawClient.CallContext(ctx, result, method, args...)

}

func (e *EthereumConnector) Client() *ethClient.Client {
	return e.client
}
