package blockchain

import (
	"context"
	"math/big"

	"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"
	"github.com/ethereum/go-ethereum/log"
	"gitlab.com/pactual1/backend/config"
	blockchain "gitlab.com/pactual1/backend/services/blockchain/lib"
)

type service struct {
	networkEndpoint  string
	contractAddress  string
	walletAddress    string
	walletPrivateKey string
	chainID          big.Int
}

func NewService(blockchainConfig config.Blockchain) Service {
	return &service{networkEndpoint: blockchainConfig.NetworkEndpoint, contractAddress: blockchainConfig.ContractAddress, walletAddress: blockchainConfig.WalletAddress, walletPrivateKey: blockchainConfig.WalletPrivateKey}
}

type Service interface {
	CreateContract(ctx context.Context, contractID [32]byte) error
	AddIOTData(ctx context.Context, contractID [32]byte, deviceID [32]byte, timestamp *big.Int, data []byte) error
	RegisterNewDeviceID(ctx context.Context, contractID [32]byte, deviceID [32]byte) error
}

func (s *service) signerFunc(address common.Address, txn *types.Transaction) (*types.Transaction, error) {
	signer := types.NewLondonSigner(&s.chainID)
	privateKey, err := crypto.HexToECDSA(s.walletPrivateKey)
	if err != nil {
		return nil, err
	}
	return types.SignTx(txn, signer, privateKey)
}

func (s *service) getTransactOpts() *bind.TransactOpts {
	return &bind.TransactOpts{
		From:   common.HexToAddress(s.walletAddress),
		Signer: s.signerFunc,
	}
}

func (s *service) getClient(ctx context.Context) (*ethclient.Client, *blockchain.Blockchain, error) {
	// connect to polygon network
	conn, err := ethclient.Dial(s.networkEndpoint)
	if err != nil {
		return nil, nil, err
	}

	// create contract client
	contract, err := blockchain.NewBlockchain(common.HexToAddress(s.contractAddress), conn)
	if err != nil {
		return nil, nil, err
	}

	chainID, err := conn.ChainID(ctx)
	if err != nil {
		return nil, nil, err
	}

	s.chainID = *chainID
	return conn, contract, nil
}

// track transaction to handle failure in go routine
func (s *service) trackTransaction(txn *types.Transaction, conn *ethclient.Client) {
	receipt, err := bind.WaitMined(context.Background(), conn, txn)
	defer conn.Close()
	if err != nil {
		log.Error("blockchain transaction failed with error", "error", err.Error())
	}

	if receipt.Status != types.ReceiptStatusSuccessful {
		log.Error("blockchain transaction not successful")
	}
}

// create contract with blockchain contract ID
func (s *service) CreateContract(ctx context.Context, contractID [32]byte) error {
	conn, contract, err := s.getClient(ctx)
	if err != nil {
		return err
	}

	txn, err := contract.CreateContract(s.getTransactOpts(), contractID)
	if err != nil {
		return err
	}

	go s.trackTransaction(txn, conn)
	return nil
}

// map devices with blockchain contract
func (s *service) RegisterNewDeviceID(ctx context.Context, contractID [32]byte, deviceID [32]byte) error {
	conn, contract, err := s.getClient(ctx)
	if err != nil {
		return err
	}

	txn, err := contract.RegisterNewDeviceId(s.getTransactOpts(), contractID, deviceID)
	if err != nil {
		return err
	}

	go s.trackTransaction(txn, conn)
	return nil
}

// store device data with blockchain contract
func (s *service) AddIOTData(ctx context.Context, contractID [32]byte, deviceID [32]byte, timestamp *big.Int, data []byte) error {
	conn, contract, err := s.getClient(ctx)
	if err != nil {
		return err
	}

	txn, err := contract.AddIOTData(s.getTransactOpts(), contractID, deviceID, timestamp, data)
	if err != nil {
		return err
	}

	go s.trackTransaction(txn, conn)
	return nil
}
