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

package cache

import (
	"context"
	"sync"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/projectcapsule/capsule/pkg/api/meta"
	"github.com/projectcapsule/capsule/pkg/users"
)

type Key struct {
	Namespace string
	Name      string
}

type ImpersonationCache struct {
	mu      sync.RWMutex
	clients map[Key]client.Client
}

func NewImpersonationCache() *ImpersonationCache {
	return &ImpersonationCache{
		clients: make(map[Key]client.Client),
	}
}

// Get returns a cached client if present.
func (c *ImpersonationCache) Get(ns, name string) (client.Client, bool) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	cl, ok := c.clients[Key{Namespace: ns, Name: name}]

	return cl, ok
}

// Set stores a client explicitly (rarely needed).
func (c *ImpersonationCache) Set(namespace, name string, cl client.Client) {
	c.mu.Lock()
	defer c.mu.Unlock()

	if cl == nil {
		return
	}

	c.clients[Key{Namespace: namespace, Name: name}] = cl
}

// Invalidate removes one entry.
func (c *ImpersonationCache) Invalidate(namespace, name string) {
	c.mu.Lock()
	defer c.mu.Unlock()

	delete(c.clients, Key{Namespace: namespace, Name: name})
}

// Clear drops all cached clients.
func (c *ImpersonationCache) Reset() {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.clients = make(map[Key]client.Client)
}

// Stats helps you log cache state.
func (c *ImpersonationCache) Stats() (entries int) {
	c.mu.RLock()
	defer c.mu.RUnlock()

	return len(c.clients)
}

// LoadOrCreate returns a cached impersonated client for the given service account,
// creating and caching it if missing.
func (c *ImpersonationCache) LoadOrCreate(
	ctx context.Context,
	log logr.Logger,
	baseREST *rest.Config,
	scheme *runtime.Scheme,
	sa meta.NamespacedRFC1123ObjectReferenceWithNamespace,
) (client.Client, error) {
	key := Key{Namespace: string(sa.Namespace), Name: string(sa.Name)}

	// Fast path
	if cl, ok := c.Get(key.Namespace, key.Name); ok {
		return cl, nil
	}

	cl, err := users.ImpersonatedKubernetesClientForServiceAccount(
		baseREST,
		scheme,
		sa,
	)
	if err != nil {
		log.Error(err, "failed to create impersonated client", "namespace", key.Namespace, "name", key.Name)

		return nil, err
	}

	// Store (double-check to avoid duplicate creation races)
	c.mu.Lock()
	defer c.mu.Unlock()

	if existing := c.clients[key]; existing != nil {
		return existing, nil
	}

	c.clients[key] = cl

	return cl, nil
}
