package result

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/coreos/go-semver/semver"
	"github.com/nspcc-dev/neo-go/pkg/config"
	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
)

type (
	// Version model used for reporting server version
	// info.
	Version struct {
		// Magic contains network magic.
		// Deprecated: use Protocol.Network instead
		Magic     netmode.Magic
		TCPPort   uint16
		WSPort    uint16
		Nonce     uint32
		UserAgent string
		Protocol  Protocol
		// StateRootInHeader is true if state root is contained in the block header.
		// Deprecated: use Protocol.StateRootInHeader instead
		StateRootInHeader bool
	}

	// Protocol represents network-dependent parameters.
	Protocol struct {
		AddressVersion              byte
		Network                     netmode.Magic
		MillisecondsPerBlock        int
		MaxTraceableBlocks          uint32
		MaxValidUntilBlockIncrement uint32
		MaxTransactionsPerBlock     uint16
		MemoryPoolMaxTransactions   int
		ValidatorsCount             byte
		InitialGasDistribution      fixedn.Fixed8

		// Below are NeoGo-specific extensions to the protocol that are
		// returned by the server in case they're enabled.

		// CommitteeHistory stores height:size map of the committee size.
		CommitteeHistory map[uint32]int
		// P2PSigExtensions is true when Notary subsystem is enabled on the network.
		P2PSigExtensions bool
		// StateRootInHeader is true if state root is contained in block header.
		StateRootInHeader bool
		// ValidatorsHistory stores height:size map of the validators count.
		ValidatorsHistory map[uint32]int
	}
)

type (
	// versionMarshallerAux is an auxiliary struct used for Version JSON marshalling.
	versionMarshallerAux struct {
		Magic             netmode.Magic         `json:"network"`
		TCPPort           uint16                `json:"tcpport"`
		WSPort            uint16                `json:"wsport,omitempty"`
		Nonce             uint32                `json:"nonce"`
		UserAgent         string                `json:"useragent"`
		Protocol          protocolMarshallerAux `json:"protocol"`
		StateRootInHeader bool                  `json:"staterootinheader,omitempty"`
	}

	// protocolMarshallerAux is an auxiliary struct used for Protocol JSON marshalling.
	protocolMarshallerAux struct {
		AddressVersion              byte          `json:"addressversion"`
		Network                     netmode.Magic `json:"network"`
		MillisecondsPerBlock        int           `json:"msperblock"`
		MaxTraceableBlocks          uint32        `json:"maxtraceableblocks"`
		MaxValidUntilBlockIncrement uint32        `json:"maxvaliduntilblockincrement"`
		MaxTransactionsPerBlock     uint16        `json:"maxtransactionsperblock"`
		MemoryPoolMaxTransactions   int           `json:"memorypoolmaxtransactions"`
		ValidatorsCount             byte          `json:"validatorscount"`
		InitialGasDistribution      int64         `json:"initialgasdistribution"`

		CommitteeHistory  map[uint32]int `json:"committeehistory,omitempty"`
		P2PSigExtensions  bool           `json:"p2psigextensions,omitempty"`
		StateRootInHeader bool           `json:"staterootinheader,omitempty"`
		ValidatorsHistory map[uint32]int `json:"validatorshistory,omitempty"`
	}

	// versionUnmarshallerAux is an auxiliary struct used for Version JSON unmarshalling.
	versionUnmarshallerAux struct {
		Magic             netmode.Magic           `json:"network"`
		TCPPort           uint16                  `json:"tcpport"`
		WSPort            uint16                  `json:"wsport,omitempty"`
		Nonce             uint32                  `json:"nonce"`
		UserAgent         string                  `json:"useragent"`
		Protocol          protocolUnmarshallerAux `json:"protocol"`
		StateRootInHeader bool                    `json:"staterootinheader,omitempty"`
	}

	// protocolUnmarshallerAux is an auxiliary struct used for Protocol JSON unmarshalling.
	protocolUnmarshallerAux struct {
		AddressVersion              byte            `json:"addressversion"`
		Network                     netmode.Magic   `json:"network"`
		MillisecondsPerBlock        int             `json:"msperblock"`
		MaxTraceableBlocks          uint32          `json:"maxtraceableblocks"`
		MaxValidUntilBlockIncrement uint32          `json:"maxvaliduntilblockincrement"`
		MaxTransactionsPerBlock     uint16          `json:"maxtransactionsperblock"`
		MemoryPoolMaxTransactions   int             `json:"memorypoolmaxtransactions"`
		ValidatorsCount             byte            `json:"validatorscount"`
		InitialGasDistribution      json.RawMessage `json:"initialgasdistribution"`

		CommitteeHistory  map[uint32]int `json:"committeehistory,omitempty"`
		P2PSigExtensions  bool           `json:"p2psigextensions,omitempty"`
		StateRootInHeader bool           `json:"staterootinheader,omitempty"`
		ValidatorsHistory map[uint32]int `json:"validatorshistory,omitempty"`
	}
)

// latestNonBreakingVersion is a latest NeoGo revision that keeps older RPC
// clients compatibility with newer RPC servers (https://github.com/nspcc-dev/neo-go/pull/2435).
var latestNonBreakingVersion = *semver.New("0.98.5")

// MarshalJSON implements the json marshaller interface.
func (v *Version) MarshalJSON() ([]byte, error) {
	aux := versionMarshallerAux{
		Magic:     v.Magic,
		TCPPort:   v.TCPPort,
		WSPort:    v.WSPort,
		Nonce:     v.Nonce,
		UserAgent: v.UserAgent,
		Protocol: protocolMarshallerAux{
			AddressVersion:              v.Protocol.AddressVersion,
			Network:                     v.Protocol.Network,
			MillisecondsPerBlock:        v.Protocol.MillisecondsPerBlock,
			MaxTraceableBlocks:          v.Protocol.MaxTraceableBlocks,
			MaxValidUntilBlockIncrement: v.Protocol.MaxValidUntilBlockIncrement,
			MaxTransactionsPerBlock:     v.Protocol.MaxTransactionsPerBlock,
			MemoryPoolMaxTransactions:   v.Protocol.MemoryPoolMaxTransactions,
			ValidatorsCount:             v.Protocol.ValidatorsCount,
			InitialGasDistribution:      int64(v.Protocol.InitialGasDistribution),

			CommitteeHistory:  v.Protocol.CommitteeHistory,
			P2PSigExtensions:  v.Protocol.P2PSigExtensions,
			StateRootInHeader: v.Protocol.StateRootInHeader,
			ValidatorsHistory: v.Protocol.ValidatorsHistory,
		},
		StateRootInHeader: v.StateRootInHeader,
	}
	return json.Marshal(aux)
}

// UnmarshalJSON implements the json unmarshaller interface.
func (v *Version) UnmarshalJSON(data []byte) error {
	var aux versionUnmarshallerAux
	err := json.Unmarshal(data, &aux)
	if err != nil {
		return err
	}
	v.Magic = aux.Magic
	v.TCPPort = aux.TCPPort
	v.WSPort = aux.WSPort
	v.Nonce = aux.Nonce
	v.UserAgent = aux.UserAgent
	v.Protocol.AddressVersion = aux.Protocol.AddressVersion
	v.Protocol.Network = aux.Protocol.Network
	v.Protocol.MillisecondsPerBlock = aux.Protocol.MillisecondsPerBlock
	v.Protocol.MaxTraceableBlocks = aux.Protocol.MaxTraceableBlocks
	v.Protocol.MaxValidUntilBlockIncrement = aux.Protocol.MaxValidUntilBlockIncrement
	v.Protocol.MaxTransactionsPerBlock = aux.Protocol.MaxTransactionsPerBlock
	v.Protocol.MemoryPoolMaxTransactions = aux.Protocol.MemoryPoolMaxTransactions
	v.Protocol.ValidatorsCount = aux.Protocol.ValidatorsCount
	v.Protocol.CommitteeHistory = aux.Protocol.CommitteeHistory
	v.Protocol.P2PSigExtensions = aux.Protocol.P2PSigExtensions
	v.Protocol.StateRootInHeader = aux.Protocol.StateRootInHeader
	v.Protocol.ValidatorsHistory = aux.Protocol.ValidatorsHistory
	v.StateRootInHeader = aux.StateRootInHeader
	if len(aux.Protocol.InitialGasDistribution) == 0 {
		return nil
	}

	if strings.HasPrefix(v.UserAgent, config.UserAgentWrapper+config.UserAgentPrefix) {
		ver, err := userAgentToVersion(v.UserAgent)
		if err == nil && ver.Compare(latestNonBreakingVersion) <= 0 {
			err := json.Unmarshal(aux.Protocol.InitialGasDistribution, &v.Protocol.InitialGasDistribution)
			if err != nil {
				return fmt.Errorf("failed to unmarshal InitialGASDistribution into fixed8: %w", err)
			}
			return nil
		}
	}
	var val int64
	err = json.Unmarshal(aux.Protocol.InitialGasDistribution, &val)
	if err != nil {
		return fmt.Errorf("failed to unmarshal InitialGASDistribution into int64: %w", err)
	}
	v.Protocol.InitialGasDistribution = fixedn.Fixed8(val)

	return nil
}

func userAgentToVersion(userAgent string) (*semver.Version, error) {
	verStr := strings.Trim(userAgent, config.UserAgentWrapper)
	verStr = strings.TrimPrefix(verStr, config.UserAgentPrefix)
	ver, err := semver.NewVersion(verStr)
	if err != nil {
		return nil, fmt.Errorf("can't retrieve neo-go version from UserAgent: %w", err)
	}
	return ver, nil
}