// Package xds is a teaching-sized model of an Envoy xDS control plane:
// versioned snapshots of listeners/routes/clusters/endpoints, a snapshot
// cache keyed by node, the ACK/NACK + version state machine, and a
// level-triggered reconcile loop with content-hash versioning. It models
// the *protocol mechanics* of go-control-plane in stdlib so they are
// legible; production = envoyproxy/go-control-plane driving real Envoy.
package xds

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"sort"
)

// ResourceType is one of the xDS resource families.
type ResourceType string

const (
	TypeListener ResourceType = "LDS"
	TypeRoute    ResourceType = "RDS"
	TypeCluster  ResourceType = "CDS"
	TypeEndpoint ResourceType = "EDS"
)

// Resource is one config object.
type Resource interface {
	Name() string
	Type() ResourceType
	fingerprint() string // canonical encoding for content-hash versioning
}

// Listener binds a port to a route config.
type Listener struct {
	ListenerName string
	Port         int
	RouteName    string
}

func (l Listener) Name() string         { return l.ListenerName }
func (l Listener) Type() ResourceType   { return TypeListener }
func (l Listener) fingerprint() string  { return fmt.Sprintf("L:%s:%d->%s", l.ListenerName, l.Port, l.RouteName) }

// Route maps a path prefix to a cluster.
type Route struct {
	RouteName string
	Prefix    string
	Cluster   string
}

func (r Route) Name() string        { return r.RouteName }
func (r Route) Type() ResourceType  { return TypeRoute }
func (r Route) fingerprint() string { return fmt.Sprintf("R:%s:%s->%s", r.RouteName, r.Prefix, r.Cluster) }

// Cluster is a logical upstream; its endpoints come via EDS.
type Cluster struct {
	ClusterName string
	LbPolicy    string
}

func (c Cluster) Name() string        { return c.ClusterName }
func (c Cluster) Type() ResourceType  { return TypeCluster }
func (c Cluster) fingerprint() string { return fmt.Sprintf("C:%s:%s", c.ClusterName, c.LbPolicy) }

// Endpoints is the membership of a cluster (the gw-04 subset source).
type Endpoints struct {
	ClusterName string
	Addrs       []string
}

func (e Endpoints) Name() string       { return e.ClusterName }
func (e Endpoints) Type() ResourceType { return TypeEndpoint }
func (e Endpoints) fingerprint() string {
	a := append([]string(nil), e.Addrs...)
	sort.Strings(a)
	return fmt.Sprintf("E:%s:%v", e.ClusterName, a)
}

// Snapshot is a consistent set of resources at a Version, pushed
// atomically. Envoy never sees a half-applied snapshot.
type Snapshot struct {
	Version   string
	Resources map[ResourceType][]Resource
}

func NewSnapshot() *Snapshot {
	return &Snapshot{Resources: map[ResourceType][]Resource{}}
}

func (s *Snapshot) Add(rs ...Resource) {
	for _, r := range rs {
		s.Resources[r.Type()] = append(s.Resources[r.Type()], r)
	}
}

// Consistent enforces cross-resource ordering invariants — the reason ADS
// exists: routes must reference defined clusters, listeners must
// reference defined routes. Pushing endpoints for an undefined cluster
// (or a route to an undefined cluster) would make Envoy drop traffic.
func (s *Snapshot) Consistent() error {
	clusters := map[string]bool{}
	for _, r := range s.Resources[TypeCluster] {
		clusters[r.Name()] = true
	}
	routes := map[string]bool{}
	for _, r := range s.Resources[TypeRoute] {
		rt := r.(Route)
		if !clusters[rt.Cluster] {
			return fmt.Errorf("xds: route %q references undefined cluster %q", rt.RouteName, rt.Cluster)
		}
		routes[rt.RouteName] = true
	}
	for _, r := range s.Resources[TypeListener] {
		ls := r.(Listener)
		if !routes[ls.RouteName] {
			return fmt.Errorf("xds: listener %q references undefined route %q", ls.ListenerName, ls.RouteName)
		}
	}
	for _, r := range s.Resources[TypeEndpoint] {
		ep := r.(Endpoints)
		if !clusters[ep.ClusterName] {
			return fmt.Errorf("xds: endpoints for undefined cluster %q", ep.ClusterName)
		}
	}
	return nil
}

// Fingerprint is a content hash of the snapshot's resources (order-
// independent). Identical desired state ⇒ identical fingerprint ⇒ no
// spurious push (debounce), and idempotent reconciles across restarts.
func (s *Snapshot) Fingerprint() string {
	var parts []string
	for _, rs := range s.Resources {
		for _, r := range rs {
			parts = append(parts, r.fingerprint())
		}
	}
	sort.Strings(parts)
	h := sha256.New()
	for _, p := range parts {
		h.Write([]byte(p))
		h.Write([]byte{0})
	}
	return hex.EncodeToString(h.Sum(nil))[:12]
}
