package v2

import (
	"fmt"

	"github.com/tendermint/tendermint/p2p"
	tmState "github.com/tendermint/tendermint/state"
	"github.com/tendermint/tendermint/types"
)

// Events generated by the processor:
// block execution failure, event will indicate the peer(s) that caused the error
type pcBlockVerificationFailure struct {
	priorityNormal
	height       int64
	firstPeerID  p2p.NodeID
	secondPeerID p2p.NodeID
}

func (e pcBlockVerificationFailure) String() string {
	return fmt.Sprintf("pcBlockVerificationFailure{%d 1st peer: %v, 2nd peer: %v}",
		e.height, e.firstPeerID, e.secondPeerID)
}

// successful block execution
type pcBlockProcessed struct {
	priorityNormal
	height int64
	peerID p2p.NodeID
}

func (e pcBlockProcessed) String() string {
	return fmt.Sprintf("pcBlockProcessed{%d peer: %v}", e.height, e.peerID)
}

// processor has finished
type pcFinished struct {
	priorityNormal
	blocksSynced int
	tmState      tmState.State
}

func (p pcFinished) Error() string {
	return "finished"
}

type queueItem struct {
	block  *types.Block
	peerID p2p.NodeID
}

type blockQueue map[int64]queueItem

type pcState struct {
	// blocks waiting to be processed
	queue blockQueue

	// draining indicates that the next rProcessBlock event with a queue miss constitutes completion
	draining bool

	// the number of blocks successfully synced by the processor
	blocksSynced int

	// the processorContext which contains the processor dependencies
	context processorContext
}

func (state *pcState) String() string {
	return fmt.Sprintf("height: %d queue length: %d draining: %v blocks synced: %d",
		state.height(), len(state.queue), state.draining, state.blocksSynced)
}

// newPcState returns a pcState initialized with the last verified block enqueued
func newPcState(context processorContext) *pcState {
	return &pcState{
		queue:        blockQueue{},
		draining:     false,
		blocksSynced: 0,
		context:      context,
	}
}

// nextTwo returns the next two unverified blocks
func (state *pcState) nextTwo() (queueItem, queueItem, error) {
	if first, ok := state.queue[state.height()+1]; ok {
		if second, ok := state.queue[state.height()+2]; ok {
			return first, second, nil
		}
	}
	return queueItem{}, queueItem{}, fmt.Errorf("not found")
}

// synced returns true when at most the last verified block remains in the queue
func (state *pcState) synced() bool {
	return len(state.queue) <= 1
}

func (state *pcState) enqueue(peerID p2p.NodeID, block *types.Block, height int64) {
	if item, ok := state.queue[height]; ok {
		panic(fmt.Sprintf(
			"duplicate block %d (%X) enqueued by processor (sent by %v; existing block %X from %v)",
			height, block.Hash(), peerID, item.block.Hash(), item.peerID))
	}

	state.queue[height] = queueItem{block: block, peerID: peerID}
}

func (state *pcState) height() int64 {
	return state.context.tmState().LastBlockHeight
}

// purgePeer moves all unprocessed blocks from the queue
func (state *pcState) purgePeer(peerID p2p.NodeID) {
	// what if height is less than state.height?
	for height, item := range state.queue {
		if item.peerID == peerID {
			delete(state.queue, height)
		}
	}
}

// handle processes FSM events
func (state *pcState) handle(event Event) (Event, error) {
	switch event := event.(type) {
	case bcResetState:
		state.context.setState(event.state)
		return noOp, nil

	case scFinishedEv:
		if state.synced() {
			return pcFinished{tmState: state.context.tmState(), blocksSynced: state.blocksSynced}, nil
		}
		state.draining = true
		return noOp, nil

	case scPeerError:
		state.purgePeer(event.peerID)
		return noOp, nil

	case scBlockReceived:
		if event.block == nil {
			return noOp, nil
		}

		// enqueue block if height is higher than state height, else ignore it
		if event.block.Height > state.height() {
			state.enqueue(event.peerID, event.block, event.block.Height)
		}
		return noOp, nil

	case rProcessBlock:
		tmState := state.context.tmState()
		firstItem, secondItem, err := state.nextTwo()
		if err != nil {
			if state.draining {
				return pcFinished{tmState: tmState, blocksSynced: state.blocksSynced}, nil
			}
			return noOp, nil
		}

		var (
			first, second = firstItem.block, secondItem.block
			firstParts    = first.MakePartSet(types.BlockPartSizeBytes)
			firstID       = types.BlockID{Hash: first.Hash(), PartSetHeader: firstParts.Header()}
		)

		// verify if +second+ last commit "confirms" +first+ block
		err = state.context.verifyCommit(tmState.ChainID, firstID, first.Height, second.LastCommit)
		if err != nil {
			state.purgePeer(firstItem.peerID)
			if firstItem.peerID != secondItem.peerID {
				state.purgePeer(secondItem.peerID)
			}
			return pcBlockVerificationFailure{
					height: first.Height, firstPeerID: firstItem.peerID, secondPeerID: secondItem.peerID},
				nil
		}

		state.context.saveBlock(first, firstParts, second.LastCommit)

		if err := state.context.applyBlock(firstID, first); err != nil {
			panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err))
		}

		delete(state.queue, first.Height)
		state.blocksSynced++

		return pcBlockProcessed{height: first.Height, peerID: firstItem.peerID}, nil
	}

	return noOp, nil
}
