// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package metadata

import (
	"fmt"
	"net"
	"regexp"
	"strconv"
	"strings"

	"github.com/hashicorp/go-version"
	"github.com/hashicorp/serf/serf"
)

// Key is used in maps and for equality tests.  A key is based on endpoints.
type Key struct {
	name string
}

// Equal compares two Key objects
func (k *Key) Equal(x *Key) bool {
	return k.name == x.name
}

// Server is used to return details of a consul server
type Server struct {
	Name                string // <node>.<dc>
	ShortName           string // <node>
	ID                  string
	Datacenter          string
	Segment             string
	Port                int
	SegmentAddrs        map[string]string
	SegmentPorts        map[string]int
	WanJoinPort         int
	LanJoinPort         int
	ExternalGRPCPort    int
	ExternalGRPCTLSPort int
	Bootstrap           bool
	Expect              int
	Build               version.Version
	Version             int
	RaftVersion         int
	Addr                net.Addr
	Status              serf.MemberStatus
	ReadReplica         bool
	FeatureFlags        map[string]int

	// If true, use TLS when connecting to this server
	UseTLS bool
}

// Key returns the corresponding Key
func (s *Server) Key() *Key {
	return &Key{
		name: s.Name,
	}
}

// String returns a string representation of Server
func (s *Server) String() string {
	var addrStr, networkStr string
	if s.Addr != nil {
		addrStr = s.Addr.String()
		networkStr = s.Addr.Network()
	}

	return fmt.Sprintf("%s (Addr: %s/%s) (DC: %s)", s.Name, networkStr, addrStr, s.Datacenter)
}

var versionFormat = regexp.MustCompile(`\d+\.\d+\.\d+`)

// IsConsulServer returns true if a serf member is a consul server
// agent. Returns a bool and a pointer to the Server.
func IsConsulServer(m serf.Member) (bool, *Server) {
	if m.Tags["role"] != "consul" {
		return false, nil
	}

	datacenter := m.Tags["dc"]
	segment := m.Tags["segment"]
	_, bootstrap := m.Tags["bootstrap"]
	_, useTLS := m.Tags["use_tls"]

	expect := 0
	expectStr, ok := m.Tags["expect"]
	var err error
	if ok {
		expect, err = strconv.Atoi(expectStr)
		if err != nil {
			return false, nil
		}
	}

	portStr := m.Tags["port"]
	port, err := strconv.Atoi(portStr)
	if err != nil {
		return false, nil
	}

	segmentAddrs := make(map[string]string)
	segmentPorts := make(map[string]int)
	featureFlags := make(map[string]int)
	for name, value := range m.Tags {
		if strings.HasPrefix(name, "sl_") {
			addr, port, err := net.SplitHostPort(value)
			if err != nil {
				return false, nil
			}
			segmentPort, err := strconv.Atoi(port)
			if err != nil {
				return false, nil
			}

			segmentName := strings.TrimPrefix(name, "sl_")
			segmentAddrs[segmentName] = addr
			segmentPorts[segmentName] = segmentPort
		} else if strings.HasPrefix(name, featureFlagPrefix) {
			featureName := strings.TrimPrefix(name, featureFlagPrefix)
			featureState, err := strconv.Atoi(value)
			if err != nil {
				return false, nil
			}
			featureFlags[featureName] = featureState
		}
	}

	buildVersion, err := Build(&m)
	if err != nil {
		return false, nil
	}

	wanJoinPort := 0
	wanJoinPortStr, ok := m.Tags["wan_join_port"]
	if ok {
		wanJoinPort, err = strconv.Atoi(wanJoinPortStr)
		if err != nil {
			return false, nil
		}
	}

	var externalGRPCPort, externalGRPCTLSPort int
	externalGRPCPortStr, foundGRPC := m.Tags["grpc_port"]
	externalGRPCTLSPortStr, foundGRPCTLS := m.Tags["grpc_tls_port"]
	if foundGRPC {
		externalGRPCPort, _ = strconv.Atoi(externalGRPCPortStr)
	}
	if foundGRPCTLS {
		externalGRPCTLSPort, _ = strconv.Atoi(externalGRPCTLSPortStr)
	}
	// If either port tag was found, check to ensure that at least one port was valid.
	if foundGRPC || foundGRPCTLS {
		if externalGRPCPort < 1 && externalGRPCTLSPort < 1 {
			return false, nil
		}
	}

	vsnStr := m.Tags["vsn"]
	vsn, err := strconv.Atoi(vsnStr)
	if err != nil {
		return false, nil
	}

	raftVsn := 0
	raftVsnStr, ok := m.Tags["raft_vsn"]
	if ok {
		raftVsn, err = strconv.Atoi(raftVsnStr)
		if err != nil {
			return false, nil
		}
	}

	// Check if the server is a non voter
	// DEPRECATED - remove looking for the nonvoter tag eventually once we don't have to support
	// read replicas running v1.8.x and below.
	_, nonVoter := m.Tags["nonvoter"]
	_, readReplica := m.Tags["read_replica"]

	addr := &net.TCPAddr{IP: m.Addr, Port: port}

	parts := &Server{
		Name:                m.Name,
		ShortName:           strings.TrimSuffix(m.Name, "."+datacenter),
		ID:                  m.Tags["id"],
		Datacenter:          datacenter,
		Segment:             segment,
		Port:                port,
		SegmentAddrs:        segmentAddrs,
		SegmentPorts:        segmentPorts,
		WanJoinPort:         wanJoinPort,
		LanJoinPort:         int(m.Port),
		ExternalGRPCPort:    externalGRPCPort,
		ExternalGRPCTLSPort: externalGRPCTLSPort,
		Bootstrap:           bootstrap,
		Expect:              expect,
		Addr:                addr,
		Build:               *buildVersion,
		Version:             vsn,
		RaftVersion:         raftVsn,
		Status:              m.Status,
		UseTLS:              useTLS,
		// DEPRECATED - remove nonVoter check once support for that tag is removed
		ReadReplica:  nonVoter || readReplica,
		FeatureFlags: featureFlags,
	}
	return true, parts
}

// TODO(ACL-Legacy-Compat): remove in phase 2
const TagACLs = "acls"

const featureFlagPrefix = "ft_"

// AddFeatureFlags to the tags. The tags map is expected to be a serf.Config.Tags.
// The feature flags are encoded in the tags so that IsConsulServer can decode them
// and populate the Server.FeatureFlags map.
func AddFeatureFlags(tags map[string]string, flags ...string) {
	for _, flag := range flags {
		tags[featureFlagPrefix+flag] = "1"
	}
}

func GetIP(addr net.Addr) []byte {
	switch a := addr.(type) {
	case *net.UDPAddr:
		return []byte(a.IP.String())
	case *net.TCPAddr:
		return []byte(a.IP.String())
	}
	return []byte{}
}
