// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package executor

import (
	"errors"
	"fmt"

	"github.com/luxfi/log"

	"github.com/luxfi/vm/chains/atomic"
	"github.com/luxfi/node/vms/platformvm/block"
	"github.com/luxfi/node/vms/platformvm/metrics"
	"github.com/luxfi/node/vms/platformvm/state"
	"github.com/luxfi/node/vms/platformvm/validators"
)

var (
	_ block.Visitor = (*acceptor)(nil)

	errMissingBlockState = errors.New("missing state of block")
)

// acceptor handles the logic for accepting a block.
// All errors returned by this struct are fatal and should result in the chain
// being shutdown.
type acceptor struct {
	*backend
	metrics    metrics.Metrics
	validators validators.Manager
}

func (a *acceptor) AbortBlock(b *block.AbortBlock) error {
	return a.optionBlock(b, "abort")
}

func (a *acceptor) CommitBlock(b *block.CommitBlock) error {
	return a.optionBlock(b, "commit")
}

func (a *acceptor) ProposalBlock(b *block.ProposalBlock) error {
	a.proposalBlock(b, "proposal")
	return nil
}

func (a *acceptor) StandardBlock(b *block.StandardBlock) error {
	return a.standardBlock(b, "standard")
}

func (a *acceptor) optionBlock(b block.Block, blockType string) error {
	parentID := b.Parent()
	parentState, ok := a.getBlockState(parentID)
	if !ok {
		return fmt.Errorf("%w: %s", state.ErrMissingParentState, parentID)
	}

	blkID := b.ID()
	defer func() {
		// Note: we assume this block's sibling doesn't
		// need the parent's state when it's rejected.
		a.free(parentID)
		a.free(blkID)
	}()

	// Note that the parent must be accepted first.
	if err := a.commonAccept(parentState); err != nil {
		return err
	}

	// For proposal blocks, we need to apply the onDecisionState first
	// before applying the option block's state (commit or abort)
	if parentState.onDecisionState != nil {
		if err := parentState.onDecisionState.Apply(a.state); err != nil {
			return err
		}
	}

	blkState, ok := a.getBlockState(blkID)
	if !ok {
		return fmt.Errorf("%w %s", errMissingBlockState, blkID)
	}

	if err := a.commonAccept(blkState); err != nil {
		return err
	}

	if err := blkState.onAcceptState.Apply(a.state); err != nil {
		return err
	}

	defer a.state.Abort()
	batch, err := a.state.CommitBatch()
	if err != nil {
		return fmt.Errorf(
			"failed to commit VM's database for block %s: %w",
			blkID,
			err,
		)
	}

	// Note that this method writes [batch] to the database.
	// Apply atomic requests via SharedMemory from context
	if a.rt.SharedMemory != nil {
		sharedMemory := a.rt.SharedMemory.(atomic.SharedMemory)
		if err := sharedMemory.Apply(parentState.atomicRequests, batch); err != nil {
			return fmt.Errorf("failed to apply vm's state to shared memory: %w", err)
		}
	} else {
		// If SharedMemory is nil, we must write the batch ourselves
		if err := batch.Write(); err != nil {
			return fmt.Errorf("failed to write batch for block %s: %w", blkID, err)
		}
	}

	if onAcceptFunc := parentState.onAcceptFunc; onAcceptFunc != nil {
		onAcceptFunc()
	}

	log.Trace(
		"accepted block",
		log.String("blockType", blockType),
		log.Stringer("blkID", blkID),
		log.Uint64("height", b.Height()),
		log.Stringer("parentID", parentID),
		log.Stringer("checksum", a.state.Checksum()),
	)

	return nil
}

func (a *acceptor) proposalBlock(b block.Block, blockType string) {
	// Note that:
	//
	// * We don't free the proposal block in this method.
	//   It is freed when its child is accepted.
	//   We need to keep this block's state in memory for its child to use.
	//
	// * We only update the metrics to reflect this block's
	//   acceptance when its child is accepted.
	//
	// * We don't write this block to state here.
	//   That is done when this block's child (a CommitBlock or AbortBlock) is accepted.
	//   We do this so that in the event that the node shuts down, the proposal block
	//   is not written to disk unless its child is.
	//   (The VM's Shutdown method commits the database.)
	//   The chain.Engine requires that the last committed block is a decision block

	blkID := b.ID()
	a.backend.lastAccepted = blkID

	log.Trace(
		"accepted block",
		log.String("blockType", blockType),
		log.Stringer("blkID", blkID),
		log.Uint64("height", b.Height()),
		log.Stringer("parentID", b.Parent()),
		log.Stringer("checksum", a.state.Checksum()),
	)
}

func (a *acceptor) standardBlock(b block.Block, blockType string) error {
	blkID := b.ID()
	defer a.free(blkID)

	blkState, ok := a.getBlockState(blkID)
	if !ok {
		return fmt.Errorf("%w %s", errMissingBlockState, blkID)
	}

	if err := a.commonAccept(blkState); err != nil {
		return err
	}

	// Update the state to reflect the changes made in [onAcceptState].
	if err := blkState.onAcceptState.Apply(a.state); err != nil {
		return err
	}

	defer a.state.Abort()
	batch, err := a.state.CommitBatch()
	if err != nil {
		return fmt.Errorf(
			"failed to commit VM's database for block %s: %w",
			blkID,
			err,
		)
	}

	// Note that this method writes [batch] to the database.
	// Apply atomic requests via SharedMemory from context
	if a.rt.SharedMemory != nil {
		sharedMemory := a.rt.SharedMemory.(atomic.SharedMemory)
		if err := sharedMemory.Apply(blkState.atomicRequests, batch); err != nil {
			return fmt.Errorf("failed to apply vm's state to shared memory: %w", err)
		}
	} else {
		// If SharedMemory is nil, we must write the batch ourselves
		if err := batch.Write(); err != nil {
			return fmt.Errorf("failed to write batch for block %s: %w", blkID, err)
		}
	}

	if onAcceptFunc := blkState.onAcceptFunc; onAcceptFunc != nil {
		onAcceptFunc()
	}

	log.Trace(
		"accepted block",
		log.String("blockType", blockType),
		log.Stringer("blkID", blkID),
		log.Uint64("height", b.Height()),
		log.Stringer("parentID", b.Parent()),
		log.Stringer("checksum", a.state.Checksum()),
	)

	return nil
}

func (a *acceptor) commonAccept(b *blockState) error {
	if b == nil {
		return errMissingBlockState
	}
	blk := b.statelessBlock
	blkID := blk.ID()

	if err := a.metrics.MarkAccepted(b.metrics); err != nil {
		return fmt.Errorf("failed to accept block %s: %w", blkID, err)
	}

	a.backend.lastAccepted = blkID
	a.state.SetLastAccepted(blkID)
	a.state.SetHeight(blk.Height())
	a.state.AddStatelessBlock(blk)
	a.validators.OnAcceptedBlockID(blkID)
	return nil
}
