/**
 * In-memory Map-based cache with TTL support
 *
 * Replaces Redis-backed caching (T16).
 * Uses a simple Map with expiration tracking.
 * Suitable for single-instance deployments and edge functions.
 *
 * @server-only
 */
import "server-only";

const DEFAULT_TTL = 3600; // 1 hour in seconds

/** Maximum entries before oldest are evicted */
const MAX_CACHE_SIZE = 5000;

interface CacheEntry {
  value: string;
  expiresAt: number;
}

const store = new Map<string, CacheEntry>();

/**
 * Evict expired entries and enforce size limit.
 * Called lazily on set to avoid background timers.
 */
function evict(): void {
  const now = Date.now();

  // First pass: remove expired
  for (const [key, entry] of store) {
    if (entry.expiresAt < now) {
      store.delete(key);
    }
  }

  // Second pass: if still over limit, drop oldest (first inserted)
  if (store.size > MAX_CACHE_SIZE) {
    const excess = store.size - MAX_CACHE_SIZE;
    const iterator = store.keys();
    for (let i = 0; i < excess; i++) {
      const { value: key } = iterator.next();
      if (key) store.delete(key);
    }
  }
}

/**
 * Generic caching utility backed by an in-memory Map.
 * API-compatible with the previous Redis-backed cache.
 */
export const cache = {
  /**
   * Retrieve a value from the cache.
   * @param key The cache key
   * @returns The parsed value or null if missing/expired
   */
  async get<T>(key: string): Promise<T | null> {
    const entry = store.get(key);
    if (!entry) return null;

    if (entry.expiresAt < Date.now()) {
      store.delete(key);
      return null;
    }

    try {
      return JSON.parse(entry.value) as T;
    } catch {
      return entry.value as unknown as T;
    }
  },

  /**
   * Store a value in the cache.
   * @param key The cache key
   * @param value The value to store (will be JSON.stringified)
   * @param ttlSeconds Time to live in seconds (default: 3600)
   */
  async set<T>(
    key: string,
    value: T,
    ttlSeconds: number = DEFAULT_TTL,
  ): Promise<void> {
    evict();

    const serialized =
      typeof value === "string" ? value : JSON.stringify(value);
    store.set(key, {
      value: serialized,
      expiresAt: Date.now() + ttlSeconds * 1000,
    });
  },

  /**
   * Delete a value from the cache.
   * @param key The cache key
   */
  async del(key: string): Promise<void> {
    store.delete(key);
  },

  /**
   * Retrieve a value, or fetch it from the source and cache it if missing.
   * This is the most common pattern (Read-Through).
   * @param key The cache key
   * @param fetcher Function to fetch data if cache miss
   * @param ttlSeconds Time to live in seconds
   * @returns The cached or fetched value
   */
  async getOrSet<T>(
    key: string,
    fetcher: () => Promise<T>,
    ttlSeconds: number = DEFAULT_TTL,
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached !== null && cached !== undefined) {
      return cached;
    }

    const fetched = await fetcher();

    // Only cache if value is not null/undefined to avoid caching dead queries
    if (fetched !== null && fetched !== undefined) {
      await this.set(key, fetched, ttlSeconds);
    }

    return fetched;
  },
};

/** Backward-compatible alias */
export const appCache = cache;

/**
 * Cache metrics (lightweight replacement for the Redis-backed version)
 */
export function getCacheMetrics() {
  return {
    total: store.size,
    hits: 0,
    misses: 0,
    hitRatio: null,
    namespaces: {},
  };
}
