package l2

import (
	"fmt"
	"math/big"

	"github.com/ethereum-optimism/optimism/op-program/client/l2/engineapi"
	"github.com/ethereum-optimism/optimism/op-service/eth"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/consensus"
	"github.com/ethereum/go-ethereum/consensus/beacon"
	"github.com/ethereum/go-ethereum/core/rawdb"
	"github.com/ethereum/go-ethereum/core/state"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/core/vm"
	"github.com/ethereum/go-ethereum/ethdb"
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/params"
)

type OracleBackedL2Chain struct {
	log        log.Logger
	oracle     Oracle
	chainCfg   *params.ChainConfig
	engine     consensus.Engine
	oracleHead *types.Header
	head       *types.Header
	safe       *types.Header
	finalized  *types.Header
	vmCfg      vm.Config

	// Block by number cache
	hashByNum            map[uint64]common.Hash
	earliestIndexedBlock *types.Header

	// Inserted blocks
	blocks map[common.Hash]*types.Block
	db     ethdb.KeyValueStore
}

var _ engineapi.EngineBackend = (*OracleBackedL2Chain)(nil)

func NewOracleBackedL2Chain(logger log.Logger, oracle Oracle, chainCfg *params.ChainConfig, l2OutputRoot common.Hash) (*OracleBackedL2Chain, error) {
	output := oracle.OutputByRoot(l2OutputRoot)
	outputV0, ok := output.(*eth.OutputV0)
	if !ok {
		return nil, fmt.Errorf("unsupported L2 output version: %d", output.Version())
	}
	head := oracle.BlockByHash(outputV0.BlockHash)
	logger.Info("Loaded L2 head", "hash", head.Hash(), "number", head.Number())
	return &OracleBackedL2Chain{
		log:      logger,
		oracle:   oracle,
		chainCfg: chainCfg,
		engine:   beacon.New(nil),

		hashByNum: map[uint64]common.Hash{
			head.NumberU64(): head.Hash(),
		},
		earliestIndexedBlock: head.Header(),

		// Treat the agreed starting head as finalized - nothing before it can be disputed
		head:       head.Header(),
		safe:       head.Header(),
		finalized:  head.Header(),
		oracleHead: head.Header(),
		blocks:     make(map[common.Hash]*types.Block),
		db:         NewOracleBackedDB(oracle),
	}, nil
}

func (o *OracleBackedL2Chain) CurrentHeader() *types.Header {
	return o.head
}

func (o *OracleBackedL2Chain) GetHeaderByNumber(n uint64) *types.Header {
	if o.head.Number.Uint64() < n {
		return nil
	}
	hash, ok := o.hashByNum[n]
	if ok {
		return o.GetHeaderByHash(hash)
	}
	// Walk back from current head to the requested block number
	h := o.head
	for h.Number.Uint64() > n {
		h = o.GetHeaderByHash(h.ParentHash)
		o.hashByNum[h.Number.Uint64()] = h.Hash()
	}
	o.earliestIndexedBlock = h
	return h
}

func (o *OracleBackedL2Chain) GetTd(hash common.Hash, number uint64) *big.Int {
	// Difficulty is always 0 post-merge and bedrock starts post-merge so total difficulty also always 0
	return common.Big0
}

func (o *OracleBackedL2Chain) CurrentSafeBlock() *types.Header {
	return o.safe
}

func (o *OracleBackedL2Chain) CurrentFinalBlock() *types.Header {
	return o.finalized
}

func (o *OracleBackedL2Chain) GetHeaderByHash(hash common.Hash) *types.Header {
	return o.GetBlockByHash(hash).Header()
}

func (o *OracleBackedL2Chain) GetBlockByHash(hash common.Hash) *types.Block {
	// Check inserted blocks
	block, ok := o.blocks[hash]
	if ok {
		return block
	}
	// Retrieve from the oracle
	return o.oracle.BlockByHash(hash)
}

func (o *OracleBackedL2Chain) GetBlock(hash common.Hash, number uint64) *types.Block {
	var block *types.Block
	if o.oracleHead.Number.Uint64() < number {
		// For blocks above the chain head, only consider newly built blocks
		// Avoids requesting an unknown block from the oracle which would panic.
		block = o.blocks[hash]
	} else {
		block = o.GetBlockByHash(hash)
	}
	if block == nil {
		return nil
	}
	if block.NumberU64() != number {
		return nil
	}
	return block
}

func (o *OracleBackedL2Chain) GetHeader(hash common.Hash, u uint64) *types.Header {
	block := o.GetBlock(hash, u)
	return block.Header()
}

func (o *OracleBackedL2Chain) HasBlockAndState(hash common.Hash, number uint64) bool {
	block := o.GetBlock(hash, number)
	return block != nil
}

func (o *OracleBackedL2Chain) GetCanonicalHash(n uint64) common.Hash {
	header := o.GetHeaderByNumber(n)
	if header == nil {
		return common.Hash{}
	}
	return header.Hash()
}

func (o *OracleBackedL2Chain) GetVMConfig() *vm.Config {
	return &o.vmCfg
}

func (o *OracleBackedL2Chain) Config() *params.ChainConfig {
	return o.chainCfg
}

func (o *OracleBackedL2Chain) Engine() consensus.Engine {
	return o.engine
}

func (o *OracleBackedL2Chain) StateAt(root common.Hash) (*state.StateDB, error) {
	return state.New(root, state.NewDatabase(rawdb.NewDatabase(o.db)), nil)
}

func (o *OracleBackedL2Chain) InsertBlockWithoutSetHead(block *types.Block) error {
	processor, err := engineapi.NewBlockProcessorFromHeader(o, block.Header())
	if err != nil {
		return err
	}
	for i, tx := range block.Transactions() {
		err = processor.AddTx(tx)
		if err != nil {
			return fmt.Errorf("invalid transaction (%d): %w", i, err)
		}
	}
	expected, err := processor.Assemble()
	if err != nil {
		return fmt.Errorf("invalid block: %w", err)
	}
	if expected.Hash() != block.Hash() {
		return fmt.Errorf("block root mismatch, expected: %v, actual: %v", expected.Hash(), block.Hash())
	}
	err = processor.Commit()
	if err != nil {
		return fmt.Errorf("commit block: %w", err)
	}
	o.blocks[block.Hash()] = block
	return nil
}

func (o *OracleBackedL2Chain) SetCanonical(head *types.Block) (common.Hash, error) {
	oldHead := o.head
	o.head = head.Header()

	// Remove canonical hashes after the new header
	for n := head.NumberU64() + 1; n <= oldHead.Number.Uint64(); n++ {
		delete(o.hashByNum, n)
	}

	// Add new canonical blocks to the block by number cache
	// Since the original head is added to the block number cache and acts as the finalized block,
	// at some point we must reach the existing canonical chain and can stop updating.
	h := o.head
	for {
		newHash := h.Hash()
		prevHash, ok := o.hashByNum[h.Number.Uint64()]
		if ok && prevHash == newHash {
			// Connected with the existing canonical chain so stop updating
			break
		}
		o.hashByNum[h.Number.Uint64()] = newHash
		h = o.GetHeaderByHash(h.ParentHash)
	}
	return head.Hash(), nil
}

func (o *OracleBackedL2Chain) SetFinalized(header *types.Header) {
	o.finalized = header
}

func (o *OracleBackedL2Chain) SetSafe(header *types.Header) {
	o.safe = header
}
