/**
 * Redis-like in-memory cache system for handling large datasets
 * Provides advanced caching features similar to Redis but in browser memory
 */
// No policy checks here; persistence controlled via config

export interface RedisLikeCacheConfig {
  maxMemory: number; // Maximum memory in bytes
  evictionPolicy: 'lru' | 'lfu' | 'ttl' | 'random';
  keyExpiry: number; // Default TTL in milliseconds
  compressionEnabled: boolean;
  persistToBrowser: boolean; // Persist to localStorage/IndexedDB
}

export interface CacheEntry<T> {
  key: string;
  value: T;
  timestamp: number;
  accessCount: number;
  lastAccessed: number;
  ttl: number;
  size: number; // Size in bytes
  compressed?: boolean;
}

export interface CacheStats {
  totalKeys: number;
  totalMemory: number;
  hitRate: number;
  missRate: number;
  evictions: number;
  operations: number;
  oldestEntry: number;
  newestEntry: number;
}

export interface HashOperations<T> {
  hset: (field: string, value: T) => void;
  hget: (field: string) => T | null;
  hdel: (field: string) => boolean;
  hgetall: () => Record<string, T>;
  hkeys: () => string[];
  hvals: () => T[];
  hexists: (field: string) => boolean;
}

/**
 * Default cache configuration
 */
export const defaultRedisConfig: RedisLikeCacheConfig = {
  maxMemory: 50 * 1024 * 1024, // 50MB
  evictionPolicy: 'lru',
  keyExpiry: 30 * 60 * 1000, // 30 minutes
  compressionEnabled: false,
  persistToBrowser: true,
};

/**
 * Redis-like cache implementation
 */
export class RedisLikeCache {
  private cache = new Map<string, CacheEntry<unknown>>();
  private config: RedisLikeCacheConfig;
  private stats: CacheStats;
  private accessOrder: string[] = []; // For LRU
  private accessFrequency = new Map<string, number>(); // For LFU

  constructor(config: Partial<RedisLikeCacheConfig> = {}) {
    this.config = { ...defaultRedisConfig, ...config };
    this.stats = {
      totalKeys: 0,
      totalMemory: 0,
      hitRate: 0,
      missRate: 0,
      evictions: 0,
      operations: 0,
      oldestEntry: Date.now(),
      newestEntry: Date.now(),
    };

    this.loadFromBrowser();
    this.startCleanupTimer();
  }

  /**
   * Set a value with optional TTL
   */
  set<T>(key: string, value: T, ttl?: number): boolean {
    try {
      this.stats.operations++;
      
      const serializedValue = JSON.stringify(value);
      const size = new Blob([serializedValue]).size;
      
      // Check memory limit
      if (this.stats.totalMemory + size > this.config.maxMemory) {
        this.evict(size);
      }

      // Replace existing to keep stats consistent
      this.delete(key);

      const entry: CacheEntry<T> = {
        key,
        value,
        timestamp: Date.now(),
        accessCount: 0,
        lastAccessed: Date.now(),
        ttl: ttl ?? this.config.keyExpiry,
        size,
        compressed: false,
      };

      this.cache.set(key, entry as CacheEntry<unknown>);
      this.updateAccessOrder(key);
      this.accessFrequency.set(key, 1);
      
      this.stats.totalKeys++;
      this.stats.totalMemory += size;
      this.stats.newestEntry = Date.now();
      
      this.persistToBrowser();
      return true;
    } catch (error) {
      console.error('Cache set error:', error);
      return false;
    }
  }

  /**
   * Get a value from cache
   */
  get<T>(key: string): T | null {
    this.stats.operations++;
    
    const entry = this.cache.get(key) as CacheEntry<T> | undefined;
    
    if (!entry) {
      this.stats.missRate++;
      return null;
    }

    // Check if expired
    if (Date.now() - entry.timestamp > entry.ttl) {
      this.delete(key);
      this.stats.missRate++;
      return null;
    }

    // Update access stats
    entry.accessCount++;
    entry.lastAccessed = Date.now();
    this.updateAccessOrder(key);
    this.accessFrequency.set(key, (this.accessFrequency.get(key) || 0) + 1);
    
    this.stats.hitRate++;
    return entry.value;
  }

  /**
   * Check if key exists
   */
  exists(key: string): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;
    
    // Check if expired
    if (Date.now() - entry.timestamp > entry.ttl) {
      this.delete(key);
      return false;
    }
    
    return true;
  }

  /**
   * Delete a key
   */
  delete(key: string): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;
    
    this.cache.delete(key);
    this.accessOrder = this.accessOrder.filter(k => k !== key);
    this.accessFrequency.delete(key);
    
    this.stats.totalKeys--;
    this.stats.totalMemory -= entry.size;
    
    this.persistToBrowser();
    return true;
  }

  /**
   * Set expiration time for a key
   */
  expire(key: string, ttl: number): boolean {
    const entry = this.cache.get(key);
    if (!entry) return false;
    
    entry.ttl = ttl;
    entry.timestamp = Date.now(); // Reset timestamp
    return true;
  }

  /**
   * Get remaining TTL for a key
   */
  ttl(key: string): number {
    const entry = this.cache.get(key);
    if (!entry) return -2; // Key doesn't exist
    
    const remaining = entry.ttl - (Date.now() - entry.timestamp);
    return remaining > 0 ? remaining : -1; // Expired
  }

  /**
   * Get all keys matching pattern
   */
  keys(pattern?: string): string[] {
    const allKeys = Array.from(this.cache.keys());
    
    if (!pattern) return allKeys;
    
    // Simple pattern matching (* wildcard)
    const regex = new RegExp(
      pattern.replace(/\*/g, '.*').replace(/\?/g, '.')
    );
    
    return allKeys.filter(key => regex.test(key));
  }

  /**
   * Hash operations for complex data structures
   */
  hash<T>(key: string): HashOperations<T> {
    return {
      hset: (field: string, value: T): void => {
        const hashKey = `${key}:${field}`;
        this.set<T>(hashKey, value);
      },
      hget: (field: string): T | null => {
        const hashKey = `${key}:${field}`;
        return this.get<T>(hashKey);
      },
      hdel: (field: string): boolean => {
        const hashKey = `${key}:${field}`;
        return this.delete(hashKey);
      },
      hgetall: (): Record<string, T> => {
        const pattern = `${key}:*`;
        const keys = this.keys(pattern);
        const result: Record<string, T> = {};
        keys.forEach(k => {
          const field = k.replace(`${key}:`, '');
          const value = this.get<T>(k);
          if (value !== null) {
            result[field] = value as T;
          }
        });
        return result;
      },
      hkeys: (): string[] => {
        const pattern = `${key}:*`;
        return this.keys(pattern).map(k => k.replace(`${key}:`, ''));
      },
      hvals: (): T[] => {
        const pattern = `${key}:*`;
        const keys = this.keys(pattern);
        return keys.map(k => this.get<T>(k)).filter(v => v !== null) as T[];
      },
      hexists: (field: string): boolean => {
        const hashKey = `${key}:${field}`;
        return this.exists(hashKey);
      },
    };
  }

  /**
   * List operations
   */
  lpush<T>(key: string, ...values: T[]): number {
    const list = this.get<T[]>(key) || [];
    list.unshift(...values);
    this.set(key, list);
    return list.length;
  }

  rpush<T>(key: string, ...values: T[]): number {
    const list = this.get<T[]>(key) || [];
    list.push(...values);
    this.set(key, list);
    return list.length;
  }

  lpop<T>(key: string): T | null {
    const list = this.get<T[]>(key);
    if (!list || list.length === 0) return null;
    
    const value = list.shift();
    this.set(key, list);
    return value || null;
  }

  rpop<T>(key: string): T | null {
    const list = this.get<T[]>(key);
    if (!list || list.length === 0) return null;
    
    const value = list.pop();
    this.set(key, list);
    return value || null;
  }

  llen(key: string): number {
    const list = this.get<unknown[]>(key);
    return list ? list.length : 0;
  }

  /**
   * Increment/Decrement operations
   */
  incr(key: string): number {
    const value = this.get<number>(key) || 0;
    const newValue = value + 1;
    this.set(key, newValue);
    return newValue;
  }

  decr(key: string): number {
    const value = this.get<number>(key) || 0;
    const newValue = value - 1;
    this.set(key, newValue);
    return newValue;
  }

  incrby(key: string, increment: number): number {
    const value = this.get<number>(key) || 0;
    const newValue = value + increment;
    this.set(key, newValue);
    return newValue;
  }

  /**
   * Clear all data
   */
  flushall(): void {
    this.cache.clear();
    this.accessOrder = [];
    this.accessFrequency.clear();
    this.stats = {
      ...this.stats,
      totalKeys: 0,
      totalMemory: 0,
    };
    this.persistToBrowser();
  }

  /**
   * Get cache statistics
   */
  info(): CacheStats {
    const operations = this.stats.hitRate + this.stats.missRate;
    return {
      ...this.stats,
      hitRate: operations > 0 ? (this.stats.hitRate / operations) * 100 : 0,
      missRate: operations > 0 ? (this.stats.missRate / operations) * 100 : 0,
    };
  }

  /**
   * Evict entries based on policy
   */
  private evict(neededSpace: number): void {
    let freedSpace = 0;
    
    while (freedSpace < neededSpace && this.cache.size > 0) {
      let keyToEvict: string | null = null;
      
      switch (this.config.evictionPolicy) {
        case 'lru':
          keyToEvict = this.accessOrder[0] || null;
          break;
          
        case 'lfu':
          keyToEvict = this.getLeastFrequentlyUsed();
          break;
          
        case 'ttl':
          keyToEvict = this.getEarliestExpiring();
          break;
          
        case 'random':
          const keys = Array.from(this.cache.keys());
          keyToEvict = keys[Math.floor(Math.random() * keys.length)];
          break;
      }
      
      if (keyToEvict) {
        const entry = this.cache.get(keyToEvict);
        if (entry) {
          freedSpace += entry.size;
          this.delete(keyToEvict);
          this.stats.evictions++;
        }
      } else {
        break; // No more keys to evict
      }
    }
  }

  /**
   * Update access order for LRU
   */
  private updateAccessOrder(key: string): void {
    const index = this.accessOrder.indexOf(key);
    if (index > -1) {
      this.accessOrder.splice(index, 1);
    }
    this.accessOrder.push(key);
  }

  /**
   * Get least frequently used key
   */
  private getLeastFrequentlyUsed(): string | null {
    let minCount = Infinity;
    let leastUsedKey: string | null = null;
    
    this.accessFrequency.forEach((count, key) => {
      if (count < minCount) {
        minCount = count;
        leastUsedKey = key;
      }
    });
    
    return leastUsedKey;
  }

  /**
   * Get key with earliest expiration
   */
  private getEarliestExpiring(): string | null {
    let earliestTime = Infinity;
    let earliestKey: string | null = null;
    
    this.cache.forEach((entry, key) => {
      const expiryTime = entry.timestamp + entry.ttl;
      if (expiryTime < earliestTime) {
        earliestTime = expiryTime;
        earliestKey = key;
      }
    });
    
    return earliestKey;
  }

  /**
   * Cleanup expired entries
   */
  private cleanup(): void {
    const now = Date.now();
    const expiredKeys: string[] = [];
    
    this.cache.forEach((entry, key) => {
      if (now - entry.timestamp > entry.ttl) {
        expiredKeys.push(key);
      }
    });
    
    expiredKeys.forEach(key => this.delete(key));
  }

  /**
   * Start cleanup timer
   */
  private startCleanupTimer(): void {
    setInterval(() => {
      this.cleanup();
    }, 5 * 60 * 1000); // Every 5 minutes
  }

  /**
   * Persist to browser storage
   */
  private persistToBrowser(): void {
    if (!this.config.persistToBrowser) return;
    
    try {
      const cacheData = Array.from(this.cache.entries());
      localStorage.setItem('redis_cache', JSON.stringify(cacheData));
    } catch (error) {
      console.warn('Failed to persist cache to browser:', error);
    }
  }

  /**
   * Load from browser storage
   */
  private loadFromBrowser(): void {
    if (!this.config.persistToBrowser) return;
    
    try {
      const stored = localStorage.getItem('redis_cache');
      if (stored) {
        const cacheData = JSON.parse(stored) as [string, CacheEntry<unknown>][];
        cacheData.forEach(([key, entry]) => {
          // Check if not expired
          if (Date.now() - entry.timestamp <= entry.ttl) {
            this.cache.set(key, entry);
            this.accessOrder.push(key);
            this.accessFrequency.set(key, entry.accessCount);
            this.stats.totalKeys++;
            this.stats.totalMemory += entry.size;
          }
        });
      }
    } catch (error) {
      console.warn('Failed to load cache from browser:', error);
    }
  }
}

/**
 * Global Redis-like cache instance
 */
// Dynamic instance: persistence only takes effect when local storage is enabled.
export const redisCache = new RedisLikeCache({ persistToBrowser: true });

/**
 * Helper functions for common operations
 */
export const cacheHelpers = {
  /**
   * Cache API response
   */
  cacheApiResponse: <T>(endpoint: string, data: T, ttl?: number): void => {
    redisCache.set(`api:${endpoint}`, data, ttl);
  },

  /**
   * Get cached API response
   */
  getCachedApiResponse: <T>(endpoint: string): T | null => {
    return redisCache.get<T>(`api:${endpoint}`);
  },

  /**
   * Cache user data
   */
  cacheUserData: <T>(userId: string, data: T): void => {
    redisCache.hash<T>('users').hset(userId, data);
  },

  /**
   * Get cached user data
   */
  getCachedUserData: <T>(userId: string): T | null => {
    return redisCache.hash<T>('users').hget(userId);
  },

  /**
   * Cache product data with categories
   */
  cacheProductsByCategory: (categoryId: string, products: unknown[], ttl?: number): void => {
    redisCache.set(`products:category:${categoryId}`, products, ttl);
  },

  /**
   * Get cached products by category
   */
  getCachedProductsByCategory: (categoryId: string): unknown[] | null => {
    return redisCache.get<unknown[]>(`products:category:${categoryId}`);
  },

  /**
   * Invalidate cache by pattern
   */
  invalidatePattern: (pattern: string): number => {
    const keys = redisCache.keys(pattern);
    keys.forEach(key => redisCache.delete(key));
    return keys.length;
  },
};

export default RedisLikeCache;