// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package maps

import (
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"

	"github.com/cilium/cilium/pkg/byteorder"
	"github.com/cilium/cilium/pkg/clustermesh/types"
	"github.com/cilium/cilium/pkg/loadbalancer"
)

// MapDump is a dump of a BPF map. These are generated by the dump() method, which
// solely defines the format.
type MapDump = string

type numeric interface {
	~int | ~uint32 | ~uint16
}

// TODO: Figure out what to do about the IDs. If we want to do fault inject the
// operations will be retried and the ID allocations are non-deterministic.
func sanitizeID[Num numeric](n Num, sanitize bool) string {
	if !sanitize {
		return strconv.FormatInt(int64(n), 10)
	}
	if n == 0 {
		return "<zero>"
	}
	return "<non-zero>"
}

// DumpLBMaps the load-balancing maps into a concise format for assertions in tests.
func DumpLBMaps(lbmaps LBMaps, sanitizeIDs bool, customizeAddr func(types.AddrCluster, uint16) string) (out []MapDump) {
	out = []string{}

	if customizeAddr == nil {
		customizeAddr = func(addr types.AddrCluster, port uint16) (s string) {
			s = addr.String()
			if !addr.Is4() {
				s = "[" + s + "]"
			}
			return fmt.Sprintf("%s:%d", s, port)
		}
	}

	// BackendID corresponds to a union type in bpf/lib/common.h, and we stringify it differently depending on the case of the union.
	stringFromBackendID := func(svcValue ServiceValue, slot int) string {
		if slot != 0 {
			return fmt.Sprintf("BEID=%s", sanitizeID(svcValue.GetBackendID(), sanitizeIDs))
		}
		if loadbalancer.ServiceFlags(svcValue.GetFlags()).IsL7LB() {
			return fmt.Sprintf("L7Proxy=%d", svcValue.GetL7LBProxyPort())
		}
		return fmt.Sprintf("LBALG=%s AFFTimeout=%d",
			loadbalancer.SVCLoadBalancingAlgorithm(svcValue.GetLbAlg()).String(),
			svcValue.GetSessionAffinityTimeoutSec(),
		)
	}

	svcCB := func(svcKey ServiceKey, svcValue ServiceValue) {
		svcKey = svcKey.ToHost()
		svcValue = svcValue.ToHost()
		addr := types.MustAddrClusterFromIP(svcKey.GetAddress())
		addrS := customizeAddr(addr, svcKey.GetPort())
		addrS += "/" + loadbalancer.NewL4TypeFromNumber(svcKey.GetProtocol())
		if svcKey.GetScope() == loadbalancer.ScopeInternal {
			addrS += "/i"
		}
		out = append(out, fmt.Sprintf("SVC: ID=%s ADDR=%s SLOT=%d %s COUNT=%d QCOUNT=%d FLAGS=%s",
			sanitizeID(svcValue.GetRevNat(), sanitizeIDs),
			addrS,
			svcKey.GetBackendSlot(),
			stringFromBackendID(svcValue, svcKey.GetBackendSlot()),
			svcValue.GetCount(),
			svcValue.GetQCount(),
			strings.ReplaceAll(
				loadbalancer.ServiceFlags(svcValue.GetFlags()).String(),
				", ", "+"),
		))
	}
	if err := lbmaps.DumpService(svcCB); err != nil {
		panic(err)
	}

	beCB := func(beKey BackendKey, beValue BackendValue) {
		beValue = beValue.ToHost()
		addr := beValue.GetAddress()
		addrS := customizeAddr(addr, beValue.GetPort())
		addrS += "/" + loadbalancer.NewL4TypeFromNumber(beValue.GetProtocol())
		stateS, _ := loadbalancer.GetBackendStateFromFlags(beValue.GetFlags()).String()
		out = append(out, fmt.Sprintf("BE: ID=%s ADDR=%s STATE=%s",
			sanitizeID(beKey.GetID(), sanitizeIDs),
			addrS,
			stateS,
		))
	}
	if err := lbmaps.DumpBackend(beCB); err != nil {
		panic(err)
	}

	revCB := func(revKey RevNatKey, revValue RevNatValue) {
		revKey = revKey.ToHost()
		revValue = revValue.ToHost()

		var addr string

		switch v := revValue.(type) {
		case *RevNat4Value:
			addr = customizeAddr(types.MustAddrClusterFromIP(v.Address.IP()), v.Port)

		case *RevNat6Value:
			addr = customizeAddr(types.MustAddrClusterFromIP(v.Address.IP()), v.Port)
		}

		out = append(out, fmt.Sprintf("REV: ID=%s ADDR=%s",
			sanitizeID(revKey.GetKey(), sanitizeIDs),
			addr,
		))
	}
	if err := lbmaps.DumpRevNat(revCB); err != nil {
		panic(err)
	}

	affCB := func(affKey *AffinityMatchKey, _ *AffinityMatchValue) {
		affKey = affKey.ToHost()
		out = append(out, fmt.Sprintf("AFF: ID=%s BEID=%d",
			sanitizeID(affKey.RevNATID, sanitizeIDs),
			affKey.BackendID,
		))
	}

	if err := lbmaps.DumpAffinityMatch(affCB); err != nil {
		panic(err)
	}

	srcRangeCB := func(key SourceRangeKey, _ *SourceRangeValue) {
		key = key.ToHost()
		out = append(out, fmt.Sprintf("SRCRANGE: ID=%s CIDR=%s",
			sanitizeID(key.GetRevNATID(), sanitizeIDs),
			key.GetCIDR(),
		))
	}
	if err := lbmaps.DumpSourceRange(srcRangeCB); err != nil {
		panic(err)
	}

	maglevCB := func(key MaglevOuterKey, _ MaglevOuterVal, _ MaglevInnerKey, innerValue *MaglevInnerVal, _ bool) {
		key = MaglevOuterKey{
			RevNatID: byteorder.NetworkToHost16(key.RevNatID),
		}
		idCounts := make(map[loadbalancer.BackendID]int)
		for _, backend := range innerValue.BackendIDs {
			idCounts[backend]++
		}
		type idWithCount struct {
			loadbalancer.BackendID
			count int
		}
		var compactIDs []idWithCount
		for id, count := range idCounts {
			compactIDs = append(compactIDs, idWithCount{id, count})
		}
		slices.SortFunc(compactIDs, func(a, b idWithCount) int {
			diff := a.count - b.count
			if diff != 0 {
				return -diff // Descending order by counts
			}
			return int(a.BackendID) - int(b.BackendID)
		})
		var ids []string
		for _, idWithCount := range compactIDs {
			ids = append(ids, fmt.Sprintf("%s(%d)", sanitizeID(idWithCount.BackendID, sanitizeIDs), idWithCount.count))
		}
		out = append(out, fmt.Sprintf("MAGLEV: ID=%s INNER=[%s]",
			sanitizeID(byteorder.HostToNetwork16(key.RevNatID), sanitizeIDs),
			strings.Join(ids, ", "),
		))
	}
	if err := lbmaps.DumpMaglev(maglevCB); err != nil {
		panic(err)
	}

	sort.Strings(out)
	return
}
