package cache

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"log/slog"
	"time"

	"github.com/ashie/gosearch/internal/contracts"
	"github.com/redis/go-redis/v9"
)

// Config holds Valkey/Redis connection settings.
type Config struct {
	// Address is the Valkey server address (e.g. "localhost:6379").
	Address string
	// Password for authentication (empty = no auth).
	Password string
	// Database index (default 0).
	DB int
	// Default TTL for cached search results.
	DefaultTTL time.Duration
}

// Cache provides a Valkey-backed cache for search responses.
// It is safe for concurrent use.
// If the Valkey connection is nil or fails, cache operations are no-ops.
type Cache struct {
	client *redis.Client
	ttl    time.Duration
	logger *slog.Logger
}

// New creates a new Cache. If cfg.Address is empty, returns a no-op cache.
func New(cfg Config, logger *slog.Logger) *Cache {
	if logger == nil {
		logger = slog.Default()
	}

	if cfg.Address == "" {
		logger.Debug("cache disabled: no valkey address configured")
		return &Cache{logger: logger}
	}

	ttl := cfg.DefaultTTL
	if ttl <= 0 {
		ttl = 5 * time.Minute
	}

	client := redis.NewClient(&redis.Options{
		Addr:     cfg.Address,
		Password: cfg.Password,
		DB:       cfg.DB,
	})

	// Verify connectivity with a short timeout.
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	if err := client.Ping(ctx).Err(); err != nil {
		logger.Warn("cache disabled: valkey ping failed", "addr", cfg.Address, "error", err)
		return &Cache{logger: logger}
	}

	logger.Info("cache connected", "addr", cfg.Address, "db", cfg.DB, "ttl", ttl)
	return &Cache{client: client, ttl: ttl, logger: logger}
}

// Enabled returns true if the cache has a live Valkey connection.
func (c *Cache) Enabled() bool {
	return c.client != nil
}

// Get retrieves a cached search response. Returns (response, true) on hit,
// (zero, false) on miss or error.
func (c *Cache) Get(ctx context.Context, key string) (contracts.SearchResponse, bool) {
	if !c.Enabled() {
		return contracts.SearchResponse{}, false
	}

	fullKey := "gosearch:" + key

	data, err := c.client.Get(ctx, fullKey).Bytes()
	if err != nil {
		if err != redis.Nil {
			c.logger.Debug("cache miss (error)", "key", fullKey, "error", err)
		}
		return contracts.SearchResponse{}, false
	}

	var resp contracts.SearchResponse
	if err := json.Unmarshal(data, &resp); err != nil {
		c.logger.Warn("cache hit but unmarshal failed", "key", fullKey, "error", err)
		return contracts.SearchResponse{}, false
	}

	c.logger.Debug("cache hit", "key", fullKey)
	return resp, true
}

// Set stores a search response in the cache with the default TTL.
func (c *Cache) Set(ctx context.Context, key string, resp contracts.SearchResponse) {
	if !c.Enabled() {
		return
	}

	data, err := json.Marshal(resp)
	if err != nil {
		c.logger.Warn("cache set: marshal failed", "key", key, "error", err)
		return
	}

	fullKey := "gosearch:" + key
	if err := c.client.Set(ctx, fullKey, data, c.ttl).Err(); err != nil {
		c.logger.Warn("cache set failed", "key", fullKey, "error", err)
	}
}

// Invalidate removes a specific key from the cache.
func (c *Cache) Invalidate(ctx context.Context, key string) {
	if !c.Enabled() {
		return
	}
	fullKey := "gosearch:" + key
	c.client.Del(ctx, fullKey)
}

// Close closes the Valkey connection.
func (c *Cache) Close() error {
	if c.client == nil {
		return nil
	}
	return c.client.Close()
}

// Key generates a deterministic cache key from search parameters.
// The key is a SHA-256 hash of the normalized parameters, prefixed for readability.
func Key(req contracts.SearchRequest) string {
	h := sha256.New()

	fmt.Fprintf(h, "q=%s|", req.Query)
	fmt.Fprintf(h, "format=%s|", req.Format)
	fmt.Fprintf(h, "pageno=%d|", req.Pageno)
	fmt.Fprintf(h, "safesearch=%d|", req.Safesearch)
	fmt.Fprintf(h, "lang=%s|", req.Language)

	if req.TimeRange != nil {
		fmt.Fprintf(h, "tr=%s|", *req.TimeRange)
	}

	for _, e := range req.Engines {
		fmt.Fprintf(h, "e=%s|", e)
	}
	for _, cat := range req.Categories {
		fmt.Fprintf(h, "c=%s|", cat)
	}

	return hex.EncodeToString(h.Sum(nil))[:32]
}
