package engine

import (
	"context"
	"errors"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"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"
	log "github.com/sirupsen/logrus"
	"math/big"
)

type ExChainAPI struct {
	rollup *rollup.Config
	chain  chaindb.ChainDB
	engine exchain.Engine
	cached *lru.Cache
}

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) {
	//TODO implement me
	panic("implement me")
}

func (e *ExChainAPI) OutputV0AtBlock(ctx context.Context, blockHash common.Hash) (*eth.OutputV0, error) {
	//TODO implement me
	panic("implement me")
}

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))
		log.WithField("label", label).WithField("ref", ref).Info("L2BlockRefByLabel")
		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) *ExChainAPI {
	cache, _ := lru.New(100)
	return &ExChainAPI{
		rollup: cfg,
		chain:  database,
		engine: engine,
		cached: cache,
	}
}
