/**
 * Response Cache Middleware for Express
 *
 * Provides in-memory caching for API responses to reduce database load
 * and improve response times. Especially useful for frequently accessed
 * data like songs, lists, and stats.
 *
 * Features:
 * - Configurable TTL per route
 * - Cache invalidation on mutations
 * - ETag support for conditional requests
 * - Memory-efficient with automatic cleanup
 */

const crypto = require("crypto");

// Cache storage
const cache = new Map();

// Default TTL values (in seconds)
const DEFAULT_TTL = {
  "/api/songs": 300, // 5 minutes for song list
  "/api/lists": 120, // 2 minutes for worship lists
  "/api/profiles": 300, // 5 minutes for profiles
  "/api/stats": 60, // 1 minute for stats
  default: 60, // 1 minute default
};

// Cache entry structure: { data, etag, timestamp, ttl }

/**
 * Generate a cache key from request
 */
function generateCacheKey(req) {
  const baseKey = `${req.method}:${req.originalUrl}`;
  // Include query parameters in key
  return baseKey;
}

/**
 * Generate ETag from response data
 */
function generateETag(data) {
  return crypto.createHash("md5").update(JSON.stringify(data)).digest("hex");
}

/**
 * Check if cache entry is still valid
 */
function isCacheValid(entry) {
  if (!entry) return false;
  const now = Date.now();
  return now - entry.timestamp < entry.ttl * 1000;
}

/**
 * Get TTL for a specific route
 */
function getTTL(path) {
  // Strip query params for TTL lookup
  const basePath = path.split("?")[0];

  // Check for exact match
  if (DEFAULT_TTL[basePath]) {
    return DEFAULT_TTL[basePath];
  }

  // Check for prefix match
  for (const [key, ttl] of Object.entries(DEFAULT_TTL)) {
    if (basePath.startsWith(key)) {
      return ttl;
    }
  }

  return DEFAULT_TTL.default;
}

/**
 * Cache middleware - only caches GET requests
 */
function cacheMiddleware(options = {}) {
  return (req, res, next) => {
    // Only cache GET requests
    if (req.method !== "GET") {
      return next();
    }

    // Skip caching for authenticated user-specific data
    const skipPaths = ["/api/auth/me", "/api/admin/"];
    if (skipPaths.some((path) => req.originalUrl.startsWith(path))) {
      return next();
    }

    const cacheKey = generateCacheKey(req);
    const cachedEntry = cache.get(cacheKey);

    // Check if we have valid cached data
    if (isCacheValid(cachedEntry)) {
      // Check If-None-Match header for conditional request
      const clientETag = req.headers["if-none-match"];
      if (clientETag && clientETag === cachedEntry.etag) {
        return res.status(304).end(); // Not Modified
      }

      // Return cached response
      res.set("X-Cache", "HIT");
      res.set("ETag", cachedEntry.etag);
      res.set(
        "Cache-Control",
        `private, max-age=${Math.floor((cachedEntry.ttl * 1000 - (Date.now() - cachedEntry.timestamp)) / 1000)}`,
      );
      return res.json(cachedEntry.data);
    }

    // Cache miss - capture the response
    const originalJson = res.json.bind(res);

    res.json = (data) => {
      // Only cache successful responses
      if (res.statusCode >= 200 && res.statusCode < 300) {
        const ttl = options.ttl || getTTL(req.originalUrl);
        const etag = generateETag(data);

        cache.set(cacheKey, {
          data,
          etag,
          timestamp: Date.now(),
          ttl,
        });

        res.set("X-Cache", "MISS");
        res.set("ETag", etag);
        res.set("Cache-Control", `private, max-age=${ttl}`);
      }

      return originalJson(data);
    };

    next();
  };
}

/**
 * Invalidate cache entries matching a pattern
 */
function invalidateCache(pattern) {
  if (typeof pattern === "string") {
    // Invalidate specific key
    cache.delete(pattern);

    // Also invalidate any keys that start with pattern
    for (const key of cache.keys()) {
      if (key.includes(pattern)) {
        cache.delete(key);
      }
    }
  } else if (pattern instanceof RegExp) {
    // Invalidate matching pattern
    for (const key of cache.keys()) {
      if (pattern.test(key)) {
        cache.delete(key);
      }
    }
  }
}

/**
 * Invalidation middleware for mutations (POST, PUT, DELETE)
 */
function invalidationMiddleware(req, res, next) {
  // Skip for GET requests
  if (req.method === "GET") {
    return next();
  }

  const originalJson = res.json.bind(res);

  res.json = (data) => {
    // Invalidate related caches on successful mutations
    if (res.statusCode >= 200 && res.statusCode < 300) {
      const basePath = req.baseUrl || "";

      // Invalidate caches based on route
      if (basePath.includes("/songs") || req.originalUrl.includes("/songs")) {
        invalidateCache("/api/songs");
        invalidateCache("/api/stats");
      }
      if (basePath.includes("/lists") || req.originalUrl.includes("/lists")) {
        invalidateCache("/api/lists");
        invalidateCache("/api/stats");
      }
      if (
        basePath.includes("/profiles") ||
        req.originalUrl.includes("/profiles")
      ) {
        invalidateCache("/api/profiles");
        invalidateCache("/api/stats");
      }
    }

    return originalJson(data);
  };

  next();
}

/**
 * Clear all cache entries
 */
function clearCache() {
  cache.clear();
}

/**
 * Get cache statistics
 */
function getCacheStats() {
  let validEntries = 0;
  let expiredEntries = 0;

  for (const entry of cache.values()) {
    if (isCacheValid(entry)) {
      validEntries++;
    } else {
      expiredEntries++;
    }
  }

  return {
    totalEntries: cache.size,
    validEntries,
    expiredEntries,
    keys: Array.from(cache.keys()),
  };
}

/**
 * Periodic cache cleanup (remove expired entries)
 */
function startCacheCleanup(intervalMs = 60000) {
  setInterval(() => {
    const now = Date.now();
    for (const [key, entry] of cache.entries()) {
      if (!isCacheValid(entry)) {
        cache.delete(key);
      }
    }
  }, intervalMs);
}

module.exports = {
  cacheMiddleware,
  invalidationMiddleware,
  invalidateCache,
  clearCache,
  getCacheStats,
  startCacheCleanup,
};
