package services import ( "context" "crypto/md5" "encoding/json" "fmt" "time" "github.com/redis/go-redis/v9" "github.com/rs/zerolog/log" "github.com/treytartt/honeydue-api/internal/config" ) // CacheService provides Redis caching functionality type CacheService struct { client *redis.Client } var cacheInstance *CacheService // NewCacheService creates a new cache service func NewCacheService(cfg *config.RedisConfig) (*CacheService, error) { opt, err := redis.ParseURL(cfg.URL) if err != nil { return nil, fmt.Errorf("failed to parse Redis URL: %w", err) } if cfg.Password != "" { opt.Password = cfg.Password } if cfg.DB != 0 { opt.DB = cfg.DB } client := redis.NewClient(opt) // Test connection ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { return nil, fmt.Errorf("failed to connect to Redis: %w", err) } log.Info(). Str("url", cfg.URL). Int("db", opt.DB). Msg("Connected to Redis") cacheInstance = &CacheService{client: client} return cacheInstance, nil } // GetCache returns the cache service instance func GetCache() *CacheService { return cacheInstance } // Client returns the underlying Redis client func (c *CacheService) Client() *redis.Client { return c.client } // Set stores a value with expiration func (c *CacheService) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { data, err := json.Marshal(value) if err != nil { return fmt.Errorf("failed to marshal value: %w", err) } return c.client.Set(ctx, key, data, expiration).Err() } // Get retrieves a value by key func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error { data, err := c.client.Get(ctx, key).Bytes() if err != nil { return err } return json.Unmarshal(data, dest) } // GetString retrieves a string value by key func (c *CacheService) GetString(ctx context.Context, key string) (string, error) { return c.client.Get(ctx, key).Result() } // SetString stores a string value with expiration func (c *CacheService) SetString(ctx context.Context, key string, value string, expiration time.Duration) error { return c.client.Set(ctx, key, value, expiration).Err() } // Delete removes a key func (c *CacheService) Delete(ctx context.Context, keys ...string) error { return c.client.Del(ctx, keys...).Err() } // Exists checks if a key exists func (c *CacheService) Exists(ctx context.Context, keys ...string) (int64, error) { return c.client.Exists(ctx, keys...).Result() } // Close closes the Redis connection func (c *CacheService) Close() error { if c.client != nil { return c.client.Close() } return nil } // Auth token cache helpers const ( AuthTokenPrefix = "auth_token_" TokenCacheTTL = 5 * time.Minute ) // CacheAuthToken caches a user ID for a token func (c *CacheService) CacheAuthToken(ctx context.Context, token string, userID uint) error { key := AuthTokenPrefix + token return c.SetString(ctx, key, fmt.Sprintf("%d", userID), TokenCacheTTL) } // GetCachedAuthToken gets a cached user ID for a token func (c *CacheService) GetCachedAuthToken(ctx context.Context, token string) (uint, error) { key := AuthTokenPrefix + token val, err := c.GetString(ctx, key) if err != nil { return 0, err } var userID uint _, err = fmt.Sscanf(val, "%d", &userID) return userID, err } // InvalidateAuthToken removes a cached token func (c *CacheService) InvalidateAuthToken(ctx context.Context, token string) error { key := AuthTokenPrefix + token return c.Delete(ctx, key) } // Static data cache helpers const ( StaticDataKey = "static_data" StaticDataTTL = 1 * time.Hour ) // CacheStaticData caches static lookup data func (c *CacheService) CacheStaticData(ctx context.Context, data interface{}) error { return c.Set(ctx, StaticDataKey, data, StaticDataTTL) } // GetCachedStaticData retrieves cached static data func (c *CacheService) GetCachedStaticData(ctx context.Context, dest interface{}) error { return c.Get(ctx, StaticDataKey, dest) } // InvalidateStaticData removes cached static data func (c *CacheService) InvalidateStaticData(ctx context.Context) error { return c.Delete(ctx, StaticDataKey) } // Lookup data cache helpers - each lookup type gets its own key const ( LookupKeyPrefix = "lookup:" LookupCategoriesKey = LookupKeyPrefix + "categories" LookupPrioritiesKey = LookupKeyPrefix + "priorities" LookupFrequenciesKey = LookupKeyPrefix + "frequencies" LookupResidenceTypesKey = LookupKeyPrefix + "residence_types" LookupSpecialtiesKey = LookupKeyPrefix + "specialties" LookupTaskTemplatesKey = LookupKeyPrefix + "task_templates" LookupDataTTL = 24 * time.Hour // Lookup data rarely changes ) // CacheLookupData caches data for a specific lookup type func (c *CacheService) CacheLookupData(ctx context.Context, key string, data interface{}) error { return c.Set(ctx, key, data, LookupDataTTL) } // GetCachedLookupData retrieves cached lookup data for a specific key func (c *CacheService) GetCachedLookupData(ctx context.Context, key string, dest interface{}) error { return c.Get(ctx, key, dest) } // InvalidateLookupData removes cached data for a specific lookup type func (c *CacheService) InvalidateLookupData(ctx context.Context, key string) error { return c.Delete(ctx, key) } // InvalidateAllLookups removes all cached lookup data func (c *CacheService) InvalidateAllLookups(ctx context.Context) error { keys := []string{ LookupCategoriesKey, LookupPrioritiesKey, LookupFrequenciesKey, LookupResidenceTypesKey, LookupSpecialtiesKey, LookupTaskTemplatesKey, StaticDataKey, // Also invalidate the combined static data SeededDataKey, // Invalidate unified seeded data SeededDataETagKey, // Invalidate seeded data ETag } return c.Delete(ctx, keys...) } // CacheCategories caches task categories func (c *CacheService) CacheCategories(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupCategoriesKey, data) } // GetCachedCategories retrieves cached task categories func (c *CacheService) GetCachedCategories(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupCategoriesKey, dest) } // InvalidateCategories removes cached task categories func (c *CacheService) InvalidateCategories(ctx context.Context) error { // Invalidate both specific key and combined static data return c.Delete(ctx, LookupCategoriesKey, StaticDataKey) } // CachePriorities caches task priorities func (c *CacheService) CachePriorities(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupPrioritiesKey, data) } // GetCachedPriorities retrieves cached task priorities func (c *CacheService) GetCachedPriorities(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupPrioritiesKey, dest) } // InvalidatePriorities removes cached task priorities func (c *CacheService) InvalidatePriorities(ctx context.Context) error { return c.Delete(ctx, LookupPrioritiesKey, StaticDataKey) } // CacheFrequencies caches task frequencies func (c *CacheService) CacheFrequencies(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupFrequenciesKey, data) } // GetCachedFrequencies retrieves cached task frequencies func (c *CacheService) GetCachedFrequencies(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupFrequenciesKey, dest) } // InvalidateFrequencies removes cached task frequencies func (c *CacheService) InvalidateFrequencies(ctx context.Context) error { return c.Delete(ctx, LookupFrequenciesKey, StaticDataKey) } // CacheResidenceTypes caches residence types func (c *CacheService) CacheResidenceTypes(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupResidenceTypesKey, data) } // GetCachedResidenceTypes retrieves cached residence types func (c *CacheService) GetCachedResidenceTypes(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupResidenceTypesKey, dest) } // InvalidateResidenceTypes removes cached residence types func (c *CacheService) InvalidateResidenceTypes(ctx context.Context) error { return c.Delete(ctx, LookupResidenceTypesKey, StaticDataKey) } // CacheSpecialties caches contractor specialties func (c *CacheService) CacheSpecialties(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupSpecialtiesKey, data) } // GetCachedSpecialties retrieves cached contractor specialties func (c *CacheService) GetCachedSpecialties(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupSpecialtiesKey, dest) } // InvalidateSpecialties removes cached contractor specialties func (c *CacheService) InvalidateSpecialties(ctx context.Context) error { return c.Delete(ctx, LookupSpecialtiesKey, StaticDataKey) } // CacheTaskTemplates caches task templates func (c *CacheService) CacheTaskTemplates(ctx context.Context, data interface{}) error { return c.CacheLookupData(ctx, LookupTaskTemplatesKey, data) } // GetCachedTaskTemplates retrieves cached task templates func (c *CacheService) GetCachedTaskTemplates(ctx context.Context, dest interface{}) error { return c.GetCachedLookupData(ctx, LookupTaskTemplatesKey, dest) } // InvalidateTaskTemplates removes cached task templates func (c *CacheService) InvalidateTaskTemplates(ctx context.Context) error { return c.Delete(ctx, LookupTaskTemplatesKey, StaticDataKey) } // Unified seeded data cache helpers const ( SeededDataKey = "seeded_data" SeededDataETagKey = "seeded_data:etag" SeededDataTTL = 24 * time.Hour ) // CacheSeededData caches the unified seeded data and generates an ETag func (c *CacheService) CacheSeededData(ctx context.Context, data interface{}) (string, error) { jsonData, err := json.Marshal(data) if err != nil { return "", fmt.Errorf("failed to marshal seeded data: %w", err) } // Generate MD5 ETag from the JSON data hash := md5.Sum(jsonData) etag := fmt.Sprintf("\"%x\"", hash) // Store both the data and the ETag if err := c.client.Set(ctx, SeededDataKey, jsonData, SeededDataTTL).Err(); err != nil { return "", fmt.Errorf("failed to cache seeded data: %w", err) } if err := c.client.Set(ctx, SeededDataETagKey, etag, SeededDataTTL).Err(); err != nil { return "", fmt.Errorf("failed to cache seeded data etag: %w", err) } return etag, nil } // GetCachedSeededData retrieves cached unified seeded data func (c *CacheService) GetCachedSeededData(ctx context.Context, dest interface{}) error { return c.Get(ctx, SeededDataKey, dest) } // GetSeededDataETag retrieves the cached ETag for seeded data func (c *CacheService) GetSeededDataETag(ctx context.Context) (string, error) { return c.GetString(ctx, SeededDataETagKey) } // InvalidateSeededData removes cached seeded data and its ETag func (c *CacheService) InvalidateSeededData(ctx context.Context) error { return c.Delete(ctx, SeededDataKey, SeededDataETagKey) }