package netmap

import (
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap"
	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
	"git.frostfs.info/TrueCloudLab/hrw"
)

// NodeInfo groups information about FrostFS storage node which is reflected
// in the FrostFS network map. Storage nodes advertise this information when
// registering with the FrostFS network. After successful registration, information
// about the nodes is available to all network participants to work with the network
// map (mainly to comply with container storage policies).
//
// NodeInfo is mutually compatible with git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap.NodeInfo
// message. See ReadFromV2 / WriteToV2 methods.
//
// Instances can be created using built-in var declaration.
type NodeInfo struct {
	m    netmap.NodeInfo
	hash uint64
}

// reads NodeInfo from netmap.NodeInfo message. If checkFieldPresence is set,
// returns an error on absence of any protocol-required field. Verifies format of any
// presented field according to FrostFS API V2 protocol.
func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error {
	var err error

	binPublicKey := m.GetPublicKey()
	if checkFieldPresence && len(binPublicKey) == 0 {
		return errors.New("missing public key")
	}

	if checkFieldPresence && m.NumberOfAddresses() <= 0 {
		return errors.New("missing network endpoints")
	}

	attributes := m.GetAttributes()
	mAttr := make(map[string]struct{}, len(attributes))
	for i := range attributes {
		key := attributes[i].GetKey()
		if key == "" {
			return fmt.Errorf("empty key of the attribute #%d", i)
		} else if _, ok := mAttr[key]; ok {
			return fmt.Errorf("duplicated attbiuted %s", key)
		}

		switch {
		case key == attrCapacity:
			_, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64)
			if err != nil {
				return fmt.Errorf("invalid %s attribute: %w", attrCapacity, err)
			}
		case key == attrPrice:
			var err error
			_, err = strconv.ParseUint(attributes[i].GetValue(), 10, 64)
			if err != nil {
				return fmt.Errorf("invalid %s attribute: %w", attrPrice, err)
			}
		default:
			if attributes[i].GetValue() == "" {
				return fmt.Errorf("empty value of the attribute %s", key)
			}
		}
	}

	x.m = m
	x.hash = hrw.Hash(binPublicKey)

	return nil
}

// ReadFromV2 reads NodeInfo from the netmap.NodeInfo message. Checks if the
// message conforms to FrostFS API V2 protocol.
//
// See also WriteToV2.
func (x *NodeInfo) ReadFromV2(m netmap.NodeInfo) error {
	return x.readFromV2(m, true)
}

// WriteToV2 writes NodeInfo to the netmap.NodeInfo message. The message MUST NOT
// be nil.
//
// See also ReadFromV2.
func (x NodeInfo) WriteToV2(m *netmap.NodeInfo) {
	*m = x.m
}

// Marshal encodes NodeInfo into a binary format of the FrostFS API protocol
// (Protocol Buffers with direct field order).
//
// See also Unmarshal.
func (x NodeInfo) Marshal() []byte {
	var m netmap.NodeInfo
	x.WriteToV2(&m)

	return m.StableMarshal(nil)
}

// Unmarshal decodes FrostFS API protocol binary format into the NodeInfo
// (Protocol Buffers with direct field order). Returns an error describing
// a format violation.
//
// See also Marshal.
func (x *NodeInfo) Unmarshal(data []byte) error {
	var m netmap.NodeInfo

	err := m.Unmarshal(data)
	if err != nil {
		return err
	}

	return x.readFromV2(m, false)
}

// MarshalJSON encodes NodeInfo into a JSON format of the FrostFS API protocol
// (Protocol Buffers JSON).
//
// See also UnmarshalJSON.
func (x NodeInfo) MarshalJSON() ([]byte, error) {
	var m netmap.NodeInfo
	x.WriteToV2(&m)

	return m.MarshalJSON()
}

// UnmarshalJSON decodes FrostFS API protocol JSON format into the NodeInfo
// (Protocol Buffers JSON). Returns an error describing a format violation.
//
// See also MarshalJSON.
func (x *NodeInfo) UnmarshalJSON(data []byte) error {
	var m netmap.NodeInfo

	err := m.UnmarshalJSON(data)
	if err != nil {
		return err
	}

	return x.readFromV2(m, false)
}

// SetPublicKey sets binary-encoded public key bound to the node. The key
// authenticates the storage node, so it MUST be unique within the network.
//
// Argument MUST NOT be mutated, make a copy first.
//
// See also PublicKey.
func (x *NodeInfo) SetPublicKey(key []byte) {
	x.m.SetPublicKey(key)
	x.hash = hrw.Hash(x.m.GetPublicKey())
}

// PublicKey returns value set using SetPublicKey.
//
// Zero NodeInfo has no public key, which is incorrect according to
// FrostFS system requirements.
//
// Return value MUST not be mutated, make a copy first.
func (x NodeInfo) PublicKey() []byte {
	return x.m.GetPublicKey()
}

// StringifyPublicKey returns HEX representation of PublicKey.
func StringifyPublicKey(node NodeInfo) string {
	return frostfscrypto.StringifyKeyBinary(node.PublicKey())
}

// SetNetworkEndpoints sets list to the announced node's network endpoints.
// Node MUSt have at least one announced endpoint. List MUST be unique.
// Endpoints are used for communication with the storage node within FrostFS
// network. It is expected that node serves storage node services on these
// endpoints (it also adds a wait on their network availability).
//
// Argument MUST NOT be mutated, make a copy first.
//
// See also IterateNetworkEndpoints.
func (x *NodeInfo) SetNetworkEndpoints(v ...string) {
	x.m.SetAddresses(v...)
}

// NumberOfNetworkEndpoints returns number of network endpoints announced by the node.
//
// See also SetNetworkEndpoints.
func (x NodeInfo) NumberOfNetworkEndpoints() int {
	return x.m.NumberOfAddresses()
}

// IterateNetworkEndpoints iterates over network endpoints announced by the
// node and pass them into f. Breaks iteration on f's true return. Handler
// MUST NOT be nil.
//
// Zero NodeInfo contains no endpoints which is incorrect according to
// FrostFS system requirements.
//
// See also SetNetworkEndpoints.
func (x NodeInfo) IterateNetworkEndpoints(f func(string) bool) {
	x.m.IterateAddresses(f)
}

// IterateNetworkEndpoints is an extra-sugared function over IterateNetworkEndpoints
// method which allows to unconditionally iterate over all node's network endpoints.
func IterateNetworkEndpoints(node NodeInfo, f func(string)) {
	node.IterateNetworkEndpoints(func(addr string) bool {
		f(addr)
		return false
	})
}

// assert NodeInfo type provides hrw.Hasher required for HRW sorting.
var _ hrw.Hasher = NodeInfo{}

// Hash implements hrw.Hasher interface.
//
// Hash is needed to support weighted HRW therefore sort function sorts nodes
// based on their public key. Hash isn't expected to be used directly.
func (x NodeInfo) Hash() uint64 {
	if x.hash != 0 {
		return x.hash
	}
	return hrw.Hash(x.m.GetPublicKey())
}

// less declares "less than" comparison between two NodeInfo instances:
// x1 is less than x2 if it has less Hash().
//
// Method is needed for internal placement needs.
func less(x1, x2 NodeInfo) bool {
	return x1.Hash() < x2.Hash()
}

func (x *NodeInfo) setNumericAttribute(key string, num uint64) {
	x.SetAttribute(key, strconv.FormatUint(num, 10))
}

// SetPrice sets the storage cost declared by the node. By default, zero
// price is announced.
func (x *NodeInfo) SetPrice(price uint64) {
	x.setNumericAttribute(attrPrice, price)
}

// Price returns price set using SetPrice.
//
// Zero NodeInfo has zero price.
func (x NodeInfo) Price() uint64 {
	val := x.Attribute(attrPrice)
	if val == "" {
		return 0
	}

	price, err := strconv.ParseUint(val, 10, 64)
	if err != nil {
		panic(fmt.Sprintf("unexpected price parsing error %s: %v", val, err))
	}

	return price
}

// SetCapacity sets the storage capacity declared by the node. By default, zero
// capacity is announced.
func (x *NodeInfo) SetCapacity(capacity uint64) {
	x.setNumericAttribute(attrCapacity, capacity)
}

// capacity returns capacity set using SetCapacity.
//
// Zero NodeInfo has zero capacity.
func (x NodeInfo) capacity() uint64 {
	val := x.Attribute(attrCapacity)
	if val == "" {
		return 0
	}

	capacity, err := strconv.ParseUint(val, 10, 64)
	if err != nil {
		panic(fmt.Sprintf("unexpected capacity parsing error %s: %v", val, err))
	}

	return capacity
}

const attrUNLOCODE = "UN-LOCODE"

// SetLOCODE specifies node's geographic location in UN/LOCODE format. Each
// storage node MUST declare it for entrance to the FrostFS network. Node MAY
// declare the code of the nearest location as needed, for example, when it is
// impossible to unambiguously attribute the node to any location from UN/LOCODE
// database.
//
// See also LOCODE.
func (x *NodeInfo) SetLOCODE(locode string) {
	x.SetAttribute(attrUNLOCODE, locode)
}

// LOCODE returns node's location code set using SetLOCODE.
//
// Zero NodeInfo has empty location code which is invalid according to
// FrostFS API system requirement.
func (x NodeInfo) LOCODE() string {
	return x.Attribute(attrUNLOCODE)
}

// SetCountryCode sets code of the country in ISO 3166-1_alpha-2 to which
// storage node belongs (or the closest one).
//
// SetCountryCode is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetCountryCode(countryCode string) {
	x.SetAttribute("CountryCode", countryCode)
}

// SetCountryName sets short name of the country in ISO-3166 format to which
// storage node belongs (or the closest one).
//
// SetCountryName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetCountryName(country string) {
	x.SetAttribute("Country", country)
}

// SetLocationName sets storage node's location name from "NameWoDiacritics"
// column in the UN/LOCODE record corresponding to the specified LOCODE.
//
// SetLocationName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetLocationName(location string) {
	x.SetAttribute("Location", location)
}

// SetSubdivisionCode sets storage node's subdivision code from "SubDiv" column in
// the UN/LOCODE record corresponding to the specified LOCODE.
//
// SetSubdivisionCode is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetSubdivisionCode(subDiv string) {
	x.SetAttribute("SubDivCode", subDiv)
}

// SetSubdivisionName sets storage node's subdivision name in ISO 3166-2 format.
//
// SetSubdivisionName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetSubdivisionName(subDiv string) {
	x.SetAttribute("SubDiv", subDiv)
}

// SetContinentName sets name of the storage node's continent from
// Seven-Continent model.
//
// SetContinentName is intended only for processing the network registration
// request by the Inner Ring. Other parties SHOULD NOT use it.
func (x *NodeInfo) SetContinentName(continent string) {
	x.SetAttribute("Continent", continent)
}

// Enumeration of well-known attributes.
const (
	// attrPrice is a key to the node attribute that indicates the
	// price in GAS tokens for storing one GB of data during one Epoch.
	attrPrice = "Price"

	// attrCapacity is a key to the node attribute that indicates the
	// total available disk space in Gigabytes.
	attrCapacity = "Capacity"

	// attrExternalAddr is a key for the attribute storing node external addresses.
	attrExternalAddr = "ExternalAddr"
	// sepExternalAddr is a separator for multi-value ExternalAddr attribute.
	sepExternalAddr = ","
)

// SetExternalAddresses sets multi-addresses to use
// to connect to this node from outside.
//
// Panics if addr is an empty list.
func (x *NodeInfo) SetExternalAddresses(addr ...string) {
	x.SetAttribute(attrExternalAddr, strings.Join(addr, sepExternalAddr))
}

// ExternalAddresses returns list of multi-addresses to use
// to connect to this node from outside.
func (x NodeInfo) ExternalAddresses() []string {
	a := x.Attribute(attrExternalAddr)
	if len(a) == 0 {
		return nil
	}

	return strings.Split(a, sepExternalAddr)
}

// NumberOfAttributes returns number of attributes announced by the node.
//
// See also SetAttribute.
func (x NodeInfo) NumberOfAttributes() int {
	return len(x.m.GetAttributes())
}

// IterateAttributes iterates over all node attributes and passes the into f.
// Handler MUST NOT be nil.
func (x NodeInfo) IterateAttributes(f func(key, value string)) {
	a := x.m.GetAttributes()
	for i := range a {
		f(a[i].GetKey(), a[i].GetValue())
	}
}

// SetAttribute sets value of the node attribute value by the given key.
// Both key and value MUST NOT be empty.
func (x *NodeInfo) SetAttribute(key, value string) {
	if key == "" {
		panic("empty key in SetAttribute")
	} else if value == "" {
		panic("empty value in SetAttribute")
	}

	a := x.m.GetAttributes()
	for i := range a {
		if a[i].GetKey() == key {
			a[i].SetValue(value)
			return
		}
	}

	a = append(a, netmap.Attribute{})
	a[len(a)-1].SetKey(key)
	a[len(a)-1].SetValue(value)

	x.m.SetAttributes(a)
}

// Attribute returns value of the node attribute set using SetAttribute by the
// given key. Returns empty string if attribute is missing.
func (x NodeInfo) Attribute(key string) string {
	a := x.m.GetAttributes()
	for i := range a {
		if a[i].GetKey() == key {
			return a[i].GetValue()
		}
	}

	return ""
}

// SortAttributes sorts node attributes set using SetAttribute lexicographically.
// The method is only needed to make NodeInfo consistent, e.g. for signing.
func (x *NodeInfo) SortAttributes() {
	as := x.m.GetAttributes()
	if len(as) == 0 {
		return
	}

	sort.Slice(as, func(i, j int) bool {
		switch strings.Compare(as[i].GetKey(), as[j].GetKey()) {
		case -1:
			return true
		case 1:
			return false
		default:
			return as[i].GetValue() < as[j].GetValue()
		}
	})

	x.m.SetAttributes(as)
}

// SetOffline sets the state of the node to "offline". When a node updates
// information about itself in the network map, this action is interpreted as
// an intention to leave the network.
func (x *NodeInfo) SetOffline() {
	x.m.SetState(netmap.Offline)
}

// IsOffline checks if the node is in the "offline" state.
//
// Zero NodeInfo has undefined state which is not offline (note that it does not
// mean online).
//
// See also SetOffline.
func (x NodeInfo) IsOffline() bool {
	return x.m.GetState() == netmap.Offline
}

// SetOnline sets the state of the node to "online". When a node updates
// information about itself in the network map, this
// action is interpreted as an intention to enter the network.
//
// See also IsOnline.
func (x *NodeInfo) SetOnline() {
	x.m.SetState(netmap.Online)
}

// IsOnline checks if the node is in the "online" state.
//
// Zero NodeInfo has undefined state which is not online (note that it does not
// mean offline).
//
// See also SetOnline.
func (x NodeInfo) IsOnline() bool {
	return x.m.GetState() == netmap.Online
}

// SetMaintenance sets the state of the node to "maintenance". When a node updates
// information about itself in the network map, this
// state declares temporal unavailability for a node.
//
// See also IsMaintenance.
func (x *NodeInfo) SetMaintenance() {
	x.m.SetState(netmap.Maintenance)
}

// IsMaintenance checks if the node is in the "maintenance" state.
//
// Zero NodeInfo has undefined state.
//
// See also SetMaintenance.
func (x NodeInfo) IsMaintenance() bool {
	return x.m.GetState() == netmap.Maintenance
}