package config

import (
	"fmt"
	"net"
	"sort"
	"strconv"
	"strings"

	"github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig"
)

// ApplicationConfiguration config specific to the node.
type ApplicationConfiguration struct {
	Ledger `yaml:",inline"`

	// Deprecated: please, use Addresses field of P2P section instead, this field will be removed in future versions.
	Address *string `yaml:"Address,omitempty"`
	// Deprecated: please, use Addresses field of P2P section instead, this field will be removed in future versions.
	AnnouncedNodePort *uint16 `yaml:"AnnouncedPort,omitempty"`
	// Deprecated: this option is moved to the P2P section.
	AttemptConnPeers int `yaml:"AttemptConnPeers"`
	// BroadcastFactor is the factor (0-100) controlling gossip fan-out number optimization.
	//
	// Deprecated: this option is moved to the P2P section.
	BroadcastFactor int                      `yaml:"BroadcastFactor"`
	DBConfiguration dbconfig.DBConfiguration `yaml:"DBConfiguration"`
	// Deprecated: this option is moved to the P2P section.
	DialTimeout int64  `yaml:"DialTimeout"`
	LogLevel    string `yaml:"LogLevel"`
	LogPath     string `yaml:"LogPath"`
	// Deprecated: this option is moved to the P2P section.
	MaxPeers int `yaml:"MaxPeers"`
	// Deprecated: this option is moved to the P2P section.
	MinPeers int `yaml:"MinPeers"`
	// Deprecated: please, use Addresses field of P2P section instead, this field will be removed in future versions.
	NodePort *uint16 `yaml:"NodePort,omitempty"`
	P2P      P2P     `yaml:"P2P"`
	// Deprecated: this option is moved to the P2P section.
	PingInterval int64 `yaml:"PingInterval"`
	// Deprecated: this option is moved to the P2P section.
	PingTimeout int64        `yaml:"PingTimeout"`
	Pprof       BasicService `yaml:"Pprof"`
	Prometheus  BasicService `yaml:"Prometheus"`
	// Deprecated: this option is moved to the P2P section.
	ProtoTickInterval int64               `yaml:"ProtoTickInterval"`
	Relay             bool                `yaml:"Relay"`
	Consensus         Consensus           `yaml:"Consensus"`
	RPC               RPC                 `yaml:"RPC"`
	UnlockWallet      Wallet              `yaml:"UnlockWallet"`
	Oracle            OracleConfiguration `yaml:"Oracle"`
	P2PNotary         P2PNotary           `yaml:"P2PNotary"`
	StateRoot         StateRoot           `yaml:"StateRoot"`
	// ExtensiblePoolSize is the maximum amount of the extensible payloads from a single sender.
	//
	// Deprecated: this option is moved to the P2P section.
	ExtensiblePoolSize int `yaml:"ExtensiblePoolSize"`
}

// EqualsButServices returns true when the o is the same as a except for services
// (Oracle, P2PNotary, Pprof, Prometheus, RPC, StateRoot and UnlockWallet sections)
// and LogLevel field.
func (a *ApplicationConfiguration) EqualsButServices(o *ApplicationConfiguration) bool {
	if len(a.P2P.Addresses) != len(o.P2P.Addresses) {
		return false
	}
	aCp := make([]string, len(a.P2P.Addresses))
	oCp := make([]string, len(o.P2P.Addresses))
	copy(aCp, a.P2P.Addresses)
	copy(oCp, o.P2P.Addresses)
	sort.Strings(aCp)
	sort.Strings(oCp)
	for i := range aCp {
		if aCp[i] != oCp[i] {
			return false
		}
	}
	if a.Address != o.Address || //nolint:staticcheck // SA1019: a.Address is deprecated
		a.AnnouncedNodePort != o.AnnouncedNodePort || //nolint:staticcheck // SA1019: a.AnnouncedNodePort is deprecated
		a.AttemptConnPeers != o.AttemptConnPeers || //nolint:staticcheck // SA1019: a.AttemptConnPeers is deprecated
		a.P2P.AttemptConnPeers != o.P2P.AttemptConnPeers ||
		a.BroadcastFactor != o.BroadcastFactor || //nolint:staticcheck // SA1019: a.BroadcastFactor is deprecated
		a.P2P.BroadcastFactor != o.P2P.BroadcastFactor ||
		a.DBConfiguration != o.DBConfiguration ||
		a.DialTimeout != o.DialTimeout || //nolint:staticcheck // SA1019: a.DialTimeout is deprecated
		a.P2P.DialTimeout != o.P2P.DialTimeout ||
		a.ExtensiblePoolSize != o.ExtensiblePoolSize || //nolint:staticcheck // SA1019: a.ExtensiblePoolSize is deprecated
		a.P2P.ExtensiblePoolSize != o.P2P.ExtensiblePoolSize ||
		a.LogPath != o.LogPath ||
		a.MaxPeers != o.MaxPeers || //nolint:staticcheck // SA1019: a.MaxPeers is deprecated
		a.P2P.MaxPeers != o.P2P.MaxPeers ||
		a.MinPeers != o.MinPeers || //nolint:staticcheck // SA1019: a.MinPeers is deprecated
		a.P2P.MinPeers != o.P2P.MinPeers ||
		a.NodePort != o.NodePort || //nolint:staticcheck // SA1019: a.NodePort is deprecated
		a.PingInterval != o.PingInterval || //nolint:staticcheck // SA1019: a.PingInterval is deprecated
		a.P2P.PingInterval != o.P2P.PingInterval ||
		a.PingTimeout != o.PingTimeout || //nolint:staticcheck // SA1019: a.PingTimeout is deprecated
		a.P2P.PingTimeout != o.P2P.PingTimeout ||
		a.ProtoTickInterval != o.ProtoTickInterval || //nolint:staticcheck // SA1019: a.ProtoTickInterval is deprecated
		a.P2P.ProtoTickInterval != o.P2P.ProtoTickInterval ||
		a.Relay != o.Relay {
		return false
	}
	return true
}

// AnnounceableAddress is a pair of node address in the form of "[host]:[port]"
// with optional corresponding announced port to be used in version exchange.
type AnnounceableAddress struct {
	Address       string
	AnnouncedPort uint16
}

// GetAddresses parses returns the list of AnnounceableAddress containing information
// gathered from both deprecated Address / NodePort / AnnouncedNodePort and newly
// created Addresses fields.
func (a *ApplicationConfiguration) GetAddresses() ([]AnnounceableAddress, error) {
	addrs := make([]AnnounceableAddress, 0, len(a.P2P.Addresses)+1)
	if a.Address != nil || a.NodePort != nil || a.AnnouncedNodePort != nil { //nolint:staticcheck // SA1019: a.Address is deprecated
		var (
			host     string
			nodePort uint16
		)
		if a.Address != nil { //nolint:staticcheck // SA1019: a.Address is deprecated
			host = *a.Address //nolint:staticcheck // SA1019: a.Address is deprecated
		}
		if a.NodePort != nil { //nolint:staticcheck // SA1019: a.NodePort is deprecated
			nodePort = *a.NodePort //nolint:staticcheck // SA1019: a.NodePort is deprecated
		}
		addr := AnnounceableAddress{Address: net.JoinHostPort(host, strconv.Itoa(int(nodePort)))}
		if a.AnnouncedNodePort != nil { //nolint:staticcheck // SA1019: a.AnnouncedNodePort is deprecated
			addr.AnnouncedPort = *a.AnnouncedNodePort //nolint:staticcheck // SA1019: a.AnnouncedNodePort is deprecated
		}
		addrs = append(addrs, addr)
	}
	for i, addrStr := range a.P2P.Addresses {
		if len(addrStr) == 0 {
			return nil, fmt.Errorf("address #%d is empty", i)
		}
		lastCln := strings.LastIndex(addrStr, ":")
		if lastCln == -1 {
			addrs = append(addrs, AnnounceableAddress{
				Address: addrStr, // Plain IPv4 address without port.
			})
			continue
		}
		lastPort, err := strconv.ParseUint(addrStr[lastCln+1:], 10, 16)
		if err != nil {
			addrs = append(addrs, AnnounceableAddress{
				Address: addrStr, // Still may be a valid IPv4 of the form "X.Y.Z.Q:" or plain IPv6 "A:B::", keep it.
			})
			continue
		}
		penultimateCln := strings.LastIndex(addrStr[:lastCln], ":")
		if penultimateCln == -1 {
			addrs = append(addrs, AnnounceableAddress{
				Address: addrStr, // IPv4 address with port "X.Y.Z.Q:123"
			})
			continue
		}
		isV6 := strings.Count(addrStr, ":") > 2
		hasBracket := strings.Contains(addrStr, "]")
		if penultimateCln == lastCln-1 {
			if isV6 && !hasBracket {
				addrs = append(addrs, AnnounceableAddress{
					Address: addrStr, // Plain IPv6 of the form "A:B::123"
				})
			} else {
				addrs = append(addrs, AnnounceableAddress{
					Address:       addrStr[:lastCln], // IPv4 with empty port and non-empty announced port "X.Y.Z.Q::123" or IPv6 with non-empty announced port "[A:B::]::123".
					AnnouncedPort: uint16(lastPort),
				})
			}
			continue
		}
		_, err = strconv.ParseUint(addrStr[penultimateCln+1:lastCln], 10, 16)
		if err != nil {
			if isV6 {
				addrs = append(addrs, AnnounceableAddress{
					Address: addrStr, // Still may be a valid plain IPv6 of the form "A::B:123" or IPv6 with single port [A:B::]:123, keep it.
				})
				continue
			}
			return nil, fmt.Errorf("failed to parse port from %s: %w", addrStr, err) // Some garbage.
		}
		if isV6 && !hasBracket {
			addrs = append(addrs, AnnounceableAddress{
				Address: addrStr, // Plain IPv6 of the form "A::1:1"
			})
		} else {
			addrs = append(addrs, AnnounceableAddress{
				Address:       addrStr[:lastCln], // IPv4 with both ports or IPv6 with both ports specified.
				AnnouncedPort: uint16(lastPort),
			})
		}
	}
	if len(addrs) == 0 {
		addrs = append(addrs, AnnounceableAddress{
			Address: ":0",
		})
	}
	return addrs, nil
}