package utils

import (
	"context"
	"math/big"
	"strings"

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

	backendabi "bridge-history-api/abi"
	"bridge-history-api/orm"
)

// CachedParsedTxCalldata store parsed batch infos
type CachedParsedTxCalldata struct {
	CallDataIndex uint64
	BatchIndices  []uint64
	StartBlocks   []uint64
	EndBlocks     []uint64
}

// ParseBackendL1EventLogs parses L1 watched events
func ParseBackendL1EventLogs(logs []types.Log) ([]*orm.CrossMsg, []*orm.RelayedMsg, error) {
	// Need use contract abi to parse event Log
	// Can only be tested after we have our contracts set up

	var l1CrossMsg []*orm.CrossMsg
	var relayedMsgs []*orm.RelayedMsg
	var msgHash string
	for _, vlog := range logs {
		switch vlog.Topics[0] {
		case backendabi.L1DepositETHSig:
			event := backendabi.DepositETH{}
			err := UnpackLog(backendabi.L1ETHGatewayABI, &event, "DepositETH", vlog)
			if err != nil {
				log.Warn("Failed to unpack DepositETH event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:     vlog.BlockNumber,
				Sender:     event.From.String(),
				Target:     event.To.String(),
				Amount:     event.Amount.String(),
				Asset:      int(orm.ETH),
				Layer1Hash: vlog.TxHash.Hex(),
				MsgType:    int(orm.Layer1Msg),
				MsgHash:    msgHash,
			})
		case backendabi.L1DepositERC20Sig:
			event := backendabi.ERC20MessageEvent{}
			err := UnpackLog(backendabi.L1StandardERC20GatewayABI, &event, "DepositERC20", vlog)
			if err != nil {
				log.Warn("Failed to unpack DepositERC20 event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Amount:      event.Amount.String(),
				Asset:       int(orm.ERC20),
				Layer1Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				MsgType:     int(orm.Layer1Msg),
				MsgHash:     msgHash,
			})
		case backendabi.L1DepositERC721Sig:
			event := backendabi.ERC721MessageEvent{}
			err := UnpackLog(backendabi.L1ERC721GatewayABI, &event, "DepositERC721", vlog)
			if err != nil {
				log.Warn("Failed to unpack DepositERC721 event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC721),
				Layer1Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				TokenIDs:    event.TokenID.String(),
				MsgType:     int(orm.Layer1Msg),
				MsgHash:     msgHash,
			})
		case backendabi.L1DepositERC1155Sig:
			event := backendabi.ERC1155MessageEvent{}
			err := UnpackLog(backendabi.L1ERC1155GatewayABI, &event, "DepositERC1155", vlog)
			if err != nil {
				log.Warn("Failed to unpack DepositERC1155 event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC1155),
				Layer1Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				TokenIDs:    event.TokenID.String(),
				Amount:      event.Amount.String(),
				MsgType:     int(orm.Layer1Msg),
				MsgHash:     msgHash,
			})
		case backendabi.L1SentMessageEventSignature:
			event := backendabi.L1SentMessageEvent{}
			err := UnpackLog(backendabi.L1ScrollMessengerABI, &event, "SentMessage", vlog)
			if err != nil {
				log.Warn("Failed to unpack SentMessage event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			// since every deposit event will emit after a sent event, so can use this msg_hash as next withdraw event's msg_hash
			msgHash = ComputeMessageHash(event.Sender, event.Target, event.Value, event.MessageNonce, event.Message).Hex()
		case backendabi.L1BatchDepositERC721Sig:
			event := backendabi.BatchERC721MessageEvent{}
			err := UnpackLog(backendabi.L1ERC721GatewayABI, &event, "BatchDepositERC721", vlog)
			if err != nil {
				log.Warn("Failed to unpack BatchDepositERC721 event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC721),
				Layer1Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				TokenIDs:    convertBigIntArrayToString(event.TokenIDs),
				MsgType:     int(orm.Layer1Msg),
				MsgHash:     msgHash,
			})
		case backendabi.L1BatchDepositERC1155Sig:
			event := backendabi.BatchERC1155MessageEvent{}
			err := UnpackLog(backendabi.L1ERC1155GatewayABI, &event, "BatchDepositERC1155", vlog)
			if err != nil {
				log.Warn("Failed to unpack BatchDepositERC1155 event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			l1CrossMsg = append(l1CrossMsg, &orm.CrossMsg{
				Height:       vlog.BlockNumber,
				Sender:       event.From.String(),
				Target:       event.To.String(),
				Asset:        int(orm.ERC1155),
				Layer1Hash:   vlog.TxHash.Hex(),
				Layer1Token:  event.L1Token.Hex(),
				Layer2Token:  event.L2Token.Hex(),
				TokenIDs:     convertBigIntArrayToString(event.TokenIDs),
				TokenAmounts: convertBigIntArrayToString(event.TokenAmounts),
				MsgType:      int(orm.Layer1Msg),
				MsgHash:      msgHash,
			})
		case backendabi.L1RelayedMessageEventSignature:
			event := backendabi.L1RelayedMessageEvent{}
			err := UnpackLog(backendabi.L1ScrollMessengerABI, &event, "RelayedMessage", vlog)
			if err != nil {
				log.Warn("Failed to unpack RelayedMessage event", "err", err)
				return l1CrossMsg, relayedMsgs, err
			}
			relayedMsgs = append(relayedMsgs, &orm.RelayedMsg{
				MsgHash:    event.MessageHash.String(),
				Height:     vlog.BlockNumber,
				Layer1Hash: vlog.TxHash.Hex(),
			})

		}

	}
	return l1CrossMsg, relayedMsgs, nil
}

// ParseBackendL2EventLogs parses L2 watched events
func ParseBackendL2EventLogs(logs []types.Log) ([]*orm.CrossMsg, []*orm.RelayedMsg, []*orm.L2SentMsg, error) {
	// Need use contract abi to parse event Log
	// Can only be tested after we have our contracts set up

	var l2CrossMsg []*orm.CrossMsg
	// this is use to confirm finalized l1 msg
	var relayedMsgs []*orm.RelayedMsg
	var l2SentMsgs []*orm.L2SentMsg
	for _, vlog := range logs {
		switch vlog.Topics[0] {
		case backendabi.L2WithdrawETHSig:
			event := backendabi.DepositETH{}
			err := UnpackLog(backendabi.L2ETHGatewayABI, &event, "WithdrawETH", vlog)
			if err != nil {
				log.Warn("Failed to unpack WithdrawETH event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:     vlog.BlockNumber,
				Sender:     event.From.String(),
				Target:     event.To.String(),
				Amount:     event.Amount.String(),
				Asset:      int(orm.ETH),
				Layer2Hash: vlog.TxHash.Hex(),
				MsgType:    int(orm.Layer2Msg),
				MsgHash:    l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2WithdrawERC20Sig:
			event := backendabi.ERC20MessageEvent{}
			err := UnpackLog(backendabi.L2StandardERC20GatewayABI, &event, "WithdrawERC20", vlog)
			if err != nil {
				log.Warn("Failed to unpack WithdrawERC20 event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Amount:      event.Amount.String(),
				Asset:       int(orm.ERC20),
				Layer2Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				MsgType:     int(orm.Layer2Msg),
				MsgHash:     l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2WithdrawERC721Sig:
			event := backendabi.ERC721MessageEvent{}
			err := UnpackLog(backendabi.L2ERC721GatewayABI, &event, "WithdrawERC721", vlog)
			if err != nil {
				log.Warn("Failed to unpack WithdrawERC721 event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC721),
				Layer2Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				TokenIDs:    event.TokenID.String(),
				MsgType:     int(orm.Layer2Msg),
				MsgHash:     l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2WithdrawERC1155Sig:
			event := backendabi.ERC1155MessageEvent{}
			err := UnpackLog(backendabi.L2ERC1155GatewayABI, &event, "WithdrawERC1155", vlog)
			if err != nil {
				log.Warn("Failed to unpack WithdrawERC1155 event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC1155),
				Layer2Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				TokenIDs:    event.TokenID.String(),
				Amount:      event.Amount.String(),
				MsgType:     int(orm.Layer2Msg),
				MsgHash:     l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2BatchWithdrawERC721Sig:
			event := backendabi.BatchERC721MessageEvent{}
			err := UnpackLog(backendabi.L2ERC721GatewayABI, &event, "BatchWithdrawERC721", vlog)
			if err != nil {
				log.Warn("Failed to unpack BatchWithdrawERC721 event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:      vlog.BlockNumber,
				Sender:      event.From.String(),
				Target:      event.To.String(),
				Asset:       int(orm.ERC721),
				Layer1Hash:  vlog.TxHash.Hex(),
				Layer1Token: event.L1Token.Hex(),
				Layer2Token: event.L2Token.Hex(),
				MsgType:     int(orm.Layer2Msg),
				TokenIDs:    convertBigIntArrayToString(event.TokenIDs),
				MsgHash:     l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2BatchWithdrawERC1155Sig:
			event := backendabi.BatchERC1155MessageEvent{}
			err := UnpackLog(backendabi.L2ERC1155GatewayABI, &event, "BatchWithdrawERC1155", vlog)
			if err != nil {
				log.Warn("Failed to unpack BatchWithdrawERC1155 event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			l2SentMsgs[len(l2SentMsgs)-1].OriginalSender = event.From.Hex()
			l2CrossMsg = append(l2CrossMsg, &orm.CrossMsg{
				Height:       vlog.BlockNumber,
				Sender:       event.From.String(),
				Target:       event.To.String(),
				Asset:        int(orm.ERC1155),
				Layer1Hash:   vlog.TxHash.Hex(),
				Layer1Token:  event.L1Token.Hex(),
				Layer2Token:  event.L2Token.Hex(),
				MsgType:      int(orm.Layer2Msg),
				TokenIDs:     convertBigIntArrayToString(event.TokenIDs),
				TokenAmounts: convertBigIntArrayToString(event.TokenAmounts),
				MsgHash:      l2SentMsgs[len(l2SentMsgs)-1].MsgHash,
			})
		case backendabi.L2SentMessageEventSignature:
			event := backendabi.L2SentMessageEvent{}
			err := UnpackLog(backendabi.L2ScrollMessengerABI, &event, "SentMessage", vlog)
			if err != nil {
				log.Warn("Failed to unpack SentMessage event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			// since every withdraw event will emit after a sent event, so can use this msg_hash as next withdraw event's msg_hash
			msgHash := ComputeMessageHash(event.Sender, event.Target, event.Value, event.MessageNonce, event.Message)
			l2SentMsgs = append(l2SentMsgs,
				&orm.L2SentMsg{
					Sender:  event.Sender.Hex(),
					TxHash:  vlog.TxHash.Hex(),
					Target:  event.Target.Hex(),
					Value:   event.Value.String(),
					MsgHash: msgHash.Hex(),
					Height:  vlog.BlockNumber,
					Nonce:   event.MessageNonce.Uint64(),
					MsgData: hexutil.Encode(event.Message),
				})
		case backendabi.L2RelayedMessageEventSignature:
			event := backendabi.L2RelayedMessageEvent{}
			err := UnpackLog(backendabi.L2ScrollMessengerABI, &event, "RelayedMessage", vlog)
			if err != nil {
				log.Warn("Failed to unpack RelayedMessage event", "err", err)
				return l2CrossMsg, relayedMsgs, l2SentMsgs, err
			}
			relayedMsgs = append(relayedMsgs, &orm.RelayedMsg{
				MsgHash:    event.MessageHash.String(),
				Height:     vlog.BlockNumber,
				Layer2Hash: vlog.TxHash.Hex(),
			})

		}
	}
	return l2CrossMsg, relayedMsgs, l2SentMsgs, nil
}

// ParseBatchInfoFromScrollChain parses ScrollChain events
func ParseBatchInfoFromScrollChain(ctx context.Context, client *ethclient.Client, logs []types.Log) ([]*orm.RollupBatch, error) {
	var rollupBatches []*orm.RollupBatch
	for _, vlog := range logs {
		switch vlog.Topics[0] {
		case backendabi.L1CommitBatchEventSignature:
			event := backendabi.L1CommitBatchEvent{}
			err := UnpackLog(backendabi.ScrollChainABI, &event, "CommitBatch", vlog)
			if err != nil {
				log.Warn("Failed to unpack CommitBatch event", "err", err)
				return rollupBatches, err
			}
			commitTx, isPending, err := client.TransactionByHash(ctx, vlog.TxHash)
			if err != nil || isPending {
				log.Warn("Failed to get commit Batch tx receipt or the tx is still pending", "err", err)
				return rollupBatches, err
			}
			index, startBlock, endBlock, err := GetBatchRangeFromCalldataV2(commitTx.Data())
			if err != nil {
				log.Warn("Failed to get batch range from calldata", "hash", commitTx.Hash().Hex(), "height", vlog.BlockNumber)
				return rollupBatches, err
			}
			rollupBatches = append(rollupBatches, &orm.RollupBatch{
				CommitHeight:     vlog.BlockNumber,
				BatchIndex:       index,
				BatchHash:        event.BatchHash.Hex(),
				StartBlockNumber: startBlock,
				EndBlockNumber:   endBlock,
			})

		default:
			continue
		}
	}
	return rollupBatches, nil
}

func convertBigIntArrayToString(array []*big.Int) string {
	stringArray := make([]string, len(array))
	for i, num := range array {
		stringArray[i] = num.String()
	}

	result := strings.Join(stringArray, ", ")
	return result
}
