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

package resourcepool

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/go-logr/logr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
	"github.com/projectcapsule/capsule/pkg/api/meta"
	ad "github.com/projectcapsule/capsule/pkg/runtime/admission"
	"github.com/projectcapsule/capsule/pkg/runtime/events"
	"github.com/projectcapsule/capsule/pkg/runtime/handlers"
)

type claimMutationHandler struct {
	log logr.Logger
}

func ClaimMutationHandler(log logr.Logger) handlers.Handler {
	return &claimMutationHandler{log: log}
}

func (h *claimMutationHandler) OnUpdate(
	c client.Client,
	_ client.Reader,
	decoder admission.Decoder,
	_ events.EventRecorder,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return h.handle(ctx, c, req, decoder, h.handleReleaseAnnotation)
	}
}

func (h *claimMutationHandler) OnDelete(
	client.Client,
	client.Reader,
	admission.Decoder,
	events.EventRecorder,
) handlers.Func {
	return func(context.Context, admission.Request) *admission.Response {
		return nil
	}
}

func (h *claimMutationHandler) OnCreate(
	c client.Client,
	_ client.Reader,
	decoder admission.Decoder,
	_ events.EventRecorder,
) handlers.Func {
	return func(ctx context.Context, req admission.Request) *admission.Response {
		return h.handle(ctx, c, req, decoder, func(claim *capsulev1beta2.ResourcePoolClaim) {
			meta.ReleaseAnnotationRemove(claim)
		})
	}
}

func (h *claimMutationHandler) handle(
	ctx context.Context,
	c client.Client,
	req admission.Request,
	decoder admission.Decoder,
	annoHandler func(c *capsulev1beta2.ResourcePoolClaim),
) *admission.Response {
	claim := &capsulev1beta2.ResourcePoolClaim{}

	if err := decoder.Decode(req, claim); err != nil {
		return ad.ErroredResponse(fmt.Errorf("failed to decode new object: %w", err))
	}

	annoHandler(claim)

	if err := h.autoAssignPools(ctx, c, claim); err != nil {
		response := admission.Errored(http.StatusInternalServerError, err)

		return &response
	}

	marshaled, err := json.Marshal(claim)
	if err != nil {
		response := admission.Errored(http.StatusInternalServerError, err)

		return &response
	}

	response := admission.PatchResponseFromRaw(req.Object.Raw, marshaled)

	return &response
}

// Only Adds release label when necessary.
func (h *claimMutationHandler) handleReleaseAnnotation(
	claim *capsulev1beta2.ResourcePoolClaim,
) {
	if !meta.ReleaseAnnotationTriggers(claim) {
		return
	}

	if claim.IsBoundInResourcePool() {
		return
	}

	meta.ReleaseAnnotationRemove(claim)
}

func (h *claimMutationHandler) autoAssignPools(
	ctx context.Context,
	c client.Client,
	claim *capsulev1beta2.ResourcePoolClaim,
) error {
	if claim.Spec.Pool != "" {
		return nil
	}

	poolList := &capsulev1beta2.ResourcePoolList{}
	if err := c.List(ctx, poolList, client.MatchingFields{".status.namespaces": claim.Namespace}); err != nil {
		return err
	}

	if len(poolList.Items) == 0 {
		return nil
	}

	candidates := make([]*capsulev1beta2.ResourcePool, 0)

	for _, pool := range poolList.Items {
		assignable := true
		allocatable := true

		for resource, requested := range claim.Spec.ResourceClaims {
			if _, ok := pool.Status.Allocation.Hard[resource]; !ok {
				assignable = false

				break
			}

			available, ok := pool.Status.Allocation.Available[resource]
			if !ok || available.Cmp(requested) < 0 {
				allocatable = false

				break
			}
		}

		if !assignable {
			continue
		}

		if allocatable {
			candidates = append([]*capsulev1beta2.ResourcePool{&pool}, candidates...)

			continue
		}

		candidates = append(candidates, &pool)
	}

	if len(candidates) == 0 {
		return nil // no eligible pools
	}

	claim.Spec.Pool = candidates[0].Name

	return nil
}
