package ca

import (
	"sync"
	"time"

	"github.com/iotaledger/goshimmer/packages/identity"

	"github.com/iotaledger/goshimmer/packages/ca/heartbeat"

	"github.com/iotaledger/goshimmer/packages/errors"
)

type HeartbeatManager struct {
	identity         *identity.Identity
	options          *HeartbeatManagerOptions
	statementChain   *StatementChain
	neighborManagers map[string]*NeighborManager
	initialOpinions  map[string]bool

	neighborManagersMutex sync.RWMutex
}

func NewHeartbeatManager(identity *identity.Identity, options ...HeartbeatManagerOption) *HeartbeatManager {
	return &HeartbeatManager{
		identity: identity,
		options:  DEFAULT_OPTIONS.Override(options...),

		statementChain:   NewStatementChain(),
		neighborManagers: make(map[string]*NeighborManager),
		initialOpinions:  make(map[string]bool),
	}
}

func (heartbeatManager *HeartbeatManager) InitialDislike(transactionId []byte) {
	heartbeatManager.initialOpinions[string(transactionId)] = false
}

func (heartbeatManager *HeartbeatManager) InitialLike(transactionId []byte) {
	heartbeatManager.initialOpinions[string(transactionId)] = true
}

func (heartbeatManager *HeartbeatManager) GenerateHeartbeat() (result *heartbeat.Heartbeat, err errors.IdentifiableError) {
	if mainStatement, mainStatementErr := heartbeatManager.GenerateMainStatement(); mainStatementErr == nil {
		generatedHeartbeat := heartbeat.NewHeartbeat()
		generatedHeartbeat.SetNodeId(heartbeatManager.identity.StringIdentifier)
		generatedHeartbeat.SetMainStatement(mainStatement)
		generatedHeartbeat.SetNeighborStatements(nil)

		if signingErr := generatedHeartbeat.Sign(heartbeatManager.identity); signingErr == nil {
			result = generatedHeartbeat
		} else {
			err = signingErr
		}
	} else {
		err = mainStatementErr
	}

	return
}

func (heartbeatManager *HeartbeatManager) GenerateMainStatement() (result *heartbeat.OpinionStatement, err errors.IdentifiableError) {
	mainStatement := heartbeat.NewOpinionStatement()
	mainStatement.SetNodeId(heartbeatManager.identity.StringIdentifier)
	mainStatement.SetTime(uint64(time.Now().Unix()))
	mainStatement.SetToggledTransactions(heartbeatManager.GenerateToggledTransactions())

	if lastAppliedStatement := heartbeatManager.statementChain.lastAppliedStatement; lastAppliedStatement != nil {
		mainStatement.SetPreviousStatementHash(lastAppliedStatement.GetHash())
	}

	if signingErr := mainStatement.Sign(heartbeatManager.identity); signingErr == nil {
		result = mainStatement

		heartbeatManager.ResetInitialOpinions()
		heartbeatManager.statementChain.lastAppliedStatement = mainStatement
	} else {
		err = signingErr
	}

	return
}

func (heartbeatManager *HeartbeatManager) GenerateToggledTransactions() []*heartbeat.ToggledTransaction {
	toggledTransactions := make([]*heartbeat.ToggledTransaction, 0)
	for transactionId, liked := range heartbeatManager.initialOpinions {
		if !liked {
			newToggledTransaction := heartbeat.NewToggledTransaction()
			newToggledTransaction.SetInitialStatement(true)
			newToggledTransaction.SetFinalStatement(false)
			newToggledTransaction.SetTransactionId([]byte(transactionId))

			toggledTransactions = append(toggledTransactions, newToggledTransaction)
		}
	}

	return toggledTransactions
}

func (heartbeatManager *HeartbeatManager) ResetInitialOpinions() {
	heartbeatManager.initialOpinions = make(map[string]bool)
}

func (heartbeatManager *HeartbeatManager) ApplyHeartbeat(heartbeat *heartbeat.Heartbeat) (err errors.IdentifiableError) {
	heartbeatManager.neighborManagersMutex.RLock()
	defer heartbeatManager.neighborManagersMutex.RUnlock()

	issuerId := heartbeat.GetNodeId()

	neighborManager, neighborExists := heartbeatManager.neighborManagers[issuerId]
	if !neighborExists {
		err = ErrUnknownNeighbor.Derive("unknown neighbor: " + issuerId)
	} else {
		err = neighborManager.ApplyHeartbeat(heartbeat)
	}

	return
}