// Package presigncache provides a Valkey (Redis-compatible) backed cache for
// MinIO presigned URLs. The backend generates presigned URLs and stores them
// here with a TTL; subsequent requests for the same key return the cached URL
// without re-contacting MinIO.
//
// Design:
//   - Cache is intentionally best-effort: Get returns ("", false, nil) on any
//     Valkey error, so callers always have a fallback path to regenerate.
//   - Set silently drops errors — a miss on the next request is acceptable.
//   - TTL should be set shorter than the actual presigned URL lifetime so that
//     cached URLs are always valid when served. Recommended: 55 minutes for a
//     1-hour presigned URL.
package presigncache

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

// Cache is the interface for presign URL caching.
// Implementations must be safe for concurrent use.
type Cache interface {
	// Get returns the cached URL for key. ok is false on cache miss or error.
	Get(ctx context.Context, key string) (url string, ok bool, err error)
	// Set stores url under key with the given TTL.
	Set(ctx context.Context, key, url string, ttl time.Duration) error
	// Delete removes key from the cache.
	Delete(ctx context.Context, key string) error
}

// ValkeyCache is a Cache backed by Valkey / Redis via go-redis.
type ValkeyCache struct {
	rdb *redis.Client
}

// New creates a ValkeyCache connecting to addr (e.g. "valkey:6379").
// The connection is not established until the first command; use Ping to
// verify connectivity at startup.
func New(addr string) *ValkeyCache {
	rdb := redis.NewClient(&redis.Options{
		Addr:         addr,
		DialTimeout:  2 * time.Second,
		ReadTimeout:  1 * time.Second,
		WriteTimeout: 1 * time.Second,
	})
	return &ValkeyCache{rdb: rdb}
}

// Ping checks connectivity. Call once at startup.
func (c *ValkeyCache) Ping(ctx context.Context) error {
	if err := c.rdb.Ping(ctx).Err(); err != nil {
		return fmt.Errorf("presigncache: ping valkey: %w", err)
	}
	return nil
}

// Get returns (url, true, nil) on hit, ("", false, nil) on miss, and
// ("", false, err) only on unexpected errors (not redis.Nil).
func (c *ValkeyCache) Get(ctx context.Context, key string) (string, bool, error) {
	val, err := c.rdb.Get(ctx, key).Result()
	if err == redis.Nil {
		return "", false, nil
	}
	if err != nil {
		return "", false, fmt.Errorf("presigncache: get %q: %w", key, err)
	}
	return val, true, nil
}

// Set stores url under key with ttl. Errors are returned but are non-fatal
// for callers — a Set failure means the next request will miss and regenerate.
func (c *ValkeyCache) Set(ctx context.Context, key, url string, ttl time.Duration) error {
	if err := c.rdb.Set(ctx, key, url, ttl).Err(); err != nil {
		return fmt.Errorf("presigncache: set %q: %w", key, err)
	}
	return nil
}

// Delete removes key from the cache. It is not an error if the key does not exist.
func (c *ValkeyCache) Delete(ctx context.Context, key string) error {
	if err := c.rdb.Del(ctx, key).Err(); err != nil {
		return fmt.Errorf("presigncache: delete %q: %w", key, err)
	}
	return nil
}

// NoopCache is a no-op Cache that always returns a miss. Used when Valkey is
// not configured (e.g. local development without Docker).
type NoopCache struct{}

func (NoopCache) Get(_ context.Context, _ string) (string, bool, error)     { return "", false, nil }
func (NoopCache) Set(_ context.Context, _, _ string, _ time.Duration) error { return nil }
func (NoopCache) Delete(_ context.Context, _ string) error                  { return nil }
