// Copyright 2020-2026 Project Capsule Authors
// SPDX-License-Identifier: Apache-2.0

package resourcepools

import (
	"context"
	"fmt"
	"sort"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/events"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/api/meta"
	evt "github.com/projectcapsule/capsule/pkg/runtime/events"
)

// Update the Status of a claim and emit an event if Status changed.
func updateStatusAndEmitEvent(
	ctx context.Context,
	c client.Client,
	recorder events.EventRecorder,
	claim *capsulev1beta2.ResourcePoolClaim,
	pool *capsulev1beta2.ResourcePool,
	condition meta.Condition,
) (err error) {
	updated := claim.Status.Conditions.UpdateConditionByTypeWithStatus(condition)

	if !updated {
		return nil
	}

	err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
		current := &capsulev1beta2.ResourcePoolClaim{}
		if err := c.Get(ctx, client.ObjectKeyFromObject(claim), current); err != nil {
			return fmt.Errorf("failed to refetch instance before update: %w", err)
		}

		current.Status.Conditions = claim.Status.Conditions

		return c.Status().Update(ctx, current)
	})
	if err != nil {
		return err
	}

	eventType := corev1.EventTypeNormal
	if condition.Status == metav1.ConditionFalse {
		eventType = corev1.EventTypeWarning
	}

	recorder.Eventf(
		claim,
		pool,
		eventType,
		condition.Reason,
		evt.ActionReconciled,
		condition.Message,
	)

	return err
}

func filterResourceListByKeys(in corev1.ResourceList, keys corev1.ResourceList) corev1.ResourceList {
	out := corev1.ResourceList{}

	for k := range keys {
		if v, ok := in[k]; ok {
			out[k] = v
		}
	}

	return out
}

func selectClaimsCoveringUsageGreedy(
	used corev1.ResourceList,
	claims []capsulev1beta2.ResourcePoolClaim,
) map[string]struct{} {
	selected := map[string]struct{}{}

	if resourceListAllZero(used) || len(claims) == 0 {
		return selected
	}

	// Stable deterministic order for ties (creation ts, name, namespace)
	sort.Slice(claims, func(i, j int) bool {
		a, b := claims[i], claims[j]
		if !a.CreationTimestamp.Equal(&b.CreationTimestamp) {
			return a.CreationTimestamp.Before(&b.CreationTimestamp)
		}

		if a.Name != b.Name {
			return a.Name < b.Name
		}

		return a.Namespace < b.Namespace
	})

	remaining := used.DeepCopy()
	// Greedy: repeatedly pick best claim against remaining.
	for !resourceListAllZero(remaining) {
		bestIdx := -1
		bestScore := float64(0)

		for i := range claims {
			uid := string(claims[i].UID)
			if _, ok := selected[uid]; ok {
				continue
			}

			score := claimCoverageScore(remaining, claims[i].Spec.ResourceClaims)
			if score > bestScore {
				bestScore = score
				bestIdx = i
			}
		}

		if bestIdx == -1 || bestScore == 0 {
			// Can't cover remaining with available claims (or used contains resources not claimed).
			break
		}

		chosen := claims[bestIdx]
		selected[string(chosen.UID)] = struct{}{}

		// remaining -= chosen.request (clamped at zero)
		for rName, req := range chosen.Spec.ResourceClaims {
			if rem, ok := remaining[rName]; ok {
				rem.Sub(req)

				if rem.Sign() < 0 {
					rem = rem.DeepCopy()
					rem.Set(0)
				}

				remaining[rName] = rem
			}
		}
	}

	return selected
}

func claimCoverageScore(remaining corev1.ResourceList, reqs corev1.ResourceList) float64 {
	var score float64

	for rName, rem := range remaining {
		if rem.IsZero() {
			continue
		}

		req, ok := reqs[rName]
		if !ok || req.IsZero() {
			continue
		}

		// covered = min(rem, req)
		covered := req.DeepCopy()
		if covered.Cmp(rem) > 0 {
			covered = rem.DeepCopy()
		}

		// Approx score: float is OK as heuristic
		score += covered.AsApproximateFloat64()
	}

	return score
}

func resourceListAllZero(rl corev1.ResourceList) bool {
	for _, q := range rl {
		if !q.IsZero() && q.Sign() > 0 {
			return false
		}
	}

	return true
}
