package event

import (
	"context"
	"fmt"
	"math/big"
	"net/url"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/eth"
	"github.com/ethereum/go-ethereum/log"
	"github.com/ethereum/go-ethereum/params"

	"github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/encoding"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/manifest"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/bindings/metadata"
	anchorTxConstructor "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/anchor_tx_constructor"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/chain_syncer/beaconsync"
	blocksInserter "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/chain_syncer/event/blocks_inserter"
	derivation "github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/chain_syncer/event/derivation"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/driver/state"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/internal/metrics"
	eventIterator "github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/chain_iterator/event_iterator"
	"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/rpc"
)

// Syncer responsible for letting the L2 execution engine catching up with protocol's latest
// pending block through deriving L1 calldata.
type Syncer struct {
	ctx             context.Context
	rpc             *rpc.Client
	state           *state.State
	progressTracker *beaconsync.SyncProgressTracker // Sync progress tracker
	blocksInserter  blocksInserter.Inserter         // Blocks inserter

	lastInsertedProposalID *big.Int
	reorgDetectedFlag      bool

	// Derivation source fetcher
	derivationSourceFetcher *derivation.DerivationSourceFetcher
}

// NewSyncer creates a new syncer instance.
func NewSyncer(
	ctx context.Context,
	client *rpc.Client,
	state *state.State,
	progressTracker *beaconsync.SyncProgressTracker,
	blobServerEndpoint *url.URL,
	latestSeenProposalCh chan *encoding.LastSeenProposal,
) (*Syncer, error) {
	constructor, err := anchorTxConstructor.New(client)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize anchor constructor: %w", err)
	}

	blobDataSource := rpc.NewBlobDataSource(ctx, client, blobServerEndpoint)

	return &Syncer{
		ctx:             ctx,
		rpc:             client,
		state:           state,
		progressTracker: progressTracker,
		blocksInserter: blocksInserter.NewBlocksInserter(
			client,
			progressTracker,
			constructor,
			latestSeenProposalCh,
		),
		derivationSourceFetcher: derivation.NewDerivationSourceFetcher(client, blobDataSource),
	}, nil
}

// ProcessL1Blocks fetches all `Inbox.Proposed` events between given L1 block heights,
// and then tries inserting them into L2 execution engine's blockchain.
func (s *Syncer) ProcessL1Blocks(ctx context.Context) error {
	for {
		if err := s.processL1Blocks(ctx); err != nil {
			return fmt.Errorf("failed to process L1 blocks: %w", err)
		}

		// If the L1 chain has been reorged, we process the new L1 blocks again with
		// the new L1Current cursor.
		if s.reorgDetectedFlag {
			s.reorgDetectedFlag = false
			continue
		}

		return nil
	}
}

// processL1Blocks is the inner method which responsible for processing
// all new L1 blocks.
func (s *Syncer) processL1Blocks(ctx context.Context) error {
	var (
		l1End          = s.state.GetL1Head()
		startL1Current = s.state.GetL1Current()
	)
	// If there is a L1 reorg, sometimes this will happen.
	if startL1Current.Number.Uint64() >= l1End.Number.Uint64() && startL1Current.Hash() != l1End.Hash() {
		newL1Current, err := s.rpc.L1.HeaderByNumber(ctx, new(big.Int).Sub(l1End.Number, common.Big1))
		if err != nil {
			return fmt.Errorf("failed to fetch L1 header during reorg detection: %w", err)
		}

		log.Info(
			"Reorg detected",
			"oldL1CurrentHeight", startL1Current.Number,
			"oldL1CurrentHash", startL1Current.Hash(),
			"newL1CurrentHeight", newL1Current.Number,
			"newL1CurrentHash", newL1Current.Hash(),
			"l1Head", l1End.Number,
		)

		s.state.SetL1Current(newL1Current)
		s.lastInsertedProposalID = nil
	}

	iter, err := eventIterator.NewProposalIterator(ctx, &eventIterator.ProposalIteratorConfig{
		RpcClient:       s.rpc,
		StartHeight:     s.state.GetL1Current().Number,
		EndHeight:       l1End.Number,
		OnProposalEvent: s.onProposal,
	})
	if err != nil {
		return fmt.Errorf("failed to create event iterator: %w", err)
	}

	if err := iter.Iter(); err != nil {
		return fmt.Errorf("failed to iterate through events: %w", err)
	}

	// If there is a L1 reorg, we don't update the L1Current cursor.
	if !s.reorgDetectedFlag {
		s.state.SetL1Current(l1End)
		metrics.DriverL1CurrentHeightGauge.Set(float64(s.state.GetL1Current().Number.Uint64()))
	}

	return nil
}

// onProposal is the proposal-event callback which is responsible for
// inserting proposal blocks one by one into the L2 execution engine.
func (s *Syncer) onProposal(
	ctx context.Context,
	meta metadata.TaikoProposalMetaData,
	endIter eventIterator.EndProposalEventIterFunc,
) error {
	return s.processProposal(ctx, meta, endIter)
}

// processProposal processes a proposal event, and tries inserting
// the proposal's blocks into the L2 execution engine.
func (s *Syncer) processProposal(
	ctx context.Context,
	metadata metadata.TaikoProposalMetaData,
	endIter eventIterator.EndProposalEventIterFunc,
) error {
	var (
		meta   = metadata.Shasta()
		parent *types.Block
		err    error
	)

	// We simply ignore the genesis L2 block's `Proposed` event.
	if meta.GetEventData().Id.Cmp(common.Big0) == 0 {
		// Reset the lastInsertedProposalID when processing the genesis proposal.
		s.lastInsertedProposalID = common.Big0
		log.Debug("Ignore genesis proposal event", "proposalID", meta.GetEventData().Id)
		return nil
	}

	// If we are not inserting a block whose parent block is the latest verified block in protocol,
	// and the node hasn't just finished the P2P sync, we check if the L1 chain has been reorged.
	if !s.progressTracker.Triggered() {
		reorgCheckResult, err := s.checkReorg(ctx, meta.GetEventData().Id)
		if err != nil {
			return err
		}

		if reorgCheckResult.IsReorged {
			log.Info(
				"Reset L1Current cursor due to L1 reorg",
				"l1CurrentHeightOld", s.state.GetL1Current().Number,
				"l1CurrentHashOld", s.state.GetL1Current().Hash(),
				"l1CurrentHeightNew", reorgCheckResult.L1CurrentToReset.Number,
				"l1CurrentHashNew", reorgCheckResult.L1CurrentToReset.Hash(),
				"lastInsertedProposalIDOld", s.lastInsertedProposalID,
				"lastInsertedProposalIDNew", reorgCheckResult.LastHandledProposalIDToReset,
			)
			s.state.SetL1Current(reorgCheckResult.L1CurrentToReset)
			s.lastInsertedProposalID = reorgCheckResult.LastHandledProposalIDToReset
			s.reorgDetectedFlag = true
			endIter()

			return nil
		}
	}

	// Ignore those already inserted proposals.
	if s.lastInsertedProposalID != nil && meta.GetEventData().Id.Cmp(s.lastInsertedProposalID) <= 0 {
		log.Debug(
			"Skip already inserted proposal",
			"proposalID", meta.GetEventData().Id,
			"lastInsertedProposalID", s.lastInsertedProposalID,
		)
		return nil
	}

	log.Info(
		"New Proposed event",
		"proposalID", meta.GetEventData().Id,
		"proposer", meta.GetEventData().Proposer,
		"derivationSources", len(meta.GetEventData().Sources),
		"l1Height", meta.GetRawBlockHeight(),
		"l1Hash", meta.GetRawBlockHash(),
	)

	// If the event's timestamp is in the future, we wait until the timestamp is reached, should
	// only happen when testing.
	if meta.GetTimestamp() > uint64(time.Now().Unix()) {
		log.Warn(
			"Future L2 block, waiting",
			"L2BlockTimestamp", meta.GetTimestamp(),
			"now", time.Now().Unix(),
		)
		time.Sleep(time.Until(time.Unix(int64(meta.GetTimestamp()), 0)))
	}

	if meta.GetEventData().Id.Cmp(common.Big1) == 0 {
		// For the first proposal, its parent block is the genesis block.
		log.Info(
			"First proposal, fetch genesis block as parent",
			"proposalID", meta.GetEventData().Id,
			"proposer", meta.GetEventData().Proposer,
		)
		if parent, err = s.rpc.L2.BlockByNumber(ctx, common.Big0); err != nil {
			return fmt.Errorf("failed to fetch genesis block: %w", err)
		}
	} else {
		// For other proposals, fetch the parent block by getting the last block in the previous proposal.
		blockID, err := s.rpc.L2Engine.LastBlockIDByBatchID(ctx, new(big.Int).Sub(meta.GetEventData().Id, common.Big1))
		if err != nil {
			return fmt.Errorf("failed to fetch last L1 origin for proposal: %w", err)
		}
		if blockID == nil {
			return fmt.Errorf(
				"no last L1 origin found for proposal %s",
				new(big.Int).Sub(meta.GetEventData().Id, common.Big1),
			)
		}

		if parent, err = s.rpc.L2.BlockByNumber(ctx, blockID.ToInt()); err != nil {
			return err
		}
	}

	// Prefetch all derivation source payloads.
	sourcePayloads := make([]*derivation.DerivationSourcePayload, len(meta.GetEventData().Sources))
	// Fetch all derivation source payloads.
	for i := range meta.GetEventData().Sources {
		p, err := s.derivationSourceFetcher.Fetch(ctx, meta, i)
		if err != nil {
			return fmt.Errorf("failed to fetch derivation payload for index %d: %w", i, err)
		}
		sourcePayloads[i] = p
	}

	for derivationIdx := range meta.GetEventData().Sources {
		log.Info(
			"Processing derivation source",
			"proposalID", meta.GetEventData().Id,
			"proposer", meta.GetEventData().Proposer,
			"index", derivationIdx,
			"l1Height", meta.GetRawBlockHeight(),
			"l1Hash", meta.GetRawBlockHash(),
		)
		// Reuse the prefetched derivation payload.
		sourcePayload := sourcePayloads[derivationIdx]
		if sourcePayload == nil {
			return fmt.Errorf("missing derivation payload for index %d", derivationIdx)
		}
		sourcePayload.ParentBlock = parent
		isForcedInclusion := meta.GetEventData().Sources[derivationIdx].IsForcedInclusion

		log.Info(
			"Parent block info for derivation payload",
			"proposalID", meta.GetEventData().Id,
			"blocks", len(sourcePayload.BlockPayloads),
			"parentBlockID", sourcePayload.ParentBlock.Number(),
			"parentHash", sourcePayload.ParentBlock.Hash(),
			"parentGasLimit", sourcePayload.ParentBlock.GasLimit(),
			"parentTimestamp", sourcePayload.ParentBlock.Time(),
		)

		var lastAnchorBlockNumber uint64
		if s.rpc.L2.ChainID.Cmp(params.TaikoMainnetNetworkID) == 0 &&
			meta.GetEventData().Id.Uint64() <= manifest.MainnetAnchorCheckSkipProposalOffset {
			if _, lastAnchorBlockNumber, _, err = s.rpc.GetSyncedL1SnippetFromAnchor(
				sourcePayload.ParentBlock.Transactions()[0],
			); err != nil {
				return err
			}
		} else {
			latestBlockState, err := s.rpc.GetAnchorState(
				&bind.CallOpts{BlockHash: sourcePayload.ParentBlock.Hash(), Context: ctx},
			)
			if err != nil {
				return err
			}

			lastAnchorBlockNumber = latestBlockState.AnchorBlockNumber.Uint64()
			if meta.GetEventData().Id.Cmp(common.Big1) == 0 && sourcePayload.ParentBlock.Number().Cmp(common.Big0) != 0 {
				if _, lastAnchorBlockNumber, _, err = s.rpc.GetSyncedL1SnippetFromAnchor(
					sourcePayload.ParentBlock.Transactions()[0],
				); err != nil {
					return err
				}
			}
		}

		// If the derivation source is forced inclusion, we apply inherited metadata first.
		if isForcedInclusion {
			derivation.ApplyInheritedMetadata(
				sourcePayload,
				meta.GetEventData(),
				meta.GetTimestamp(),
				lastAnchorBlockNumber,
				s.rpc.L2.ChainID,
			)
		}

		// If the derivation source payload's metadata is invalid, we replace it with default metadata.
		if !derivation.ValidateMetadata(
			s.rpc,
			sourcePayload,
			meta.GetEventData(),
			meta.GetTimestamp(),
			meta.GetRawBlockHeight().Uint64()-1,
			lastAnchorBlockNumber,
			isForcedInclusion,
		) {
			sourcePayload.Default = true
			sourcePayload.BlockPayloads = []*derivation.BlockPayload{
				{BlockManifest: manifest.BlockManifest{Transactions: types.Transactions{}}},
			}
			derivation.ApplyInheritedMetadata(
				sourcePayload,
				meta.GetEventData(),
				meta.GetTimestamp(),
				lastAnchorBlockNumber,
				s.rpc.L2.ChainID,
			)
			log.Info(
				"Use default derivation payload",
				"proposalID", meta.GetEventData().Id,
				"proposer", meta.GetEventData().Proposer,
				"anchorBlockNumber", lastAnchorBlockNumber,
			)
		}

		// Insert new blocks to L2 EE's chain.
		lastInsertedBlockID, err := s.blocksInserter.InsertBlocksWithManifest(
			ctx,
			metadata,
			sourcePayload,
			endIter,
		)
		if err != nil {
			return fmt.Errorf("failed to insert proposal blocks: %w", err)
		}
		if parent, err = s.rpc.WaitL2Block(ctx, lastInsertedBlockID); err != nil {
			log.Warn("Failed to fetch the new parent block", "error", err)
			return err
		}
	}
	metrics.DriverL1CurrentHeightGauge.Set(float64(meta.GetRawBlockHeight().Uint64()))
	s.lastInsertedProposalID = meta.GetEventData().Id

	if s.progressTracker.Triggered() {
		s.progressTracker.ClearMeta()
	}
	return nil
}

// checkLastVerifiedBlockMismatch checks if there is a mismatch between protocol's last verified block hash and
// the corresponding L2 EE block hash.
func (s *Syncer) checkLastVerifiedBlockMismatch(ctx context.Context) (*rpc.ReorgCheckResult, error) {
	reorgCheckResult := new(rpc.ReorgCheckResult)

	coreState, err := s.rpc.GetCoreState(&bind.CallOpts{Context: ctx})
	if err != nil {
		return nil, fmt.Errorf("failed to fetch core state: %w", err)
	}

	// If there is no finalized proposal yet, we skip the check.
	if coreState.LastFinalizedProposalId.Cmp(common.Big0) == 0 {
		return reorgCheckResult, nil
	}

	lastBlockInBatch, err := s.rpc.L2Engine.LastL1OriginByBatchID(ctx, coreState.LastFinalizedProposalId)
	if err != nil &&
		err.Error() != ethereum.NotFound.Error() &&
		err.Error() != eth.ErrProposalLastBlockUncertain.Error() {
		return nil, fmt.Errorf("failed to fetch last block in batch: %w", err)
	}
	// If the current L2 chain is behind of the last verified block, or the hash matches, return directly.
	if lastBlockInBatch == nil || lastBlockInBatch.L2BlockHash == coreState.LastFinalizedBlockHash {
		return reorgCheckResult, nil
	}

	log.Info(
		"Verified block mismatch",
		"currentHeightToCheck", lastBlockInBatch.BlockID,
		"chainBlockHash", lastBlockInBatch.L2BlockHash,
		"transitionBlockHash", common.Hash(coreState.LastFinalizedBlockHash),
	)

	// For Shasta, we simply reset to genesis if there is a mismatch.
	reorgCheckResult.IsReorged = true
	if reorgCheckResult.L1CurrentToReset, err = s.rpc.L1.HeaderByNumber(ctx, common.Big0); err != nil {
		return nil, fmt.Errorf("failed to fetch L1 header by number: %w", err)
	}
	reorgCheckResult.LastHandledProposalIDToReset = common.Big0
	return reorgCheckResult, nil
}

// checkReorg checks whether the L1 chain has been reorged, and resets the L1Current cursor if necessary.
func (s *Syncer) checkReorg(
	ctx context.Context,
	proposalID *big.Int,
) (*rpc.ReorgCheckResult, error) {
	// 1. Check if the verified blocks in L2 EE have been reorged.
	reorgCheckResult, err := s.checkLastVerifiedBlockMismatch(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to check if the verified blocks in L2 EE have been reorged: %w", err)
	}

	// 2. Proposal 1 has no prior proposal to validate against L1 origin data.
	// Skipping the proposalID-1 reorg check here avoids treating normal genesis state
	// as a synthetic reorg back to genesis.
	if proposalID.Cmp(common.Big1) <= 0 {
		return reorgCheckResult, nil
	}

	// 3. If the verified blocks check is passed, we check the unverified blocks.
	if reorgCheckResult == nil || !reorgCheckResult.IsReorged {
		if reorgCheckResult, err = s.rpc.CheckL1Reorg(ctx, new(big.Int).Sub(proposalID, common.Big1)); err != nil {
			return nil, fmt.Errorf("failed to check whether L1 chain has been reorged: %w", err)
		}
	}

	return reorgCheckResult, nil
}

// BlocksInserter returns the blocks inserter.
func (s *Syncer) BlocksInserter() *blocksInserter.Shasta {
	return s.blocksInserter.(*blocksInserter.Shasta)
}
