/**
 * LRU Cache System
 * Provides efficient caching for frequently accessed data with automatic eviction.
 *
 * Features:
 * - Configurable max size and TTL (time-to-live)
 * - Automatic eviction of least recently used items
 * - TTL-based expiration for stale data
 * - Statistics tracking for monitoring
 * - Type-safe generic implementation
 */

// ============================================================================
// Types
// ============================================================================

export interface CacheEntry<T> {
  value: T;
  createdAt: number;
  lastAccessedAt: number;
  accessCount: number;
}

export interface CacheStats {
  size: number;
  maxSize: number;
  hits: number;
  misses: number;
  hitRate: number;
  evictions: number;
  expirations: number;
}

export interface CacheOptions {
  maxSize?: number;
  ttlMs?: number;
  onEvict?: (key: string, value: unknown) => void;
}

// ============================================================================
// LRUCache Class
// ============================================================================

/**
 * LRU (Least Recently Used) Cache with TTL support.
 *
 * When the cache reaches maxSize, the least recently accessed items are evicted.
 * Items also expire after ttlMs milliseconds if set.
 */
export class LRUCache<T = unknown> {
  private cache: Map<string, CacheEntry<T>>;
  private maxSize: number;
  private ttlMs: number | null;
  private onEvict: ((key: string, value: T) => void) | null;

  // Statistics
  private hits: number = 0;
  private misses: number = 0;
  private evictions: number = 0;
  private expirations: number = 0;

  /**
   * Create a new LRU cache.
   *
   * @param maxSize - Maximum number of entries (default: 1000)
   * @param ttlMs - Time-to-live in milliseconds (default: null = no expiration)
   * @param onEvict - Optional callback when an entry is evicted
   */
  constructor(options: CacheOptions = {}) {
    this.maxSize = options.maxSize ?? 1000;
    this.ttlMs = options.ttlMs ?? null;
    this.onEvict = options.onEvict as ((key: string, value: T) => void) | null ?? null;
    this.cache = new Map();
  }

  /**
   * Get a value from the cache.
   * Returns undefined if the key doesn't exist or has expired.
   *
   * @param key - Cache key
   * @returns Cached value or undefined
   */
  get(key: string): T | undefined {
    const entry = this.cache.get(key);

    if (!entry) {
      this.misses++;
      return undefined;
    }

    // Check TTL expiration
    if (this.isExpired(entry)) {
      this.delete(key);
      this.expirations++;
      this.misses++;
      return undefined;
    }

    // Update access time and move to end (most recently used)
    entry.lastAccessedAt = Date.now();
    entry.accessCount++;

    // Re-insert to move to end of Map (maintains LRU order)
    this.cache.delete(key);
    this.cache.set(key, entry);

    this.hits++;
    return entry.value;
  }

  /**
   * Set a value in the cache.
   * If the cache is full, the least recently used item is evicted.
   *
   * @param key - Cache key
   * @param value - Value to cache
   * @returns The cache instance for chaining
   */
  set(key: string, value: T): this {
    // Check if key already exists
    if (this.cache.has(key)) {
      // Update existing entry
      const entry = this.cache.get(key)!;
      entry.value = value;
      entry.lastAccessedAt = Date.now();
      entry.accessCount++;

      // Re-insert to move to end
      this.cache.delete(key);
      this.cache.set(key, entry);
      return this;
    }

    // Evict if at max size
    if (this.cache.size >= this.maxSize) {
      this.evictLRU();
    }

    // Add new entry
    const now = Date.now();
    this.cache.set(key, {
      value,
      createdAt: now,
      lastAccessedAt: now,
      accessCount: 1,
    });

    return this;
  }

  /**
   * Check if a key exists in the cache (without updating access time).
   *
   * @param key - Cache key
   * @returns true if key exists and is not expired
   */
  has(key: string): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;

    if (this.isExpired(entry)) {
      this.delete(key);
      this.expirations++;
      return false;
    }

    return true;
  }

  /**
   * Delete a key from the cache.
   *
   * @param key - Cache key
   * @returns true if the key was deleted
   */
  delete(key: string): boolean {
    const entry = this.cache.get(key);
    if (entry && this.onEvict) {
      this.onEvict(key, entry.value);
    }
    return this.cache.delete(key);
  }

  /**
   * Clear all entries from the cache.
   */
  clear(): void {
    if (this.onEvict) {
      for (const [key, entry] of this.cache.entries()) {
        this.onEvict(key, entry.value);
      }
    }
    this.cache.clear();
    this.hits = 0;
    this.misses = 0;
    this.evictions = 0;
    this.expirations = 0;
  }

  /**
   * Get the current size of the cache.
   */
  get size(): number {
    return this.cache.size;
  }

  /**
   * Get all keys in the cache.
   */
  keys(): string[] {
    return Array.from(this.cache.keys());
  }

  /**
   * Get all values in the cache.
   */
  values(): T[] {
    return Array.from(this.cache.values()).map((entry) => entry.value);
  }

  /**
   * Get cache statistics.
   */
  getStats(): CacheStats {
    const totalRequests = this.hits + this.misses;
    return {
      size: this.cache.size,
      maxSize: this.maxSize,
      hits: this.hits,
      misses: this.misses,
      hitRate: totalRequests > 0 ? this.hits / totalRequests : 0,
      evictions: this.evictions,
      expirations: this.expirations,
    };
  }

  /**
   * Remove expired entries from the cache.
   *
   * @returns Number of expired entries removed
   */
  cleanup(): number {
    if (!this.ttlMs) return 0;

    let removed = 0;
    const now = Date.now();

    for (const [key, entry] of this.cache.entries()) {
      if (now - entry.createdAt > this.ttlMs) {
        this.delete(key);
        this.expirations++;
        removed++;
      }
    }

    return removed;
  }

  /**
   * Get or set a value using a factory function.
   * If the key doesn't exist, calls the factory and caches the result.
   *
   * @param key - Cache key
   * @param factory - Function to create the value if not cached
   * @returns The cached or newly created value
   */
  async getOrSet(key: string, factory: () => T | Promise<T>): Promise<T> {
    const cached = this.get(key);
    if (cached !== undefined) {
      return cached;
    }

    const value = await factory();
    this.set(key, value);
    return value;
  }

  /**
   * Get or set a value synchronously using a factory function.
   *
   * @param key - Cache key
   * @param factory - Synchronous function to create the value if not cached
   * @returns The cached or newly created value
   */
  getOrSetSync(key: string, factory: () => T): T {
    const cached = this.get(key);
    if (cached !== undefined) {
      return cached;
    }

    const value = factory();
    this.set(key, value);
    return value;
  }

  // ============================================================================
  // Private Methods
  // ============================================================================

  /**
   * Check if an entry has expired.
   */
  private isExpired(entry: CacheEntry<T>): boolean {
    if (!this.ttlMs) return false;
    return Date.now() - entry.createdAt > this.ttlMs;
  }

  /**
   * Evict the least recently used entry.
   */
  private evictLRU(): void {
    // Map maintains insertion order, first key is the LRU
    const firstKey = this.cache.keys().next().value;
    if (firstKey !== undefined) {
      this.delete(firstKey);
      this.evictions++;
    }
  }
}

// ============================================================================
// Specialized Cache Classes
// ============================================================================

/**
 * Cache specifically for database query results.
 * Includes query normalization and result serialization.
 */
export class QueryCache extends LRUCache<unknown> {
  /**
   * Create a cache key from a query and parameters.
   */
  static createKey(query: string, params?: unknown[]): string {
    const normalizedQuery = query.replace(/\s+/g, ' ').trim().toLowerCase();
    const paramsStr = params ? JSON.stringify(params) : '';
    return `${normalizedQuery}:${paramsStr}`;
  }

  /**
   * Get cached query result.
   */
  getQuery<R>(query: string, params?: unknown[]): R | undefined {
    const key = QueryCache.createKey(query, params);
    return this.get(key) as R | undefined;
  }

  /**
   * Cache a query result.
   */
  setQuery<R>(query: string, params: unknown[] | undefined, result: R): this {
    const key = QueryCache.createKey(query, params);
    this.set(key, result);
    return this;
  }

  /**
   * Invalidate all cached queries matching a pattern.
   */
  invalidatePattern(pattern: string | RegExp): number {
    const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
    let invalidated = 0;

    for (const key of this.keys()) {
      if (regex.test(key)) {
        this.delete(key);
        invalidated++;
      }
    }

    return invalidated;
  }
}

/**
 * Cache for player data with player-specific invalidation.
 */
export class PlayerDataCache extends LRUCache<unknown> {
  private playerKeys: Map<string, Set<string>> = new Map();

  /**
   * Cache data for a specific player.
   */
  setPlayerData<T>(playerId: string, dataType: string, data: T): this {
    const key = `player:${playerId}:${dataType}`;
    this.set(key, data);

    // Track key for this player
    let playerKeySet = this.playerKeys.get(playerId);
    if (!playerKeySet) {
      playerKeySet = new Set();
      this.playerKeys.set(playerId, playerKeySet);
    }
    playerKeySet.add(key);

    return this;
  }

  /**
   * Get cached data for a specific player.
   */
  getPlayerData<T>(playerId: string, dataType: string): T | undefined {
    const key = `player:${playerId}:${dataType}`;
    return this.get(key) as T | undefined;
  }

  /**
   * Invalidate all cached data for a player.
   */
  invalidatePlayer(playerId: string): number {
    const playerKeySet = this.playerKeys.get(playerId);
    if (!playerKeySet) return 0;

    let invalidated = 0;
    for (const key of playerKeySet) {
      if (this.delete(key)) {
        invalidated++;
      }
    }

    this.playerKeys.delete(playerId);
    return invalidated;
  }

  /**
   * Invalidate specific data type for a player.
   */
  invalidatePlayerData(playerId: string, dataType: string): boolean {
    const key = `player:${playerId}:${dataType}`;
    const deleted = this.delete(key);

    const playerKeySet = this.playerKeys.get(playerId);
    if (playerKeySet) {
      playerKeySet.delete(key);
    }

    return deleted;
  }
}

// ============================================================================
// Global Cache Instances
// ============================================================================

let queryCache: QueryCache | null = null;
let playerDataCache: PlayerDataCache | null = null;
let generalCache: LRUCache | null = null;

/**
 * Get the global query cache instance.
 *
 * @param options - Cache options (only used on first call)
 */
export function getQueryCache(options?: CacheOptions): QueryCache {
  if (!queryCache) {
    queryCache = new QueryCache({
      maxSize: options?.maxSize ?? 500,
      ttlMs: options?.ttlMs ?? 5 * 60 * 1000, // 5 minutes default
      ...options,
    });
  }
  return queryCache;
}

/**
 * Get the global player data cache instance.
 *
 * @param options - Cache options (only used on first call)
 */
export function getPlayerDataCache(options?: CacheOptions): PlayerDataCache {
  if (!playerDataCache) {
    playerDataCache = new PlayerDataCache({
      maxSize: options?.maxSize ?? 1000,
      ttlMs: options?.ttlMs ?? 10 * 60 * 1000, // 10 minutes default
      ...options,
    });
  }
  return playerDataCache;
}

/**
 * Get the global general-purpose cache instance.
 *
 * @param options - Cache options (only used on first call)
 */
export function getGeneralCache(options?: CacheOptions): LRUCache {
  if (!generalCache) {
    generalCache = new LRUCache({
      maxSize: options?.maxSize ?? 1000,
      ttlMs: options?.ttlMs ?? 30 * 60 * 1000, // 30 minutes default
      ...options,
    });
  }
  return generalCache;
}

// ============================================================================
// Helper Functions
// ============================================================================

/**
 * Create a memoized version of an async function using the cache.
 *
 * @param fn - Function to memoize
 * @param keyFn - Function to generate cache key from arguments
 * @param cache - Cache instance to use (defaults to general cache)
 * @returns Memoized function
 */
export function memoize<Args extends unknown[], R>(
  fn: (...args: Args) => Promise<R>,
  keyFn: (...args: Args) => string,
  cache: LRUCache<R> = getGeneralCache() as LRUCache<R>
): (...args: Args) => Promise<R> {
  return async (...args: Args): Promise<R> => {
    const key = keyFn(...args);
    return cache.getOrSet(key, () => fn(...args));
  };
}

/**
 * Create a memoized version of a sync function using the cache.
 *
 * @param fn - Function to memoize
 * @param keyFn - Function to generate cache key from arguments
 * @param cache - Cache instance to use (defaults to general cache)
 * @returns Memoized function
 */
export function memoizeSync<Args extends unknown[], R>(
  fn: (...args: Args) => R,
  keyFn: (...args: Args) => string,
  cache: LRUCache<R> = getGeneralCache() as LRUCache<R>
): (...args: Args) => R {
  return (...args: Args): R => {
    const key = keyFn(...args);
    return cache.getOrSetSync(key, () => fn(...args));
  };
}

/**
 * Clear all global cache instances.
 */
export function clearAllCaches(): void {
  queryCache?.clear();
  playerDataCache?.clear();
  generalCache?.clear();
}

/**
 * Get statistics for all global caches.
 */
export function getAllCacheStats(): Record<string, CacheStats> {
  return {
    query: queryCache?.getStats() ?? { size: 0, maxSize: 0, hits: 0, misses: 0, hitRate: 0, evictions: 0, expirations: 0 },
    playerData: playerDataCache?.getStats() ?? { size: 0, maxSize: 0, hits: 0, misses: 0, hitRate: 0, evictions: 0, expirations: 0 },
    general: generalCache?.getStats() ?? { size: 0, maxSize: 0, hits: 0, misses: 0, hitRate: 0, evictions: 0, expirations: 0 },
  };
}

/**
 * Log cache statistics to console.
 */
export function logCacheStats(): void {
  const stats = getAllCacheStats();
  console.log('[Cache] Statistics:');
  for (const [name, stat] of Object.entries(stats)) {
    console.log(
      `  ${name}: size=${stat.size}/${stat.maxSize}, ` +
      `hits=${stat.hits}, misses=${stat.misses}, ` +
      `hitRate=${(stat.hitRate * 100).toFixed(1)}%, ` +
      `evictions=${stat.evictions}, expirations=${stat.expirations}`
    );
  }
}

// ============================================================================
// Exports
// ============================================================================

export const cacheService = {
  LRUCache,
  QueryCache,
  PlayerDataCache,
  getQueryCache,
  getPlayerDataCache,
  getGeneralCache,
  memoize,
  memoizeSync,
  clearAllCaches,
  getAllCacheStats,
  logCacheStats,
};
