// Import NestJS decorators and utilities
import { Injectable, Inject } from '@nestjs/common';

// Import Redis client type from ioredis
import { Redis } from 'ioredis';

/**
 * RedisService - Service layer for Redis operations and abstraction
 *
 * This service provides a clean, typed interface for Redis operations, abstracting
 * away the raw Redis client and providing consistent patterns for different use cases.
 *
 * Key Features:
 * - Session management with TTL
 * - Generic caching with/without expiration
 * - Shared data storage for cross-platform communication
 * - Pub/Sub for real-time messaging
 * - Namespacing with key prefixes for organization
 *
 * Design Patterns:
 * - Facade Pattern: Simplifies complex Redis operations
 * - Repository Pattern: Abstracts data access layer
 * - Strategy Pattern: Different methods for different data types
 *
 * Namespace Convention:
 * - session:{id}: User session data
 * - cache:{key}: Application cache
 * - shared:{key}: Cross-platform shared data
 */
@Injectable()
export class RedisService {
  /**
   * Constructor with dependency injection
   *
   * @param redis - Redis client instance injected using custom token 'REDIS_CLIENT'
   *                This client is configured in RedisModule factory provider
   */
  constructor(@Inject('REDIS_CLIENT') private readonly redis: Redis) {}

  // ==================== SESSION MANAGEMENT METHODS ====================

  /**
   * setSession - Stores user session data with automatic expiration
   *
   * @param sessionId - Unique session identifier (typically from cookies/tokens)
   * @param data - Session data object (will be JSON serialized)
   * @param ttl - Time-to-live in seconds (default: 1 hour = 3600 seconds)
   *
   * Key Pattern: session:{sessionId}
   * Use Case: User authentication sessions, shopping carts, user preferences
   *
   * Example:
   * setSession('abc123', { userId: 1, roles: ['admin'] }, 7200)
   * -> Stores at key: session:abc123, expires in 2 hours
   */
  async setSession(
    sessionId: string,
    data: any,
    ttl: number = 3600,
  ): Promise<void> {
    const sessionKey = `session:${sessionId}`;
    await this.redis.setex(sessionKey, ttl, JSON.stringify(data));
  }

  /**
   * getSession - Retrieves user session data
   *
   * @param sessionId - Unique session identifier
   * @returns Parsed session data or null if expired/not found
   *
   * Note: Automatically handles JSON parsing
   */
  async getSession(sessionId: string): Promise<any> {
    const sessionKey = `session:${sessionId}`;
    const data = await this.redis.get(sessionKey);
    return data ? JSON.parse(data) : null;
  }

  /**
   * deleteSession - Removes session data (logout/expire)
   *
   * @param sessionId - Session identifier to delete
   *
   * Use Case: User logout, session invalidation, security cleanup
   */
  async deleteSession(sessionId: string): Promise<void> {
    const sessionKey = `session:${sessionId}`;
    await this.redis.del(sessionKey);
  }

  /**
   * refreshSession - Extends session TTL without modifying data
   *
   * @param sessionId - Session identifier to refresh
   * @param ttl - New TTL in seconds (default: 1 hour)
   *
   * Use Case: Sliding expiration - reset timer on user activity
   *
   * Note: Uses Redis EXPIRE command (only changes TTL, not data)
   */
  async refreshSession(sessionId: string, ttl: number = 3600): Promise<void> {
    const sessionKey = `session:${sessionId}`;
    await this.redis.expire(sessionKey, ttl);
  }

  // ==================== GENERIC CACHE METHODS ====================

  /**
   * set - Generic cache setter with optional TTL
   *
   * @param key - Cache key (will be prefixed with 'cache:')
   * @param value - Data to cache (JSON serialized)
   * @param ttl - Optional TTL in seconds (undefined = no expiration)
   *
   * Key Pattern: cache:{key}
   * Use Case: Database query results, computed values, API responses
   *
   * Note: Distinguishes from sessions - different namespace, different use cases
   */
  async set(key: string, value: any, ttl?: number): Promise<void> {
    const cacheKey = `cache:${key}`;
    if (ttl) {
      // Set with expiration
      await this.redis.setex(cacheKey, ttl, JSON.stringify(value));
    } else {
      // Set without expiration (persistent until manually deleted)
      await this.redis.set(cacheKey, JSON.stringify(value));
    }
  }

  /**
   * get - Generic cache getter
   *
   * @param key - Cache key (without 'cache:' prefix)
   * @returns Parsed cached data or null
   */
  async get(key: string): Promise<any> {
    const cacheKey = `cache:${key}`;
    const data = await this.redis.get(cacheKey);
    return data ? JSON.parse(data) : null;
  }

  /**
   * del - Remove item from cache
   *
   * @param key - Cache key to delete
   *
   * Use Case: Cache invalidation when underlying data changes
   */
  async del(key: string): Promise<void> {
    const cacheKey = `cache:${key}`;
    await this.redis.del(cacheKey);
  }

  /**
   * exists - Check if cache key exists
   *
   * @param key - Cache key to check
   * @returns boolean indicating existence
   *
   * Use Case: Cache hit/miss tracking, conditional caching
   */
  async exists(key: string): Promise<boolean> {
    const cacheKey = `cache:${key}`;
    return (await this.redis.exists(cacheKey)) === 1;
  }

  // ==================== SHARED DATA METHODS ====================

  /**
   * setSharedData - Stores data for cross-platform/application sharing
   *
   * @param key - Shared data key (prefixed with 'shared:')
   * @param data - Data to share (JSON serialized)
   * @param ttl - Optional TTL in seconds
   *
   * Key Pattern: shared:{key}
   * Use Case: Sharing data between different services/applications
   * Examples: Feature flags, configuration, real-time counters
   */
  async setSharedData(key: string, data: any, ttl?: number): Promise<void> {
    const sharedKey = `shared:${key}`;
    if (ttl) {
      await this.redis.setex(sharedKey, ttl, JSON.stringify(data));
    } else {
      await this.redis.set(sharedKey, JSON.stringify(data));
    }
  }

  /**
   * getSharedData - Retrieves cross-platform shared data
   *
   * @param key - Shared data key
   * @returns Parsed shared data or null
   */
  async getSharedData(key: string): Promise<any> {
    const sharedKey = `shared:${key}`;
    const data = await this.redis.get(sharedKey);
    return data ? JSON.parse(data) : null;
  }

  // ==================== PUB/SUB METHODS ====================

  /**
   * publish - Publishes message to a Redis channel
   *
   * @param channel - Channel name to publish to
   * @param message - Message data (JSON serialized)
   *
   * Use Case: Real-time notifications, event broadcasting, cross-service communication
   * Example: publish('user:login', { userId: 1, timestamp: '...' })
   */
  async publish(channel: string, message: any): Promise<void> {
    await this.redis.publish(channel, JSON.stringify(message));
  }

  /**
   * subscribe - Subscribes to Redis channel with callback
   *
   * @param channel - Channel name to subscribe to
   * @param callback - Function to call when message received
   *
   * Implementation Details:
   * - Creates duplicate Redis client for subscription (required by Redis)
   * - Automatically parses JSON messages
   * - Only triggers callback for specified channel
   *
   * Use Case: Real-time dashboard updates, chat applications, event listeners
   *
   * Warning: This creates a long-lived connection. Consider connection management
   * for production use (reconnection logic, error handling, cleanup).
   */
  subscribe(channel: string, callback: (message: any) => void): void {
    // Create duplicate client for subscription (required by Redis pub/sub)
    const subscriber = this.redis.duplicate();

    // Subscribe to channel
    subscriber.subscribe(channel);

    // Listen for messages
    subscriber.on('message', (receivedChannel, message) => {
      // Filter by channel (client might subscribe to multiple channels)
      if (receivedChannel === channel) {
        // Parse and deliver message
        callback(JSON.parse(message));
      }
    });
  }
}
