// Package k8snet simulates the two Kubernetes-networking behaviors that
// actually cause gateway incidents: the drain / EndpointSlice
// propagation race, and conntrack-table exhaustion. You can't run a real
// cluster in a unit test, so these are faithful deterministic models of
// the mechanics — the kind you must reason about on call.
package k8snet

// SimulateDrain models removing a pod from a Service fronted by several
// kube-proxies, each of which adopts the EndpointSlice update after its
// own propagation delay (eventual consistency across the fleet).
//
// readinessFirst chooses the strategy:
//   - false (WRONG): the pod exits immediately, but kube-proxies keep
//     routing to it until they observe the removal -> those requests are
//     dropped (the classic "every deploy drops a few requests" bug).
//   - true  (RIGHT): readiness fails first so removal starts
//     propagating, the pod KEEPS SERVING during the grace period, and it
//     only exits after every proxy has stopped routing to it -> zero
//     drops.
//
// proxyDelays[i] is how many ticks proxy i takes to observe the removal;
// reqPerProxyPerTick is the traffic each still-pointing proxy sends to
// the pod each tick; graceTicks is the termination grace period.
func SimulateDrain(readinessFirst bool, proxyDelays []int, reqPerProxyPerTick, graceTicks int) int {
	if readinessFirst {
		// The pod serves until every proxy has removed it (bounded by the
		// grace period). As long as grace covers the max propagation
		// delay, nothing is dropped.
		maxDelay := 0
		for _, d := range proxyDelays {
			if d > maxDelay {
				maxDelay = d
			}
		}
		if graceTicks < maxDelay {
			// Grace too short: the pod is SIGKILLed mid-propagation; the
			// proxies that hadn't removed it yet now drop.
			dropped := 0
			for _, d := range proxyDelays {
				if d > graceTicks {
					dropped += (d - graceTicks) * reqPerProxyPerTick
				}
			}
			return dropped
		}
		return 0
	}

	// WRONG: pod stops serving at t0; each proxy keeps routing to it for
	// its propagation delay -> those requests are dropped.
	dropped := 0
	for _, d := range proxyDelays {
		dropped += d * reqPerProxyPerTick
	}
	return dropped
}

// EndpointSlice models the sharded membership list behind a Service. The
// real object caps endpoints per slice (~100) so a large Service doesn't
// ship one giant object on every change.
type EndpointSlice struct {
	Endpoints []string
	MaxPerSlice int
}

// Shard splits endpoints into slices of at most MaxPerSlice — the
// scalability fix over the old monolithic Endpoints object.
func Shard(endpoints []string, maxPerSlice int) [][]string {
	if maxPerSlice <= 0 {
		maxPerSlice = 100
	}
	var out [][]string
	for i := 0; i < len(endpoints); i += maxPerSlice {
		end := i + maxPerSlice
		if end > len(endpoints) {
			end = len(endpoints)
		}
		out = append(out, endpoints[i:end])
	}
	return out
}
