// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package builder

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"time"

	mapset "github.com/deckarep/golang-set/v2"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/dbservices"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/rawdb"
	"github.com/erigontech/erigon/db/state/execctx"
	"github.com/erigontech/erigon/execution/builder/buildercfg"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/exec"
	"github.com/erigontech/erigon/execution/protocol/misc"
	"github.com/erigontech/erigon/execution/protocol/rules"
	"github.com/erigontech/erigon/execution/stagedsync"
	"github.com/erigontech/erigon/execution/state"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/types/accounts"
)

type BuilderState struct {
	BuilderConfig   *buildercfg.BuilderConfig
	PendingResultCh chan *types.Block
	BuilderResultCh chan *types.BlockWithReceipts
	BuiltBlock      *exec.AssembledBlock
}

func NewBuilderState(cfg *buildercfg.BuilderConfig) BuilderState {
	return BuilderState{
		BuilderConfig:   cfg,
		PendingResultCh: make(chan *types.Block, 1),
		BuilderResultCh: make(chan *types.BlockWithReceipts, 1),
		BuiltBlock:      &exec.AssembledBlock{},
	}
}

type BuilderCreateBlockCfg struct {
	builder                BuilderState
	chainConfig            *chain.Config
	engine                 rules.Engine
	blockBuilderParameters *Parameters
	blockReader            dbservices.FullBlockReader
}

func StageBuilderCreateBlockCfg(
	builder BuilderState,
	chainConfig *chain.Config,
	engine rules.Engine,
	blockBuilderParameters *Parameters,
	blockReader dbservices.FullBlockReader,
) BuilderCreateBlockCfg {
	return BuilderCreateBlockCfg{
		builder:                builder,
		chainConfig:            chainConfig,
		engine:                 engine,
		blockBuilderParameters: blockBuilderParameters,
		blockReader:            blockReader,
	}
}

// createBlock constructs the block header and selects uncles.
// TODO:
// - resubmitAdjustCh - variable is not implemented
func createBlock(ctx context.Context, sd *execctx.SharedDomains, tx kv.TemporalTx, executionAt uint64, cfg BuilderCreateBlockCfg, logger log.Logger) (err error) {
	const logPrefix = "BuilderCreateBlock"

	current := cfg.builder.BuiltBlock
	*current = exec.AssembledBlock{}    // always start with a clean state
	var txPoolLocals []accounts.Address //txPoolV2 has no concept of local addresses (yet?)
	coinbase := accounts.InternAddress(cfg.builder.BuilderConfig.Etherbase)

	const (
		// staleThreshold is the maximum depth of the acceptable stale block.
		staleThreshold = 7
	)

	parent := rawdb.ReadHeaderByNumber(tx, executionAt)
	if parent == nil { // todo: how to return error and don't stop Erigon?
		return fmt.Errorf("empty block %d", executionAt)
	}

	if cfg.blockBuilderParameters != nil && cfg.blockBuilderParameters.ParentHash != parent.Hash() {
		return fmt.Errorf("wrong head block: %x (current) vs %x (requested)", parent.Hash(), cfg.blockBuilderParameters.ParentHash)
	}

	if cfg.builder.BuilderConfig.Etherbase == (common.Address{}) {
		if cfg.blockBuilderParameters == nil {
			return errors.New("refusing to build without etherbase")
		}
		// If we do not have an etherbase, let's use the suggested one
		coinbase = accounts.InternAddress(cfg.blockBuilderParameters.SuggestedFeeRecipient)
	}

	blockNum := executionAt + 1

	localUncles, remoteUncles, err := readNonCanonicalHeaders(tx, blockNum, cfg.engine, coinbase, txPoolLocals)
	if err != nil {
		return err
	}
	chain := stagedsync.ChainReader{Cfg: cfg.chainConfig, Db: tx, BlockReader: cfg.blockReader, Logger: logger}
	var GetBlocksFromHash = func(hash common.Hash, n int) (blocks []*types.Block) {
		number, _ := cfg.blockReader.HeaderNumber(context.Background(), tx, hash)
		if number == nil {
			return nil
		}
		for range n {
			block, _, _ := cfg.blockReader.BlockWithSenders(context.Background(), tx, hash, *number)
			if block == nil {
				break
			}
			blocks = append(blocks, block)
			hash = block.ParentHash()
			*number--
		}
		return
	}

	// re-written miner/worker.go:commitNewWork
	var timestamp uint64
	if cfg.blockBuilderParameters == nil {
		timestamp = uint64(time.Now().Unix())
		if parent.Time >= timestamp {
			timestamp = parent.Time + 1
		}
	} else {
		// If we are on proof-of-stake timestamp should be already set for us
		timestamp = cfg.blockBuilderParameters.Timestamp
	}

	type envT struct {
		signer    *types.Signer
		ancestors mapset.Set[common.Hash] // ancestor set (used for checking uncle parent validity)
		family    mapset.Set[common.Hash] // family set (used for checking uncle invalidity)
		uncles    mapset.Set[common.Hash] // uncle set
	}
	env := &envT{
		signer:    types.MakeSigner(cfg.chainConfig, blockNum, timestamp),
		ancestors: mapset.NewSet[common.Hash](),
		family:    mapset.NewSet[common.Hash](),
		uncles:    mapset.NewSet[common.Hash](),
	}

	targetGasLimit := cfg.builder.BuilderConfig.GasLimit
	if cfg.blockBuilderParameters != nil && cfg.blockBuilderParameters.TargetGasLimit != nil {
		targetGasLimit = cfg.blockBuilderParameters.TargetGasLimit
	}
	header := MakeEmptyHeader(parent, cfg.chainConfig, timestamp, targetGasLimit)
	if err := misc.VerifyGaslimit(parent.GasLimit, header.GasLimit); err != nil {
		logger.Warn("Failed to verify gas limit given by the validator, defaulting to parent gas limit", "err", err)
		header.GasLimit = parent.GasLimit
	}

	header.Coinbase = coinbase.Value()
	header.Extra = cfg.builder.BuilderConfig.ExtraData
	if cfg.blockBuilderParameters != nil && cfg.blockBuilderParameters.ExtraData != nil {
		header.Extra = cfg.blockBuilderParameters.ExtraData
	}

	logger.Info(fmt.Sprintf("[%s] Start building", logPrefix), "block", executionAt+1, "baseFee", header.BaseFee, "gasLimit", header.GasLimit)
	ibs := state.New(state.NewReaderV3(sd.AsGetter(tx)))
	defer ibs.Release(false)

	if err = cfg.engine.Prepare(chain, header, ibs); err != nil {
		logger.Error("Failed to prepare header for building",
			"err", err,
			"headerNumber", header.Number.Uint64(),
			"headerRoot", header.Root.String(),
			"headerParentHash", header.ParentHash.String(),
			"parentNumber", parent.Number.Uint64(),
			"parentHash", parent.Hash().String(),
			"stack", dbg.Stack())
		return err
	}

	if cfg.blockBuilderParameters != nil {
		header.MixDigest = cfg.blockBuilderParameters.PrevRandao
		header.ParentBeaconBlockRoot = cfg.blockBuilderParameters.ParentBeaconBlockRoot
		header.SlotNumber = cfg.blockBuilderParameters.SlotNumber

		current.ParentHeaderTime = parent.Time
		current.Header = header
		current.Uncles = nil
		current.Withdrawals = cfg.blockBuilderParameters.Withdrawals

		return nil
	}

	// If we are care about TheDAO hard-fork check whether to override the extra-data or not
	if daoBlock := cfg.chainConfig.DAOForkBlock; daoBlock != nil {
		// Check whether the block is among the fork extra-override range
		if header.Number.Uint64() >= *daoBlock && header.Number.Uint64() < *daoBlock+misc.DAOForkExtraRange {
			header.Extra = bytes.Clone(misc.DAOForkBlockExtra)
		}
	}

	// analog of miner.Worker.updateSnapshot
	var makeUncles = func(proposedUncles mapset.Set[common.Hash]) []*types.Header {
		var uncles []*types.Header
		proposedUncles.Each(func(hash common.Hash) bool {
			uncle, exist := localUncles[hash]
			if !exist {
				uncle, exist = remoteUncles[hash]
			}
			if !exist {
				return false
			}
			uncles = append(uncles, uncle)
			return false
		})
		return uncles
	}

	// when 08 is processed ancestors contain 07 (quick block)
	for _, ancestor := range GetBlocksFromHash(parent.Hash(), 7) {
		for _, uncle := range ancestor.Uncles() {
			env.family.Add(uncle.Hash())
		}
		env.family.Add(ancestor.Hash())
		env.ancestors.Add(ancestor.Hash())
	}
	commitUncle := func(env *envT, uncle *types.Header) error {
		hash := uncle.Hash()
		if env.uncles.Contains(hash) {
			return errors.New("uncle not unique")
		}
		if parent.Hash() == uncle.ParentHash {
			return errors.New("uncle is sibling")
		}
		if !env.ancestors.Contains(uncle.ParentHash) {
			return errors.New("uncle's parent unknown")
		}
		if env.family.Contains(hash) {
			return errors.New("uncle already included")
		}
		env.uncles.Add(uncle.Hash())
		return nil
	}
	// Accumulate the miningUncles for the env block
	// Prefer to locally generated uncle
	uncles := make([]*types.Header, 0, 2)
	for _, blocks := range []map[common.Hash]*types.Header{localUncles, remoteUncles} {
		// Clean up stale uncle blocks first
		for hash, uncle := range blocks {
			if uncle.Number.Uint64()+staleThreshold <= header.Number.Uint64() {
				delete(blocks, hash)
			}
		}
		for hash, uncle := range blocks {
			if len(uncles) == 2 {
				break
			}
			if err = commitUncle(env, uncle); err != nil {
				logger.Trace("Possible uncle rejected", "hash", hash, "reason", err)
			} else {
				logger.Trace("Committing new uncle to block", "hash", hash)
				uncles = append(uncles, uncle)
			}
		}
	}

	current.Header = header
	current.Uncles = makeUncles(env.uncles)
	current.Withdrawals = nil
	return nil
}

func readNonCanonicalHeaders(tx kv.Tx, blockNum uint64, engine rules.Engine, coinbase accounts.Address, txPoolLocals []accounts.Address) (localUncles, remoteUncles map[common.Hash]*types.Header, err error) {
	localUncles, remoteUncles = map[common.Hash]*types.Header{}, map[common.Hash]*types.Header{}
	nonCanonicalBlocks, err := rawdb.ReadHeadersByNumber(tx, blockNum)
	if err != nil {
		return
	}
	for _, u := range nonCanonicalBlocks {
		if IsLocalBlock(engine, coinbase, txPoolLocals, u) {
			localUncles[u.Hash()] = u
		} else {
			remoteUncles[u.Hash()] = u
		}

	}
	return
}
