package types

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"math"
	"math/big"
	"strings"
	"time"

	"github.com/aergoio/aergo/v2/internal/enc/gob"
	"github.com/aergoio/aergo/v2/internal/enc/hex"
)

const (
	blockVersionNil = math.MinInt32
	devChainMagic   = "dev.chain"
)

var (
	nilChainID = ChainID{
		Version:   0,
		Magic:     "",
		PublicNet: false,
		MainNet:   false,
		Consensus: "",
	}

	defaultChainID = ChainID{
		Version:   0,
		Magic:     devChainMagic,
		PublicNet: false,
		MainNet:   false,
		Consensus: "sbp",
	}
)

const (
	cidMarshal = iota
	cidUnmarshal
)

const (
	versionByteSize                  = 4
	chainIdStartOffsetWithoutVersion = versionByteSize
)

type errCidCodec struct {
	codec int
	field string
	err   error
}

func (e errCidCodec) Error() string {
	kind := "unmarshal"
	if e.codec == cidMarshal {
		kind = "marshal"
	}
	return fmt.Sprintf("failed to %s %s - %s", kind, e.field, e.err.Error())
}

// ChainID represents the identity of the chain.
type ChainID struct {
	Version   int32  `json:"version"`
	PublicNet bool   `json:"public"`
	MainNet   bool   `json:"mainnet"`
	Magic     string `json:"magic"`
	Consensus string `json:"consensus"`
}

// NewChainID returns a new ChainID initialized as nilChainID.
func NewChainID() *ChainID {
	nilCID := nilChainID

	return &nilCID
}

// Bytes returns the binary representation of cid.
func (cid *ChainID) Bytes() ([]byte, error) {
	var w bytes.Buffer

	// warning: when any field added to ChainID, the corresponding
	// serialization code must be written here.
	w.Write(ChainIdVersion(cid.Version))
	if err := binary.Write(&w, binary.LittleEndian, cid.PublicNet); err != nil {
		return nil, errCidCodec{
			codec: cidMarshal,
			field: "publicnet",
			err:   err,
		}
	}
	if err := binary.Write(&w, binary.LittleEndian, cid.MainNet); err != nil {
		return nil, errCidCodec{
			codec: cidMarshal,
			field: "mainnet",
			err:   err,
		}
	}

	others := fmt.Sprintf("%s/%s", cid.Magic, cid.Consensus)
	if err := binary.Write(&w, binary.LittleEndian, []byte(others)); err != nil {
		return nil, errCidCodec{
			codec: cidMarshal,
			field: "magic/consensus",
			err:   err,
		}
	}

	return w.Bytes(), nil
}

// Read deserialize data as a ChainID.
func (cid *ChainID) Read(data []byte) error {
	r := bytes.NewBuffer(data)

	// warning: when any field added to ChainID, the corresponding
	// deserialization code must be written here.
	if err := binary.Read(r, binary.LittleEndian, &cid.Version); err != nil {
		return errCidCodec{
			codec: cidUnmarshal,
			field: "version",
			err:   err,
		}
	}
	if err := binary.Read(r, binary.LittleEndian, &cid.PublicNet); err != nil {
		return errCidCodec{
			codec: cidUnmarshal,
			field: "publicnet",
			err:   err,
		}
	}
	if err := binary.Read(r, binary.LittleEndian, &cid.MainNet); err != nil {
		return errCidCodec{
			codec: cidUnmarshal,
			field: "mainnet",
			err:   err,
		}
	}
	mc := strings.Split(string(r.Bytes()), "/")
	if len(mc) != 2 {
		return errCidCodec{
			codec: cidUnmarshal,
			field: "magic/consensus",
			err:   fmt.Errorf("wrong number of fields: %s", mc),
		}
	}
	cid.Magic, cid.Consensus = mc[0], mc[1]

	return nil
}

// AsDefault set *cid to the default chain id (cid must be a valid pointer).
func (cid *ChainID) AsDefault() {
	*cid = defaultChainID
}

// Equals reports whether cid equals rhs or not.
func (cid *ChainID) Equals(rhs *ChainID) bool {
	if cid == nil || rhs == nil {
		return false
	}
	if cid == rhs {
		return true
	}
	if cid.Version != rhs.Version ||
		cid.PublicNet != rhs.PublicNet ||
		cid.MainNet != rhs.MainNet ||
		cid.Magic != rhs.Magic ||
		cid.Consensus != rhs.Consensus {
		return false
	}
	return true
}

// ToJSON returns a JSON encoded string of cid.
func (cid ChainID) ToJSON() string {
	if b, err := json.Marshal(cid); err == nil {
		return string(b)
	}
	return ""
}

func ChainIdVersion(v int32) []byte {
	b := make([]byte, versionByteSize)
	binary.LittleEndian.PutUint32(b, uint32(v))
	return b
}

func DecodeChainIdVersion(cid []byte) int32 {
	if len(cid) < 4 {
		return -1
	}
	return int32(binary.LittleEndian.Uint32(cid))
}

func ChainIdEqualWithoutVersion(a, b []byte) bool {
	if len(a) < chainIdStartOffsetWithoutVersion || len(b) < chainIdStartOffsetWithoutVersion {
		return false
	}
	return bytes.Equal(a[chainIdStartOffsetWithoutVersion:], b[chainIdStartOffsetWithoutVersion:])
}

type EnterpriseBP struct {
	Name string `json:"name"`
	// multiaddress format with ip or dns with port e.g. /ip4/123.45.67.89/tcp/7846
	Address string `json:"address"`
	PeerID  string `json:"peerid"`
}

// Genesis represents genesis block
type Genesis struct {
	ID            ChainID           `json:"chain_id,omitempty"`
	Timestamp     int64             `json:"timestamp,omitempty"`
	Balance       map[string]string `json:"balance"`
	BPs           []string          `json:"bps"`
	EnterpriseBPs []EnterpriseBP    `json:"enterprise_bps,omitempty"`

	// followings are for internal use only
	totalBalance *big.Int
	block        *Block
}

// Block returns Block corresponding to g. If g.block == nil, it genreates a
// genesis block before it returns.
func (g *Genesis) Validate() error {
	_, err := g.ChainID()
	if err != nil {
		return err
	}
	//TODO check BP count
	return nil
}

// Block returns Block corresponding to g.
func (g *Genesis) Block() *Block {
	if g.block == nil {
		g.SetBlock(NewBlock(&BlockHeaderInfo{Ts: g.Timestamp}, nil, nil, nil, nil, nil))
		if id, err := g.ID.Bytes(); err == nil {
			g.block.SetChainID(id)
		}
	}
	return g.block
}

// AddBalance adds bal to g.totalBalance.
func (g *Genesis) AddBalance(bal *big.Int) {
	if g.totalBalance == nil {
		g.totalBalance = big.NewInt(0)
	}
	g.totalBalance.Add(g.totalBalance, bal)
}

// TotalBalance returns the total initial balance of the chain.
func (g *Genesis) TotalBalance() *big.Int {
	return g.totalBalance
}

// SetTotalBalance sets g.totalBalance to v if g.totalBlance has no valid
// value (nil).
func (g *Genesis) SetTotalBalance(v []byte) {
	if g.totalBalance == nil {
		g.totalBalance = big.NewInt(0).SetBytes(v)
	}
}

// SetBlock sets g.block to block if g.block == nil.
func (g *Genesis) SetBlock(block *Block) {
	if g.block == nil {
		g.block = block
	}
}

// ChainID returns the binary representation of g.ID.
func (g *Genesis) ChainID() ([]byte, error) {
	return g.ID.Bytes()
}

// Bytes returns byte-encoded BPs from g.
func (g Genesis) Bytes() []byte {
	// Omit the Balance to reduce the resulting data size.
	g.Balance = nil
	if b, err := gob.Encode(g); err == nil {
		return b
	}
	return nil
}

// ConsensusType retruns g.ID.ConsensusType.
func (g Genesis) ConsensusType() string {
	return g.ID.Consensus
}

// PublicNet reports whether g corresponds to PublicNet.
func (g Genesis) PublicNet() bool {
	return g.ID.PublicNet
}

func (g Genesis) IsAergoPublicChain() bool {
	return g.IsMainNet() || g.IsTestNet()
}

func (g Genesis) HasDevChainID() bool {
	if g.ID.Magic == devChainMagic {
		return true
	}
	return false
}

func (g Genesis) HasPrivateChainID() bool {
	if g.IsAergoPublicChain() || g.HasDevChainID() {
		return false
	}
	return true
}

func (g *Genesis) IsMainNet() bool {
	return g.ID.Equals(&(GetMainNetGenesis().ID))
}

func (g *Genesis) IsTestNet() bool {
	return g.ID.Equals(&(GetTestNetGenesis().ID))
}

// GetDefaultGenesis returns default genesis structure
func GetDefaultGenesis() *Genesis {
	return &Genesis{
		ID:        defaultChainID,
		Timestamp: time.Now().UnixNano(),
		block:     nil,
	} //TODO embed MAINNET genesis block
}

func GetMainNetGenesis() *Genesis {
	if bs, err := hex.Decode(MainNetGenesis); err == nil {
		var g Genesis
		if err := json.Unmarshal(bs, &g); err == nil {
			return &g
		}
	}
	return nil
}
func GetTestNetGenesis() *Genesis {
	if bs, err := hex.Decode(TestNetGenesis); err == nil {
		var g Genesis
		if err := json.Unmarshal(bs, &g); err == nil {
			return &g
		}
	}
	return nil
}

// GetTestGenesis returns Gensis object for a unit test.
func GetTestGenesis() *Genesis {
	genesis := &Genesis{
		ID: ChainID{
			Version:   0,
			Magic:     devChainMagic,
			PublicNet: true,
			MainNet:   false,
			Consensus: "sbp",
		},
		Timestamp: time.Now().UnixNano(),
		block:     nil,
	} //TODO embed MAINNET genesis block

	genesis.Block()

	return genesis
}

// GetGenesisFromBytes decodes & return Genesis from b.
func GetGenesisFromBytes(b []byte) *Genesis {
	g := &Genesis{}
	if err := gob.Decode(b, g); err == nil {
		return g
	}
	return nil
}
