package consensus

import (
	"fmt"

	"git.marketally.com/tutus-one/tutus-consensus"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config/netmode"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	npayload "git.marketally.com/tutus-one/tutus-chain/pkg/network/payload"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/emit"
)

type (
	messageType byte

	message struct {
		Type           messageType
		BlockIndex     uint32
		ValidatorIndex byte
		ViewNumber     byte

		payload io.Serializable
		// stateRootEnabled specifies if state root is exchanged during consensus.
		stateRootEnabled bool
	}

	// Payload is a type for consensus-related messages.
	Payload struct {
		npayload.Extensible
		message
		network netmode.Magic
	}
)

const (
	changeViewType      messageType = 0x00
	prepareRequestType  messageType = 0x20
	prepareResponseType messageType = 0x21
	commitType          messageType = 0x30
	recoveryRequestType messageType = 0x40
	recoveryMessageType messageType = 0x41

	payloadGasLimit = 2000000 // 0.02 GAS
)

var _ dbft.ConsensusPayload[util.Uint256] = &Payload{}

// ViewNumber implements the payload.ConsensusPayload interface.
func (p Payload) ViewNumber() byte {
	return p.message.ViewNumber
}

// SetViewNumber implements the payload.ConsensusPayload interface.
func (p *Payload) SetViewNumber(view byte) {
	p.message.ViewNumber = view
}

// Type implements the payload.ConsensusPayload interface.
func (p Payload) Type() dbft.MessageType {
	return dbft.MessageType(p.message.Type)
}

// Payload implements the payload.ConsensusPayload interface.
func (p Payload) Payload() any {
	return p.payload
}

// GetChangeView implements the payload.ConsensusPayload interface.
func (p Payload) GetChangeView() dbft.ChangeView { return p.payload.(dbft.ChangeView) }

// GetPrepareRequest implements the payload.ConsensusPayload interface.
func (p Payload) GetPrepareRequest() dbft.PrepareRequest[util.Uint256] {
	return p.payload.(dbft.PrepareRequest[util.Uint256])
}

// GetPrepareResponse implements the payload.ConsensusPayload interface.
func (p Payload) GetPrepareResponse() dbft.PrepareResponse[util.Uint256] {
	return p.payload.(dbft.PrepareResponse[util.Uint256])
}

// GetPreCommit implements the payload.ConsensusPayload interface.
// It's a stub since PreCommits are never used on N3.
func (p Payload) GetPreCommit() dbft.PreCommit { return nil }

// GetCommit implements the payload.ConsensusPayload interface.
func (p Payload) GetCommit() dbft.Commit { return p.payload.(dbft.Commit) }

// GetRecoveryRequest implements the payload.ConsensusPayload interface.
func (p Payload) GetRecoveryRequest() dbft.RecoveryRequest {
	return p.payload.(dbft.RecoveryRequest)
}

// GetRecoveryMessage implements the payload.ConsensusPayload interface.
func (p Payload) GetRecoveryMessage() dbft.RecoveryMessage[util.Uint256] {
	return p.payload.(dbft.RecoveryMessage[util.Uint256])
}

// ValidatorIndex implements the payload.ConsensusPayload interface.
func (p Payload) ValidatorIndex() uint16 {
	return uint16(p.message.ValidatorIndex)
}

// SetValidatorIndex implements the payload.ConsensusPayload interface.
func (p *Payload) SetValidatorIndex(i uint16) {
	p.message.ValidatorIndex = byte(i)
}

// Height implements the payload.ConsensusPayload interface.
func (p Payload) Height() uint32 {
	return p.BlockIndex
}

// EncodeBinary implements the io.Serializable interface.
func (p *Payload) EncodeBinary(w *io.BinWriter) {
	p.encodeData()
	p.Extensible.EncodeBinary(w)
}

// Sign signs payload using the private key.
// It also sets corresponding verification and invocation scripts.
func (p *Payload) Sign(key *keys.PrivateKey) error {
	p.encodeData()
	sig := key.SignHashable(uint32(p.network), &p.Extensible)

	buf := io.NewBufBinWriter()
	emit.Bytes(buf.BinWriter, sig)
	p.Witness.InvocationScript = buf.Bytes()
	p.Witness.VerificationScript = key.PublicKey().GetVerificationScript()

	return nil
}

// Hash implements the payload.ConsensusPayload interface.
func (p *Payload) Hash() util.Uint256 {
	if p.Data == nil {
		p.encodeData()
	}
	return p.Extensible.Hash()
}

// DecodeBinary implements the io.Serializable interface.
func (p *Payload) DecodeBinary(r *io.BinReader) {
	p.Extensible.DecodeBinary(r)
	if r.Err == nil {
		r.Err = p.decodeData()
	}
}

// EncodeBinary implements the io.Serializable interface.
func (m *message) EncodeBinary(w *io.BinWriter) {
	w.WriteB(byte(m.Type))
	w.WriteU32LE(m.BlockIndex)
	w.WriteB(m.ValidatorIndex)
	w.WriteB(m.ViewNumber)
	m.payload.EncodeBinary(w)
}

// DecodeBinary implements the io.Serializable interface.
func (m *message) DecodeBinary(r *io.BinReader) {
	m.Type = messageType(r.ReadB())
	m.BlockIndex = r.ReadU32LE()
	m.ValidatorIndex = r.ReadB()
	m.ViewNumber = r.ReadB()

	switch m.Type {
	case changeViewType:
		cv := new(changeView)
		// newViewNumber is not marshaled
		cv.newViewNumber = m.ViewNumber + 1
		m.payload = cv
	case prepareRequestType:
		r := new(prepareRequest)
		if m.stateRootEnabled {
			r.stateRootEnabled = true
		}
		m.payload = r
	case prepareResponseType:
		m.payload = new(prepareResponse)
	case commitType:
		m.payload = new(commit)
	case recoveryRequestType:
		m.payload = new(recoveryRequest)
	case recoveryMessageType:
		r := new(recoveryMessage)
		if m.stateRootEnabled {
			r.stateRootEnabled = true
		}
		m.payload = r
	default:
		r.Err = fmt.Errorf("invalid type: 0x%02x", byte(m.Type))
		return
	}
	m.payload.DecodeBinary(r)
}

// String implements fmt.Stringer interface.
func (t messageType) String() string {
	switch t {
	case changeViewType:
		return "ChangeView"
	case prepareRequestType:
		return "PrepareRequest"
	case prepareResponseType:
		return "PrepareResponse"
	case commitType:
		return "Commit"
	case recoveryRequestType:
		return "RecoveryRequest"
	case recoveryMessageType:
		return "RecoveryMessage"
	default:
		return fmt.Sprintf("UNKNOWN(0x%02x)", byte(t))
	}
}

func (p *Payload) encodeData() {
	if p.Data == nil {
		p.ValidBlockStart = 0
		p.ValidBlockEnd = p.BlockIndex
		bw := io.NewBufBinWriter()
		p.message.EncodeBinary(bw.BinWriter)
		p.Data = bw.Bytes()
	}
}

// decode data of payload into its message.
func (p *Payload) decodeData() error {
	br := io.NewBinReaderFromBuf(p.Data)
	p.message.DecodeBinary(br)
	if br.Err != nil {
		return fmt.Errorf("can't decode message: %w", br.Err)
	}
	return nil
}
