// src/services/cacheService.server.ts
/**
 * @file Centralized caching service implementing the Cache-Aside pattern.
 * This service provides a reusable wrapper around Redis for caching read-heavy operations.
 * See ADR-009 for the caching strategy documentation.
 */
import type { Logger } from 'pino';
import { connection as redis } from './redis.server';
import { logger as globalLogger } from './logger.server';

/**
 * TTL values in seconds for different cache types.
 * These can be tuned based on data volatility and freshness requirements.
 */
export const CACHE_TTL = {
  /** Brand/store list - rarely changes, safe to cache for 1 hour */
  BRANDS: 60 * 60,
  /** Flyer list - changes when new flyers are added, cache for 5 minutes */
  FLYERS: 5 * 60,
  /** Individual flyer data - cache for 10 minutes */
  FLYER: 10 * 60,
  /** Flyer items - cache for 10 minutes */
  FLYER_ITEMS: 10 * 60,
  /** Statistics - can be slightly stale, cache for 5 minutes */
  STATS: 5 * 60,
  /** Most frequent sales - aggregated data, cache for 15 minutes */
  FREQUENT_SALES: 15 * 60,
  /** Categories - rarely changes, cache for 1 hour */
  CATEGORIES: 60 * 60,
} as const;

/**
 * Cache key prefixes for different data types.
 * Using consistent prefixes allows for pattern-based invalidation.
 */
export const CACHE_PREFIX = {
  BRANDS: 'cache:brands',
  FLYERS: 'cache:flyers',
  FLYER: 'cache:flyer',
  FLYER_ITEMS: 'cache:flyer-items',
  STATS: 'cache:stats',
  FREQUENT_SALES: 'cache:frequent-sales',
  CATEGORIES: 'cache:categories',
} as const;

export interface CacheOptions {
  /** Time-to-live in seconds */
  ttl: number;
  /** Optional logger for this operation */
  logger?: Logger;
}

/**
 * Centralized cache service implementing the Cache-Aside pattern.
 * All cache operations are fail-safe - cache failures do not break the application.
 */
class CacheService {
  /**
   * Retrieves a value from cache.
   * @param key The cache key
   * @param logger Optional logger for this operation
   * @returns The cached value or null if not found/error
   */
  async get<T>(key: string, logger: Logger = globalLogger): Promise<T | null> {
    try {
      const cached = await redis.get(key);
      if (cached) {
        logger.debug({ cacheKey: key }, 'Cache hit');
        return JSON.parse(cached) as T;
      }
      logger.debug({ cacheKey: key }, 'Cache miss');
      return null;
    } catch (error) {
      logger.warn({ err: error, cacheKey: key }, 'Redis GET failed, proceeding without cache');
      return null;
    }
  }

  /**
   * Stores a value in cache with TTL.
   * @param key The cache key
   * @param value The value to cache (will be JSON stringified)
   * @param ttl Time-to-live in seconds
   * @param logger Optional logger for this operation
   */
  async set<T>(key: string, value: T, ttl: number, logger: Logger = globalLogger): Promise<void> {
    try {
      await redis.set(key, JSON.stringify(value), 'EX', ttl);
      logger.debug({ cacheKey: key, ttl }, 'Value cached');
    } catch (error) {
      logger.warn({ err: error, cacheKey: key }, 'Redis SET failed, value not cached');
    }
  }

  /**
   * Deletes a specific key from cache.
   * @param key The cache key to delete
   * @param logger Optional logger for this operation
   */
  async del(key: string, logger: Logger = globalLogger): Promise<void> {
    try {
      await redis.del(key);
      logger.debug({ cacheKey: key }, 'Cache key deleted');
    } catch (error) {
      logger.warn({ err: error, cacheKey: key }, 'Redis DEL failed');
    }
  }

  /**
   * Invalidates all cache keys matching a pattern.
   * Uses SCAN for safe iteration over large key sets.
   * @param pattern The pattern to match (e.g., 'cache:flyers*')
   * @param logger Optional logger for this operation
   * @returns The number of keys deleted
   */
  async invalidatePattern(pattern: string, logger: Logger = globalLogger): Promise<number> {
    let cursor = '0';
    let totalDeleted = 0;

    try {
      do {
        const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
        cursor = nextCursor;
        if (keys.length > 0) {
          const deletedCount = await redis.del(...keys);
          totalDeleted += deletedCount;
        }
      } while (cursor !== '0');

      logger.info({ pattern, totalDeleted }, 'Cache invalidation completed');
      return totalDeleted;
    } catch (error) {
      logger.error({ err: error, pattern }, 'Cache invalidation failed');
      throw error;
    }
  }

  /**
   * Implements the Cache-Aside pattern: try cache first, fall back to fetcher, cache result.
   * This is the primary method for adding caching to existing repository methods.
   *
   * @param key The cache key
   * @param fetcher Function that retrieves data from the source (e.g., database)
   * @param options Cache options including TTL
   * @returns The data (from cache or fetcher)
   *
   * @example
   * ```typescript
   * const brands = await cacheService.getOrSet(
   *   CACHE_PREFIX.BRANDS,
   *   () => this.db.query('SELECT * FROM stores'),
   *   { ttl: CACHE_TTL.BRANDS, logger }
   * );
   * ```
   */
  async getOrSet<T>(
    key: string,
    fetcher: () => Promise<T>,
    options: CacheOptions,
  ): Promise<T> {
    const logger = options.logger ?? globalLogger;

    // Try to get from cache first
    const cached = await this.get<T>(key, logger);
    if (cached !== null) {
      return cached;
    }

    // Cache miss - fetch from source
    const data = await fetcher();

    // Cache the result (fire-and-forget, don't await)
    this.set(key, data, options.ttl, logger).catch(() => {
      // Error already logged in set()
    });

    return data;
  }

  // --- Convenience methods for specific cache types ---

  /**
   * Invalidates all brand-related cache entries.
   */
  async invalidateBrands(logger: Logger = globalLogger): Promise<number> {
    return this.invalidatePattern(`${CACHE_PREFIX.BRANDS}*`, logger);
  }

  /**
   * Invalidates all flyer-related cache entries.
   */
  async invalidateFlyers(logger: Logger = globalLogger): Promise<number> {
    const patterns = [
      `${CACHE_PREFIX.FLYERS}*`,
      `${CACHE_PREFIX.FLYER}*`,
      `${CACHE_PREFIX.FLYER_ITEMS}*`,
    ];

    let total = 0;
    for (const pattern of patterns) {
      total += await this.invalidatePattern(pattern, logger);
    }
    return total;
  }

  /**
   * Invalidates cache for a specific flyer and its items.
   */
  async invalidateFlyer(flyerId: number, logger: Logger = globalLogger): Promise<void> {
    await Promise.all([
      this.del(`${CACHE_PREFIX.FLYER}:${flyerId}`, logger),
      this.del(`${CACHE_PREFIX.FLYER_ITEMS}:${flyerId}`, logger),
      // Also invalidate the flyers list since it may contain this flyer
      this.invalidatePattern(`${CACHE_PREFIX.FLYERS}*`, logger),
    ]);
  }

  /**
   * Invalidates all statistics cache entries.
   */
  async invalidateStats(logger: Logger = globalLogger): Promise<number> {
    return this.invalidatePattern(`${CACHE_PREFIX.STATS}*`, logger);
  }
}

export const cacheService = new CacheService();
