package stateroot

import (
	"errors"
	"fmt"

	"github.com/nspcc-dev/neo-go/pkg/config"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/network/payload"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"go.uber.org/zap"
)

const rootValidEndInc = 100

// RelayCallback represents callback for sending validated state roots.
type RelayCallback = func(*payload.Extensible)

// AddSignature adds a state root signature.
func (s *service) AddSignature(height uint32, validatorIndex int32, sig []byte) error {
	if !s.MainCfg.Enabled {
		return nil
	}
	myIndex, acc := s.getAccount()
	if acc == nil {
		return nil
	}

	incRoot := s.getIncompleteRoot(height, myIndex)
	if incRoot == nil {
		return nil
	}

	incRoot.Lock()
	defer incRoot.Unlock()

	if validatorIndex < 0 || int(validatorIndex) >= len(incRoot.svList) {
		return errors.New("invalid validator index")
	}

	pub := incRoot.svList[validatorIndex]
	if incRoot.root != nil {
		ok := pub.VerifyHashable(sig, uint32(s.Network), incRoot.root)
		if !ok {
			return fmt.Errorf("invalid state root signature for %d", validatorIndex)
		}
	}
	incRoot.addSignature(pub, sig)
	s.trySendRoot(incRoot, acc)
	return nil
}

// GetConfig returns service configuration.
func (s *service) GetConfig() config.StateRoot {
	return s.MainCfg
}

func (s *service) getIncompleteRoot(height uint32, myIndex byte) *incompleteRoot {
	s.srMtx.Lock()
	defer s.srMtx.Unlock()
	if incRoot, ok := s.incompleteRoots[height]; ok {
		return incRoot
	}
	incRoot := &incompleteRoot{
		myIndex: int(myIndex),
		svList:  s.GetStateValidators(height),
		sigs:    make(map[string]*rootSig),
	}
	s.incompleteRoots[height] = incRoot
	return incRoot
}

// trySendRoot attempts to finalize and send MPTRoot, it must be called with the ir locked.
func (s *service) trySendRoot(ir *incompleteRoot, acc *wallet.Account) {
	if !ir.isSenderNow() {
		return
	}
	sr, ready := ir.finalize()
	if ready {
		err := s.AddStateRoot(sr)
		if err != nil {
			s.log.Error("can't add validated state root", zap.Error(err))
		}
		s.sendValidatedRoot(sr, acc)
		ir.isSent = true
	}
}

func (s *service) sendValidatedRoot(r *state.MPTRoot, acc *wallet.Account) {
	w := io.NewBufBinWriter()
	m := NewMessage(RootT, r)
	m.EncodeBinary(w.BinWriter)
	ep := &payload.Extensible{
		Category:        Category,
		ValidBlockStart: r.Index,
		ValidBlockEnd:   r.Index + rootValidEndInc,
		Sender:          acc.ScriptHash(),
		Data:            w.Bytes(),
		Witness: transaction.Witness{
			VerificationScript: acc.GetVerificationScript(),
		},
	}
	sig := acc.SignHashable(s.Network, ep)
	buf := io.NewBufBinWriter()
	emit.Bytes(buf.BinWriter, sig)
	ep.Witness.InvocationScript = buf.Bytes()
	s.relayExtensible(ep)
}