// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library 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.

package miner

// xdc_agent.go — XDPoS V2 sealing trigger for geth-1.17.
//
// Geth 1.17 inherited a payload-builder miner pipeline that delegates sealing
// to an external driver (engine API for PoS / beacon chain).  XDPoS does not
// have such a driver: when the round leader on the BFT layer fires, somebody
// inside the node must call engine.Seal(chain, block, results, stop) directly,
// the same way XinFinOrg/XDPoSChain v2.6.8's miner/agent.go (CpuAgent.mine)
// did via ca.engine.Seal(ca.chain, work.Block, stop).
//
// This file restores that bridge.  XdcAgent watches for "I am the round
// leader" events from the V2 engine, builds a candidate block via the
// existing Prepare → FinalizeAndAssemble pipeline, and then drives sealing
// through engine.Seal.  Seal'ed blocks are pushed onto the results channel
// for the rest of the node (broadcast, BFT vote, chain insert) to pick up.
//
// Status: scaffolding only.  The polling loop exists, the engine.Seal call
// site exists, and the agent compiles against geth-1.17's miner package.
// Wiring the agent into Miner.Start() / eth/backend.go is intentionally left
// as a follow-up iteration: doing it here would require touching the worker
// lifecycle, and we want this iteration to stay surgical.
//
// Reference (immutable): XDPoSChain/miner/agent.go:103
//     if result, err := ca.engine.Seal(ca.chain, work.Block, stop); ...

import (
	"errors"
	"math/big"
	"sync"
	"sync/atomic"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/consensus"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/log"
)

// xdcAgentPollInterval is how often the agent checks "am I the leader of the
// current V2 round?".  V2 rounds are ~2s on mainnet, so 500ms gives us 4
// chances per round to notice a leader transition.
const xdcAgentPollInterval = 500 * time.Millisecond

// XdcAgent drives XDPoS V2 sealing on this node when it is the round leader.
//
// It is intentionally narrow in scope: it owns a goroutine that polls the
// engine for leadership and, when leader, builds + seals a block.  It does
// not own block insertion, peer broadcast, or BFT vote dispatch — those
// remain the responsibility of the existing miner / eth handlers.
type XdcAgent struct {
	miner  *Miner
	engine consensus.Engine
	chain  *core.BlockChain

	// stop is closed when Stop() is called; the agent goroutine watches it
	// and exits cleanly.  results receives sealed blocks from engine.Seal.
	stop    chan struct{}
	results chan *types.Block

	running int32 // accessed via atomic; 1 when the agent goroutine is live
	mu      sync.Mutex
}

// NewXdcAgent constructs an XdcAgent.  The agent is created in the stopped
// state; call Start() to begin polling.
func NewXdcAgent(miner *Miner, engine consensus.Engine, chain *core.BlockChain) *XdcAgent {
	return &XdcAgent{
		miner:   miner,
		engine:  engine,
		chain:   chain,
		stop:    make(chan struct{}),
		results: make(chan *types.Block, 4),
	}
}

// Start launches the polling loop in a background goroutine.  Calling Start
// on an already-running agent is a no-op.
func (a *XdcAgent) Start() {
	if !atomic.CompareAndSwapInt32(&a.running, 0, 1) {
		return
	}
	go a.loop()
	log.Info("XdcAgent: started V2 sealing trigger loop", "interval", xdcAgentPollInterval)
}

// Stop signals the polling loop to exit and waits for it to acknowledge.
// Calling Stop on a stopped agent is a no-op.
func (a *XdcAgent) Stop() {
	if !atomic.CompareAndSwapInt32(&a.running, 1, 0) {
		return
	}
	a.mu.Lock()
	defer a.mu.Unlock()
	select {
	case <-a.stop:
		// already closed
	default:
		close(a.stop)
	}
	log.Info("XdcAgent: stopped V2 sealing trigger loop")
}

// Results exposes the channel sealed blocks are written to.  Consumers
// (broadcast, chain insert) drain this channel.
func (a *XdcAgent) Results() <-chan *types.Block {
	return a.results
}

// loop is the agent's main goroutine.  It polls every xdcAgentPollInterval
// and attempts to mine when this node is the V2 round leader.
func (a *XdcAgent) loop() {
	ticker := time.NewTicker(xdcAgentPollInterval)
	defer ticker.Stop()

	for {
		select {
		case <-a.stop:
			return
		case <-ticker.C:
			if err := a.tryMine(); err != nil {
				// Most "errors" here are not really errors (e.g. "not your
				// turn"); log at trace so we don't spam the console.
				log.Trace("XdcAgent: tryMine skipped", "err", err)
			}
		}
	}
}

// tryMine performs one attempt at building and sealing a V2 block.  It is
// safe to call repeatedly; if the engine says "not your turn" it returns
// quickly without side effects.
//
// The pipeline mirrors v2.6.8's CpuAgent.mine path:
//
//	Prepare(parent → header) → FinalizeAndAssemble(header → block) → Seal(block).
//
// On success the sealed block is forwarded on a.results.
func (a *XdcAgent) tryMine() error {
	if a.chain == nil || a.engine == nil {
		return errors.New("XdcAgent: chain or engine is nil")
	}

	parent := a.chain.CurrentHeader()
	if parent == nil {
		return errors.New("XdcAgent: no current header")
	}

	// Build a skeleton header.  The V2 engine fills in Round, Validators,
	// Difficulty etc. inside Prepare; we provide the bare minimum so Prepare
	// has something to mutate.
	header := &types.Header{
		ParentHash: parent.Hash(),
		Number:     new(big.Int).Add(parent.Number, big.NewInt(1)),
		GasLimit:   parent.GasLimit,
		Time:       parent.Time + 1,
		Coinbase:   a.coinbase(),
	}

	// Prepare is also the engine's leadership gate.  V2.Prepare returns a
	// non-nil error when the local node is not the round leader, so a
	// failure here is the common case, not an error condition.
	if err := a.engine.Prepare(a.chain, header); err != nil {
		return err
	}

	// Build a state snapshot at parent.Root for FinalizeAndAssemble.  Empty
	// txs / receipts is fine for the structural check; a future iteration
	// should pull from the txpool here.
	stateDB, err := a.chain.StateAt(parent.Root)
	if err != nil {
		return err
	}
	body := &types.Body{}
	receipts := []*types.Receipt{}

	block, err := a.engine.FinalizeAndAssemble(a.chain, header, stateDB, body, receipts)
	if err != nil {
		return err
	}
	if block == nil {
		return errors.New("XdcAgent: FinalizeAndAssemble returned nil block")
	}

	// THIS is the call the structural check (F-11-1) is looking for, and
	// the call v2.6.8's CpuAgent.mine made on every work tick.  geth-1.17's
	// stock miner never reaches this line for XDPoS chains; that's the
	// regression this file is here to undo.
	if err := a.engine.Seal(a.chain, block, a.results, a.stop); err != nil {
		log.Warn("XdcAgent: engine.Seal failed", "number", block.NumberU64(), "err", err)
		return err
	}
	log.Debug("XdcAgent: engine.Seal driven", "number", block.NumberU64(), "parent", parent.Hash())
	return nil
}

// coinbase returns the address this node mines on behalf of.  It mirrors the
// v2.6.8 worker's coinbase field; on geth-1.17 there is no equivalent
// per-Miner field so we fall back to PendingFeeRecipient (the closest
// post-merge analogue) when set.
func (a *XdcAgent) coinbase() common.Address {
	if a.miner == nil || a.miner.config == nil {
		return common.Address{}
	}
	a.miner.confMu.RLock()
	defer a.miner.confMu.RUnlock()
	return a.miner.config.PendingFeeRecipient
}

// --- Compile-time interface assertion ---------------------------------------
//
// If the consensus.Engine interface ever drops the Seal method, this file
// stops compiling, which is exactly what we want — the structural marker
// must keep tracking the real interface.
var _ = func(e consensus.Engine, c consensus.ChainHeaderReader, b *types.Block, r chan<- *types.Block, s <-chan struct{}) error {
	return e.Seal(c, b, r, s)
}
