package cache

import (
	"context"
	"errors"
	"fmt"
	"strings"
	"time"

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

// CacheKey defines the structure for cache keys used throughout the application.
type CacheKey struct {
	Kind    string // "city", "station", "search"
	Code    string // city code or station ID
	From    string // search from city code
	To      string // search to city code
	Date    string // search date
	Request string // optional request identifier
}

// Cache interface defines the Redis cache operations used by the application.
type Cache interface {
	// Get retrieves a value from cache by key.
	Get(ctx context.Context, key *CacheKey) ([]byte, error)
	// Set stores a value in cache with an expiry TTL.
	Set(ctx context.Context, key *CacheKey, value []byte, ttl time.Duration) error
	// Exists checks if a key exists in cache.
	Exists(ctx context.Context, key *CacheKey) (bool, error)
	// Delete removes a key from cache.
	Delete(ctx context.Context, key *CacheKey) error
	// Increment increments a counter key.
	Increment(ctx context.Context, key *CacheKey) (int64, error)
	// Decrement decrements a counter key.
	Decrement(ctx context.Context, key *CacheKey) (int64, error)
}

// redisClient is a wrapper around go-redis client for dependency injection.
type redisClient struct {
	client *redis.Client
}

// NewRedisClient creates a new Redis client wrapper.
func NewRedisClient(client *redis.Client) *redisClient {
	return &redisClient{client: client}
}

// Get retrieves a value from cache by key.
func (r *redisClient) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
	val, err := r.client.Get(ctx, keyString(key)).Bytes()
	if errors.Is(err, redis.Nil) {
		return nil, nil // cache miss
	}
	if err != nil {
		return nil, fmt.Errorf("cache get: %w", err)
	}
	return val, nil
}

// Set stores a value in cache with an expiry TTL.
func (r *redisClient) Set(ctx context.Context, key *CacheKey, value []byte, ttl time.Duration) error {
	return r.client.Set(ctx, keyString(key), value, ttl).Err()
}

// Exists checks if a key exists in cache.
func (r *redisClient) Exists(ctx context.Context, key *CacheKey) (bool, error) {
	_, err := r.client.Exists(ctx, keyString(key)).Result()
	if errors.Is(err, redis.Nil) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("cache exists: %w", err)
	}
	return true, nil
}

// Delete removes a key from cache.
func (r *redisClient) Delete(ctx context.Context, key *CacheKey) error {
	return r.client.Del(ctx, keyString(key)).Err()
}

// Increment increments a counter key.
func (r *redisClient) Increment(ctx context.Context, key *CacheKey) (int64, error) {
	return r.client.Incr(ctx, keyString(key)).Result()
}

// Decrement decrements a counter key.
func (r *redisClient) Decrement(ctx context.Context, key *CacheKey) (int64, error) {
	return r.client.Decr(ctx, keyString(key)).Result()
}

// keyString converts a CacheKey to a Redis string key.
// Sanitizes key components to prevent key corruption via special characters.
func sanitizeKeyComponent(s string) string {
	// Replace characters that could corrupt Redis key format
	s = strings.ReplaceAll(s, ":", "_colon_")
	s = strings.ReplaceAll(s, "/", "_slash_")
	s = strings.ReplaceAll(s, " ", "_")
	s = strings.ReplaceAll(s, "\t", "_tab_")
	s = strings.ReplaceAll(s, "\n", "_newline_")
	s = strings.ReplaceAll(s, "\r", "_cr_")
	return s
}

func keyString(k *CacheKey) string {
	switch k.Kind {
	case "city":
		return fmt.Sprintf("cities:%s", sanitizeKeyComponent(k.Code))
	case "station":
		return fmt.Sprintf("stations:%s", sanitizeKeyComponent(k.Code))
	case "search":
		return fmt.Sprintf("search:%s:%s:%s",
			sanitizeKeyComponent(k.From),
			sanitizeKeyComponent(k.To),
			sanitizeKeyComponent(k.Date))
	default:
		return fmt.Sprintf("unknown:%s", sanitizeKeyComponent(k.Kind))
	}
}

// cacheStore implements the Cache interface with TTL policies.
type cacheStore struct {
	*redisClient
}

// NewCacheStore creates a new cache store with the given Redis client.
func NewCacheStore(client *redis.Client) Cache {
	return &cacheStore{
		redisClient: NewRedisClient(client),
	}
}

// TTL constants for cache policies.
const (
	// CityTTL is the time-to-live for city/station directory data (30 days).
	CityTTL = 30 * 24 * time.Hour

	// SearchNearTermTTL is the time-to-live for search results with near-term dates (2-6 hours).
	SearchNearTermTTL = 3 * time.Hour

	// SearchFarTermTTL is the time-to-live for search results with far-term dates (7 days).
	SearchFarTermTTL = 7 * 24 * time.Hour
)

// GetCityKey returns the cache key for a city code.
func GetCityKey(code string) *CacheKey {
	return &CacheKey{Kind: "city", Code: code}
}

// GetStationKey returns the cache key for a station ID.
func GetStationKey(id string) *CacheKey {
	return &CacheKey{Kind: "station", Code: id}
}

// GetSearchKey returns the cache key for a search query.
func GetSearchKey(from, to, date string) *CacheKey {
	return &CacheKey{Kind: "search", From: from, To: to, Date: date}
}

// CacheAside represents the cache-aside pattern implementation.
// It follows the pattern: Redis → miss → Postgres/API → write-back to Redis.
type CacheAside struct {
	store Cache
}

// NewCacheAside creates a new CacheAside instance.
func NewCacheAside(store Cache) *CacheAside {
	return &CacheAside{store: store}
}

// GetOrSetFuncPattern is a generic pattern for cache-aside operations.
// It retrieves a value from cache, and if missing, calls the fetch function
// to populate the cache before returning the value.
func (c *CacheAside) GetOrSetFuncPattern(ctx context.Context, key *CacheKey, fetch func() ([]byte, error), ttl time.Duration) ([]byte, error) {
	// Try cache first
	if data, err := c.store.Get(ctx, key); err == nil && data != nil {
		return data, nil // cache hit
	}

	// Cache miss: fetch from backend
	data, err := fetch()
	if err != nil {
		return nil, err
	}

	// Write back to cache
	if err := c.store.Set(ctx, key, data, ttl); err != nil {
		return nil, err
	}

	return data, nil
}

// GetCity retrieves a city from cache, falling back to the provided fetch function.
func (c *CacheAside) GetCity(ctx context.Context, key *CacheKey, fetch func() ([]byte, error)) ([]byte, error) {
	return c.GetOrSetFuncPattern(ctx, key, fetch, CityTTL)
}

// GetStation retrieves a station from cache, falling back to the provided fetch function.
func (c *CacheAside) GetStation(ctx context.Context, key *CacheKey, fetch func() ([]byte, error)) ([]byte, error) {
	return c.GetOrSetFuncPattern(ctx, key, fetch, CityTTL)
}

// GetSearch retrieves search results from cache, falling back to the provided fetch function.
// Uses appropriate TTL based on whether the date is near-term or far-term.
func (c *CacheAside) GetSearch(ctx context.Context, key *CacheKey, fetch func() ([]byte, error), isFarTerm bool) ([]byte, error) {
	var ttl time.Duration
	if isFarTerm {
		ttl = SearchFarTermTTL
	} else {
		ttl = SearchNearTermTTL
	}
	return c.GetOrSetFuncPattern(ctx, key, fetch, ttl)
}

// InvalidateCity removes a city entry from cache.
func (c *CacheAside) InvalidateCity(ctx context.Context, key *CacheKey) error {
	return c.store.Delete(ctx, key)
}

// InvalidateStation removes a station entry from cache.
func (c *CacheAside) InvalidateStation(ctx context.Context, key *CacheKey) error {
	return c.store.Delete(ctx, key)
}

// InvalidateSearch removes search results from cache.
func (c *CacheAside) InvalidateSearch(ctx context.Context, key *CacheKey) error {
	return c.store.Delete(ctx, key)
}

// Delete removes a key from cache.
func (c *CacheAside) Delete(ctx context.Context, key *CacheKey) error {
	return c.store.Delete(ctx, key)
}

// Get retrieves a value from cache by key.
func (c *CacheAside) Get(ctx context.Context, key *CacheKey) ([]byte, error) {
	val, err := c.store.Get(ctx, key)
	if errors.Is(err, redis.Nil) {
		return nil, nil // cache miss
	}
	if err != nil {
		return nil, fmt.Errorf("cache get: %w", err)
	}
	return val, nil
}

// Set stores a value in cache with an expiry TTL.
func (c *CacheAside) Set(ctx context.Context, key *CacheKey, value []byte, ttl time.Duration) error {
	return c.store.Set(ctx, key, value, ttl)
}

// Exists checks if a key exists in cache.
func (c *CacheAside) Exists(ctx context.Context, key *CacheKey) (bool, error) {
	_, err := c.store.Exists(ctx, key)
	if errors.Is(err, redis.Nil) {
		return false, nil
	}
	if err != nil {
		return false, fmt.Errorf("cache exists: %w", err)
	}
	return true, nil
}

// Increment increments a counter key.
func (c *CacheAside) Increment(ctx context.Context, key *CacheKey) (int64, error) {
	return c.store.Increment(ctx, key)
}

// Decrement decrements a counter key.
func (c *CacheAside) Decrement(ctx context.Context, key *CacheKey) (int64, error) {
	return c.store.Decrement(ctx, key)
}
