package valuetransfers

import (
	"time"

	"github.com/iotaledger/hive.go/daemon"
	"github.com/iotaledger/hive.go/events"
	"github.com/iotaledger/hive.go/logger"
	"github.com/iotaledger/hive.go/node"

	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/ledgerstate"
	valuepayload "github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/tangle"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/utxodag"
	"github.com/iotaledger/goshimmer/packages/binary/messagelayer/message"
	messageTangle "github.com/iotaledger/goshimmer/packages/binary/messagelayer/tangle"
	"github.com/iotaledger/goshimmer/packages/database"
	"github.com/iotaledger/goshimmer/packages/shutdown"
	"github.com/iotaledger/goshimmer/packages/vote"
	"github.com/iotaledger/goshimmer/plugins/messagelayer"
)

const (
	// PluginName contains the human readable name of the plugin.
	PluginName = "ValueTransfers"

	// AverageNetworkDelay contains the average time it takes for a network to propagate through gossip.
	AverageNetworkDelay = 6 * time.Second
)

var (
	// App is the "plugin" instance of the value-transfers application.
	App = node.NewPlugin(PluginName, node.Enabled, configure, run)

	// Tangle represents the value tangle that is used to express votes on value transactions.
	Tangle *tangle.Tangle

	// UTXODAG represents the flow of funds that is derived from the value tangle.
	UTXODAG *utxodag.UTXODAG

	// LedgerState represents the ledger state, that keeps track of the liked branches and offers an API to access funds.
	LedgerState *ledgerstate.LedgerState

	// log holds a reference to the logger used by this app.
	log *logger.Logger
)

func configure(_ *node.Plugin) {
	log = logger.NewLogger(PluginName)

	log.Debug("configuring ValueTransfers")

	// create instances
	Tangle = tangle.New(database.GetBadgerInstance())
	UTXODAG = utxodag.New(database.GetBadgerInstance(), Tangle)

	// subscribe to message-layer
	messagelayer.Tangle.Events.MessageSolid.Attach(events.NewClosure(onReceiveMessageFromMessageLayer))

	// setup behavior of package instances
	Tangle.Events.PayloadSolid.Attach(events.NewClosure(UTXODAG.ProcessSolidPayload))
	UTXODAG.Events.TransactionBooked.Attach(events.NewClosure(onTransactionBooked))
	UTXODAG.Events.Fork.Attach(events.NewClosure(onFork))

	configureFPC()
	// TODO: DECIDE WHAT WE SHOULD DO IF FPC FAILS
	// voter.Events().Failed.Attach(events.NewClosure(panic))
	voter.Events().Finalized.Attach(events.NewClosure(func(id string, opinion vote.Opinion) {
		branchID, err := branchmanager.BranchIDFromBase58(id)
		if err != nil {
			log.Error(err)

			return
		}

		switch opinion {
		case vote.Like:
			UTXODAG.BranchManager().SetBranchPreferred(branchID, true)
		case vote.Dislike:
			UTXODAG.BranchManager().SetBranchPreferred(branchID, false)
		}
	}))
}

func run(*node.Plugin) {
	_ = daemon.BackgroundWorker("Tangle", func(shutdownSignal <-chan struct{}) {
		<-shutdownSignal
		Tangle.Shutdown()
		UTXODAG.Shutdown()
	}, shutdown.PriorityTangle)

	runFPC()
}

func onReceiveMessageFromMessageLayer(cachedMessage *message.CachedMessage, cachedMessageMetadata *messageTangle.CachedMessageMetadata) {
	defer cachedMessage.Release()
	defer cachedMessageMetadata.Release()

	solidMessage := cachedMessage.Unwrap()
	if solidMessage == nil {
		// TODO: LOG ERROR?

		return
	}

	messagePayload := solidMessage.Payload()
	if messagePayload.Type() != valuepayload.Type {
		// TODO: LOG ERROR?

		return
	}

	valuePayload, ok := messagePayload.(*valuepayload.Payload)
	if !ok {
		// TODO: LOG ERROR?

		return
	}

	Tangle.AttachPayload(valuePayload)
}

func onTransactionBooked(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *utxodag.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID, previousConsumersForked bool) {
	defer cachedTransaction.Release()
	defer cachedTransactionMetadata.Release()
	defer cachedBranch.Release()

	if len(conflictingInputs) >= 1 {
		// abort if the previous consumers where finalized already
		if !previousConsumersForked {
			return
		}

		branch := cachedBranch.Unwrap()
		if branch == nil {
			log.Error("failed to unpack branch")

			return
		}

		err := voter.Vote(branch.ID().String(), vote.Dislike)
		if err != nil {
			log.Error(err)
		}

		return
	}

	// If the transaction is not conflicting, then we apply the fcob rule (we finalize after 2 network delays).
	// Note: We do not set a liked flag after 1 network delay because that can be derived.
	cachedTransactionMetadata.Retain()
	time.AfterFunc(2*AverageNetworkDelay, func() {
		defer cachedTransactionMetadata.Release()

		transactionMetadata := cachedTransactionMetadata.Unwrap()
		if transactionMetadata == nil {
			return
		}

		if transactionMetadata.BranchID() != branchmanager.NewBranchID(transactionMetadata.ID()) {
			return
		}

		transactionMetadata.SetFinalized(true)
	})
}

func onFork(cachedTransaction *transaction.CachedTransaction, cachedTransactionMetadata *utxodag.CachedTransactionMetadata, cachedBranch *branchmanager.CachedBranch, conflictingInputs []transaction.OutputID) {
	defer cachedTransaction.Release()
	defer cachedTransactionMetadata.Release()
	defer cachedBranch.Release()

	transactionMetadata := cachedTransactionMetadata.Unwrap()
	if transactionMetadata == nil {
		return
	}

	branch := cachedBranch.Unwrap()
	if branch == nil {
		return
	}

	if time.Since(transactionMetadata.SoldificationTime()) < AverageNetworkDelay {
		return
	}

	if _, err := UTXODAG.BranchManager().SetBranchPreferred(branch.ID(), true); err != nil {
		log.Error(err)
	}
}