package derive

import (
	"bytes"
	"fmt"
	nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
	"math/big"

	"github.com/hashicorp/go-multierror"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/types"
)

// UserDeposits transforms the L2 block-height and L1 receipts into the transaction inputs for a full L2 block
func UserDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]*types.DepositTx, error) {
	var out []*types.DepositTx
	var result error
	for i, rec := range receipts {
		if rec.Status != types.ReceiptStatusSuccessful {
			continue
		}
		for j, log := range rec.Logs {
			if log.Address == depositContractAddr && len(log.Topics) > 0 && log.Topics[0] == DepositEventABIHash {
				dep, err := UnmarshalDepositLogEvent(log)
				if err != nil {
					result = multierror.Append(result, fmt.Errorf("malformatted L1 deposit log in receipt %d, log %d: %w", i, j, err))
				} else {
					out = append(out, dep)
				}
			}
		}
	}
	return out, result
}

func DeriveDeposits(receipts []*types.Receipt, depositContractAddr common.Address) ([]hexutil.Bytes, error) {
	var result error
	userDeposits, err := UserDeposits(receipts, depositContractAddr)
	if err != nil {
		result = multierror.Append(result, err)
	}
	encodedTxs := make([]hexutil.Bytes, 0, len(userDeposits))
	for i, tx := range userDeposits {
		opaqueTx, err := types.NewTx(tx).MarshalBinary()
		if err != nil {
			result = multierror.Append(result, fmt.Errorf("failed to encode user tx %d", i))
		} else {
			encodedTxs = append(encodedTxs, opaqueTx)
		}
	}
	return encodedTxs, result
}

func DeriveDepositsForExchain(receipts []*types.Receipt, depositContractAddr common.Address) ([]*nebulav1.Transaction_DepositTx, error) {
	var result error
	localCoinName := "USDT"
	userDeposits, err := UserDeposits(receipts, depositContractAddr)
	if err != nil {
		result = multierror.Append(result, err)
	}

	depositTxs := make([]*nebulav1.Transaction_DepositTx, 0)
	for _, tx := range userDeposits {
		var mint *nebulav1.DepositTransaction
		var transfer *nebulav1.DepositTransaction
		if tx.Mint != nil {
			// mint to from.
			mint = &nebulav1.DepositTransaction{
				SourceHash: tx.SourceHash.Bytes(),
				User:       tx.From.Bytes(),
				Coin:       []byte(localCoinName),
				Amount:     tx.Mint.Bytes(),
			}
		}
		if tx.To != nil && bytes.Compare(tx.From.Bytes(), tx.To.Bytes()) != 0 {
			// then do transfer tx
			transfer = &nebulav1.DepositTransaction{
				SourceHash: tx.SourceHash.Bytes(),
				User:       tx.To.Bytes(),
				Coin:       []byte(localCoinName),
				Amount:     tx.Value.Bytes(),
			}
		}
		if mint != nil && transfer != nil {
			actMint := new(big.Int).SetBytes(mint.Amount)
			actTransfer := new(big.Int).SetBytes(transfer.Amount)
			mint.Amount = new(big.Int).Sub(actMint, actTransfer).Bytes()
		}

		if mint != nil {
			depositTxs = append(depositTxs, &nebulav1.Transaction_DepositTx{
				DepositTx: mint,
			})
		}
		if transfer != nil {
			depositTxs = append(depositTxs, &nebulav1.Transaction_DepositTx{
				DepositTx: transfer,
			})
		}
	}

	return depositTxs, result
}
