package engine

import (
	"context"
	"errors"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/log"
	"github.com/exchain/go-exchain/exchain"
	"github.com/exchain/go-exchain/exchain/chaindb"
	nebulav1 "github.com/exchain/go-exchain/exchain/protocol/gen/go/nebula/v1"
	"github.com/exchain/go-exchain/exchain/wrapper"
	"github.com/exchain/go-exchain/op-node/p2p"
	"github.com/exchain/go-exchain/op-node/rollup"
	"github.com/exchain/go-exchain/op-node/rollup/derive"
	"github.com/exchain/go-exchain/op-node/rollup/driver"
	"github.com/exchain/go-exchain/op-node/rollup/sync"
	"github.com/exchain/go-exchain/op-service/eth"
	lru "github.com/hashicorp/golang-lru"
	"github.com/holiman/uint256"
	"math/big"
)

// ExChainAPI wrapper all api for rollup.
type ExChainAPI struct {
	rollup *rollup.Config
	chain  chaindb.ChainDB
	engine exchain.Engine
	cached *lru.Cache
	Log    log.Logger
}

func (e *ExChainAPI) BlockRefByNumber(ctx context.Context, num uint64) (eth.BlockRef, error) {
	//TODO implement me
	panic("implement me")
}

func (e *ExChainAPI) FetchReceipts(ctx context.Context, blockHash common.Hash) (eth.BlockInfo, types.Receipts, error) {
	return nil, nil, errors.New("FetchReceipts is not implemented in exchain currently")
}

func (e *ExChainAPI) GetWDTRoot(ctx context.Context, height uint64) (common.Hash, error) {
	tree, err := e.chain.GetWDT(height)
	if err != nil {
		return common.Hash{}, err
	}
	return tree.Root(), nil
}

func (e *ExChainAPI) WithdrawalProof(ctx context.Context, txHash common.Hash) (*eth.WithdrawalProof, error) {
	var res = &eth.WithdrawalProof{}
	tx, _ := e.chain.GetTransaction(txHash)
	if tx == nil {
		return nil, errors.New("transaction not found")
	}

	wtx := tx.GetWithdrawTx()
	if wtx == nil {
		return nil, errors.New("transaction is not withdrawal tx")
	}

	receipt := e.chain.GetReceipt(txHash)
	if receipt == nil {
		return nil, errors.New("not found tx receipt")
	}
	tree, err := e.chain.GetWDT(receipt.BlockHeight)
	if err != nil {
		// generate wdt for block failed.
		return nil, err
	}

	item := wrapper.NewTxWrapper(tx).WithdrawalHash()
	proof, err := tree.Proof(item.Bytes())
	if err != nil {
		return nil, errors.New(fmt.Sprintf("failed to get proof (%s)", err.Error()))
	}
	res.Proof = make([]string, len(proof))
	for i, p := range proof {
		res.Proof[i] = p
	}
	res.Value = new(big.Int).SetBytes(wtx.Amount)
	res.User = common.BytesToAddress(wtx.User)
	res.Coin = wtx.Coin

	oo, err := e.OutputV0AtBlock(ctx, receipt.BlockHeight)
	if err != nil {
		e.Log.Error("failed to get output v0 at block", "block", receipt.BlockHeight, "error", err)
		return nil, err
	}
	res.Output = *oo

	//e.Log.Info("WithdrawalProof response", "res", res)

	return res, nil
}

func (e *ExChainAPI) WithdrawalTxs(ctx context.Context, blockNum uint64) ([]common.Hash, error) {
	block := e.chain.GetBlock(uint256.NewInt(blockNum))
	if block == nil {
		return nil, errors.New("block not found in exchain")
	}
	wblk := wrapper.NewBlkWrapper(block)
	txHashes := make([]common.Hash, 0)
	for _, tx := range wblk.Transactions() {
		wtx := wrapper.NewTxWrapper(tx)
		if tx.TxType == nebulav1.TxType_WithdrawTx {
			txHashes = append(txHashes, wtx.Hash())
		}
	}

	return txHashes, nil
}

func (e *ExChainAPI) OutputV0AtBlock(ctx context.Context, number uint64) (*eth.OutputV0, error) {
	blk := e.chain.GetBlock(uint256.NewInt(number))
	if blk == nil {
		return nil, errors.New("block not found in exchain")
	}
	wblk := wrapper.NewBlkWrapper(blk)
	root := eth.Bytes32{}

	wdtroot, err := e.GetWDTRoot(ctx, number)
	if err != nil {
		e.Log.Error("failed to get wdt root", "err", err)
		return nil, err
	}
	storageRoot := eth.Bytes32(wdtroot[:])
	copy(root[:], wblk.Header().AppRoot)

	return &eth.OutputV0{
		BlockHash:                wblk.Hash(),
		StateRoot:                root,
		MessagePasserStorageRoot: storageRoot,
	}, nil
}

func (e *ExChainAPI) GetPayload(id eth.PayloadInfo) (*eth.ExecutionPayloadEnvelope, error) {
	if v, exist := e.cached.Get(id.ID); exist {
		return v.(*eth.ExecutionPayloadEnvelope), nil
	}
	return nil, errors.New("not found")
}

func (e *ExChainAPI) ChainID(ctx context.Context) (*big.Int, error) {
	id, err := e.chain.ChainId()
	if err != nil {
		return nil, err
	}
	return new(big.Int).SetUint64(id.Uint64()), nil
}

func (e *ExChainAPI) NewPayload(params exchain.PayloadParams) (exchain.ExecutionResult, eth.PayloadInfo, error) {
	result, err := e.engine.NewPayload(params)
	if err != nil {
		return exchain.ExecutionResult{}, eth.PayloadInfo{}, err
	}
	if err = e.chain.SaveBlockData(result.Payload, result.Receipts); err != nil {
		return exchain.ExecutionResult{}, eth.PayloadInfo{}, err
	}
	hash := wrapper.NewBlkWrapper(result.Payload).Hash()
	info := eth.PayloadInfo{ID: eth.PayloadID(hash[len(hash)-8:]), Timestamp: uint64(params.Timestamp)}
	e.cached.Add(info.ID, &eth.ExecutionPayloadEnvelope{
		ExecutionPayload:      eth.NewExecutePayload(result.Payload),
		ParentBeaconBlockRoot: nil,
	})

	return result, info, nil
}

func (e *ExChainAPI) ProcessPayload(block *nebulav1.Block) error {
	if blk := e.chain.GetBlock(uint256.NewInt(block.Header.Height)); blk != nil {
		// block has been processed
		return nil
	}
	result, err := e.engine.ProcessPayload(block)
	if err != nil {
		return err
	}
	return e.chain.SaveBlockData(result.Payload, result.Receipts)
}

func (e *ExChainAPI) PayloadByNumber(ctx context.Context, u uint64) (*eth.ExecutionPayloadEnvelope, error) {
	block := e.chain.GetBlock(uint256.NewInt(u))
	if block == nil {
		return &eth.ExecutionPayloadEnvelope{}, errors.New("not found block")
	}
	return &eth.ExecutionPayloadEnvelope{
		ExecutionPayload:      eth.NewExecutePayload(block),
		ParentBeaconBlockRoot: nil, // todo: vicotor fill this field
	}, nil
}

func (e *ExChainAPI) L2BlockRefByLabel(ctx context.Context, label eth.BlockLabel) (eth.L2BlockRef, error) {
	switch label {
	case eth.Safe, eth.Unsafe, eth.Finalized:
		blk, err := e.chain.GetBlockByLabel(chaindb.ExChainBlockLatest)
		if err != nil {
			return eth.L2BlockRef{}, err
		}
		ref, err := derive.PayloadToBlockRef(e.rollup, eth.NewExecutePayload(blk))
		return ref, err
	default:
		return eth.L2BlockRef{}, errors.New("unsupported label")
	}
}

func (e *ExChainAPI) L2BlockRefByHash(ctx context.Context, l2Hash common.Hash) (eth.L2BlockRef, error) {
	block := e.chain.BlockByHash(l2Hash)
	if block == nil {
		return eth.L2BlockRef{}, errors.New("not found block")
	}
	return derive.PayloadToBlockRef(e.rollup, eth.NewExecutePayload(block))
}

func (e *ExChainAPI) L2BlockRefByNumber(ctx context.Context, num uint64) (eth.L2BlockRef, error) {
	block := e.chain.GetBlock(uint256.NewInt(num))
	if block == nil {
		return eth.L2BlockRef{}, errors.New("not found block")
	}
	return derive.PayloadToBlockRef(e.rollup, eth.NewExecutePayload(block))
}

func (e *ExChainAPI) SystemConfigByL2Hash(ctx context.Context, hash common.Hash) (eth.SystemConfig, error) {
	block := e.chain.BlockByHash(hash)
	if block == nil {
		return eth.SystemConfig{}, errors.New("not found block")
	}
	return derive.PayloadToSystemConfig(e.rollup, eth.NewExecutePayload(block))
}

func (e *ExChainAPI) Close() { // do nothing
}

var (
	_ p2p.L2Chain    = (*ExChainAPI)(nil)
	_ sync.L2Chain   = (*ExChainAPI)(nil)
	_ driver.L2Chain = (*ExChainAPI)(nil)
)

func NewEngineAPI(cfg *rollup.Config, database chaindb.ChainDB, engine exchain.Engine, l log.Logger) *ExChainAPI {
	cache, _ := lru.New(100)
	return &ExChainAPI{
		rollup: cfg,
		chain:  database,
		engine: engine,
		cached: cache,
		Log:    l,
	}
}
