package redistool

import (
	"context"
	"iter"
	"strconv"
	"sync"
	"time"

	"github.com/redis/rueidis"
	"gitlab.com/gitlab-org/cluster-integration/gitlab-agent/v18/internal/api"
	otelmetric "go.opentelemetry.io/otel/metric"
)

// KeyToRedisKey is used to convert typed key (key1 or key2) into a string.
// HSET key1 key2 value.
type KeyToRedisKey[K any] func(key K) string

// ExpiringHash represents a two-level hash: key K1 -> hashKey K2 -> value []byte.
// key identifies the hash; hashKey identifies the key in the hash; value is the value for the hashKey.
type ExpiringHash[K1 any, K2 any] interface {
	// GetName returns the name of this hash
	GetName() string
	// SetEX sets the key -> hashKey -> value.
	// Use this method to re-set (i.e. refresh) the value.
	// Safe for concurrent use.
	SetEX(ctx context.Context, key K1, hashKey K2, value []byte, expiresAt time.Time) error
	// Scan iterates key-value pairs for key.
	// Safe for concurrent use.
	Scan(ctx context.Context, key K1) iter.Seq2[ScanEntry, error]
	// Len returns number of key-value mappings in the hash identified by key.
	Len(ctx context.Context, key K1) (int64, error)
	// GC iterates all relevant stored data and deletes expired entries.
	// It can be called concurrently as it does not interfere with the hash's operation.
	// Returns number of deleted Redis (hash) keys, including when an error occurred.
	// It only inspects/GCs hashes where it has entries. Other concurrent clients GC same and/or other corresponding hashes.
	// Hashes that don't have a corresponding client (e.g. because it crashed) will expire because of TTL on the hash key.
	GC(context.Context) (int /* keysDeleted */, error)
	// GCFor returns a function that iterates the hash for the given keys and deletes expired entries.
	// GCFor is useful when executing GC for specific keys.
	GCFor(keys []K1) func(context.Context) (int /* keysDeleted */, error)
	DelEX(ctx context.Context, key K1, hashKey K2) error
}

type RedisExpiringHash[K1 comparable, K2 comparable] struct {
	name            string
	client          rueidis.Client
	key1ToRedisKey  KeyToRedisKey[K1]
	key2ToRedisKey  KeyToRedisKey[K2]
	ttl             time.Duration
	api             *RedisExpiringHashAPI[K1, K2]
	mu              sync.Mutex
	dataEX          map[K1]struct{}
	transactionalGC bool
}

func NewRedisExpiringHash[K1 comparable, K2 comparable](name string, client rueidis.Client, key1ToRedisKey KeyToRedisKey[K1],
	key2ToRedisKey KeyToRedisKey[K2], ttl time.Duration, m otelmetric.Meter, transactionalGC bool) (*RedisExpiringHash[K1, K2], error) {
	api, err := NewRedisExpiringHashAPI[K1, K2](name, client, key1ToRedisKey, key2ToRedisKey, m)
	if err != nil {
		return nil, err
	}

	return &RedisExpiringHash[K1, K2]{
		name:            name,
		client:          client,
		key1ToRedisKey:  key1ToRedisKey,
		key2ToRedisKey:  key2ToRedisKey,
		ttl:             ttl,
		api:             api,
		dataEX:          make(map[K1]struct{}),
		transactionalGC: transactionalGC,
	}, nil
}

func (h *RedisExpiringHash[K1, K2]) GetName() string {
	return h.name
}

func (h *RedisExpiringHash[K1, K2]) SetEX(ctx context.Context, key K1, hashKey K2, value []byte, expiresAt time.Time) error {
	h.mu.Lock()
	h.dataEX[key] = struct{}{}
	h.mu.Unlock()

	b := h.api.IOBuilder()
	b.Set([]K1{key}, h.ttl, BuilderKV[K2]{
		HashKey: hashKey,
		Value: &ExpiringValue{
			ExpiresAt: expiresAt.Unix(),
			Value:     value,
		},
	})
	return b.Do(ctx)
}

func (h *RedisExpiringHash[K1, K2]) DelEX(ctx context.Context, key K1, hashKey K2) error {
	h.mu.Lock()
	delete(h.dataEX, key)
	h.mu.Unlock()

	b := h.api.IOBuilder()
	b.Unset([]K1{key}, hashKey)

	return b.Do(ctx)
}

func (h *RedisExpiringHash[K1, K2]) Len(ctx context.Context, key K1) (size int64, retErr error) {
	hlenCmd := h.client.B().Hlen().Key(h.key1ToRedisKey(key)).Build()
	return h.client.Do(ctx, hlenCmd).AsInt64()
}

func (h *RedisExpiringHash[K1, K2]) Scan(ctx context.Context, key K1) iter.Seq2[ScanEntry, error] {
	return h.api.Scan(ctx, key)
}

func (h *RedisExpiringHash[K1, K2]) GC(ctx context.Context) (int /* keysDeleted */, error) {
	h.mu.Lock()
	keys := make([]K1, 0, len(h.dataEX))
	for key := range h.dataEX {
		keys = append(keys, key)
	}
	clear(h.dataEX)
	h.mu.Unlock()

	return h.GCFor(keys)(ctx)
}

func (h *RedisExpiringHash[K1, K2]) GCFor(keys []K1) func(context.Context) (int /* keysDeleted */, error) {
	return h.api.GCFor(keys, h.transactionalGC)
}

func PrefixedInt64Key(prefix string, key int64) string {
	return prefix + strconv.FormatInt(key, 32)
}

func PrefixedAgentKey(prefix string, agentKey api.AgentKey) string {
	return prefix + agentKey.Type.String() + ":" + strconv.FormatInt(agentKey.ID, 32)
}
