import Redis from 'ioredis';
import { RedisConnectionManager } from './connection-manager';
import type { CacheOptions, CacheProvider, CacheStats } from './types';

/**
 * Simplified Redis-based cache provider using connection manager
 */
export class RedisCache implements CacheProvider {
  private redis: Redis;
  private logger: any;
  private defaultTTL: number;
  private keyPrefix: string;
  private enableMetrics: boolean;
  private isConnected = false;
  private startTime = Date.now();
  private connectionManager: RedisConnectionManager;

  private stats: CacheStats = {
    hits: 0,
    misses: 0,
    errors: 0,
    hitRate: 0,
    total: 0,
    uptime: 0,
  };

  constructor(options: CacheOptions) {
    this.defaultTTL = options.ttl ?? 3600; // 1 hour default
    this.keyPrefix = options.keyPrefix ?? 'cache:';
    this.enableMetrics = options.enableMetrics ?? true;
    this.logger = options.logger || console; // Use provided logger or console as fallback

    // Get connection manager instance
    this.connectionManager = RedisConnectionManager.getInstance();

    // Generate connection name based on cache type
    const baseName =
      options.name ||
      this.keyPrefix
        .replace(':', '')
        .replace(/[^a-zA-Z0-9]/g, '')
        .toUpperCase() ||
      'CACHE';

    // Get Redis connection (shared by default for cache)
    this.redis = this.connectionManager.getConnection({
      name: `${baseName}-SERVICE`,
      singleton: options.shared ?? true, // Default to shared connection for cache
      redisConfig: options.redisConfig,
      logger: this.logger,
    });

    // Only setup event handlers for non-shared connections to avoid memory leaks
    if (!(options.shared ?? true)) {
      this.setupEventHandlers();
    } else {
      // For shared connections, just monitor the connection status without adding handlers
      this.isConnected = this.redis.status === 'ready';
    }
  }

  private setupEventHandlers(): void {
    this.redis.on('connect', () => {
      this.logger.info('Redis cache connected');
    });

    this.redis.on('ready', () => {
      this.isConnected = true;
      this.logger.info('Redis cache ready');
    });

    this.redis.on('error', (error: Error) => {
      this.isConnected = false;
      this.logger.error('Redis cache connection error', { error: error.message });
    });

    this.redis.on('close', () => {
      this.isConnected = false;
      this.logger.warn('Redis cache connection closed');
    });

    this.redis.on('reconnecting', () => {
      this.logger.warn('Redis cache reconnecting...');
    });
  }

  private getKey(key: string): string {
    return `${this.keyPrefix}${key}`;
  }

  private updateStats(hit: boolean, error = false): void {
    if (!this.enableMetrics) {
      return;
    }

    if (error) {
      this.stats.errors++;
    } else if (hit) {
      this.stats.hits++;
    } else {
      this.stats.misses++;
    }

    this.stats.total = this.stats.hits + this.stats.misses;
    this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0;
    this.stats.uptime = Date.now() - this.startTime;
  }

  private async safeExecute<T>(
    operation: () => Promise<T>,
    fallback: T,
    operationName: string
  ): Promise<T> {
    try {
      if (!this.isReady()) {
        this.logger.warn(`Redis not ready for ${operationName}, using fallback`);
        this.updateStats(false, true);
        return fallback;
      }
      return await operation();
    } catch (error) {
      this.logger.error(`Redis ${operationName} failed`, {
        error: error instanceof Error ? error.message : String(error),
      });
      this.updateStats(false, true);
      return fallback;
    }
  }

  async get<T>(key: string): Promise<T | null> {
    return this.safeExecute(
      async () => {
        const fullKey = this.getKey(key);
        const value = await this.redis.get(fullKey);

        if (value === null) {
          this.updateStats(false);
          this.logger.debug('Cache miss', { key });
          return null;
        }

        this.updateStats(true);
        this.logger.debug('Cache hit', { key });

        try {
          return JSON.parse(value) as T;
        } catch {
          // If parsing fails, return as string
          return value as unknown as T;
        }
      },
      null,
      'get'
    );
  }

  async set<T>(
    key: string,
    value: T,
    options?:
      | number
      | {
          ttl?: number;
          preserveTTL?: boolean;
          onlyIfExists?: boolean;
          onlyIfNotExists?: boolean;
          getOldValue?: boolean;
        }
  ): Promise<T | null> {
    return this.safeExecute(
      async () => {
        const fullKey = this.getKey(key);
        const serialized = typeof value === 'string' ? value : JSON.stringify(value);

        // Handle backward compatibility - if options is a number, treat as TTL
        const config = typeof options === 'number' ? { ttl: options } : options || {};

        let oldValue: T | null = null;

        // Get old value if requested
        if (config.getOldValue) {
          const existingValue = await this.redis.get(fullKey);
          if (existingValue !== null) {
            try {
              oldValue = JSON.parse(existingValue) as T;
            } catch {
              oldValue = existingValue as unknown as T;
            }
          }
        }

        // Handle preserveTTL logic
        if (config.preserveTTL) {
          const currentTTL = await this.redis.ttl(fullKey);

          if (currentTTL === -2) {
            // Key doesn't exist
            if (config.onlyIfExists) {
              this.logger.debug('Set skipped - key does not exist and onlyIfExists is true', {
                key,
              });
              return oldValue;
            }
            // Set with default or specified TTL
            const ttl = config.ttl ?? this.defaultTTL;
            await this.redis.setex(fullKey, ttl, serialized);
            this.logger.debug('Cache set with new TTL (key did not exist)', { key, ttl });
          } else if (currentTTL === -1) {
            // Key exists but has no expiry - preserve the no-expiry state
            await this.redis.set(fullKey, serialized);
            this.logger.debug('Cache set preserving no-expiry', { key });
          } else {
            // Key exists with TTL - preserve it
            await this.redis.setex(fullKey, currentTTL, serialized);
            this.logger.debug('Cache set preserving existing TTL', { key, ttl: currentTTL });
          }
        } else {
          // Standard set logic with conditional operations
          if (config.onlyIfExists && config.onlyIfNotExists) {
            throw new Error('Cannot specify both onlyIfExists and onlyIfNotExists');
          }

          if (config.onlyIfExists) {
            // Only set if key exists (XX flag)
            const ttl = config.ttl ?? this.defaultTTL;
            const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX');
            if (result === null) {
              this.logger.debug('Set skipped - key does not exist', { key });
              return oldValue;
            }
          } else if (config.onlyIfNotExists) {
            // Only set if key doesn't exist (NX flag)
            const ttl = config.ttl ?? this.defaultTTL;
            const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'NX');
            if (result === null) {
              this.logger.debug('Set skipped - key already exists', { key });
              return oldValue;
            }
          } else {
            // Standard set
            const ttl = config.ttl ?? this.defaultTTL;
            await this.redis.setex(fullKey, ttl, serialized);
          }

          this.logger.debug('Cache set', { key, ttl: config.ttl ?? this.defaultTTL });
        }

        return oldValue;
      },
      null,
      'set'
    );
  }

  async del(key: string): Promise<void> {
    await this.safeExecute(
      async () => {
        const fullKey = this.getKey(key);
        await this.redis.del(fullKey);
        this.logger.debug('Cache delete', { key });
      },
      undefined,
      'del'
    );
  }

  async exists(key: string): Promise<boolean> {
    return this.safeExecute(
      async () => {
        const fullKey = this.getKey(key);
        const result = await this.redis.exists(fullKey);
        return result === 1;
      },
      false,
      'exists'
    );
  }

  async clear(): Promise<void> {
    await this.safeExecute(
      async () => {
        const pattern = `${this.keyPrefix}*`;
        const keys = await this.redis.keys(pattern);
        if (keys.length > 0) {
          await this.redis.del(...keys);
          this.logger.warn('Cache cleared', { keysDeleted: keys.length });
        }
      },
      undefined,
      'clear'
    );
  }

  async keys(pattern: string): Promise<string[]> {
    return this.safeExecute(
      async () => {
        const fullPattern = `${this.keyPrefix}${pattern}`;
        const keys = await this.redis.keys(fullPattern);
        // Remove the prefix from returned keys to match the interface expectation
        return keys.map(key => key.replace(this.keyPrefix, ''));
      },
      [],
      'keys'
    );
  }

  async health(): Promise<boolean> {
    try {
      const pong = await this.redis.ping();
      return pong === 'PONG';
    } catch (error) {
      this.logger.error('Redis health check failed', {
        error: error instanceof Error ? error.message : String(error),
      });
      return false;
    }
  }

  getStats(): CacheStats {
    return {
      ...this.stats,
      uptime: Date.now() - this.startTime,
    };
  }

  async waitForReady(timeout = 5000): Promise<void> {
    return new Promise((resolve, reject) => {
      if (this.redis.status === 'ready') {
        resolve();
        return;
      }

      const timeoutId = setTimeout(() => {
        reject(new Error(`Redis connection timeout after ${timeout}ms`));
      }, timeout);

      this.redis.once('ready', () => {
        clearTimeout(timeoutId);
        resolve();
      });

      this.redis.once('error', error => {
        clearTimeout(timeoutId);
        reject(error);
      });
    });
  }

  isReady(): boolean {
    // Always check the actual Redis connection status
    const ready = this.redis.status === 'ready';

    // Update local flag if we're not using shared connection
    if (this.isConnected !== ready) {
      this.isConnected = ready;
    }

    return ready;
  }

  // Enhanced convenience methods
  async update<T>(key: string, value: T): Promise<T | null> {
    return this.set(key, value, { preserveTTL: true, getOldValue: true });
  }

  async setIfExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
    const result = await this.set(key, value, { ttl, onlyIfExists: true });
    return result !== null || (await this.exists(key));
  }

  async setIfNotExists<T>(key: string, value: T, ttl?: number): Promise<boolean> {
    const oldValue = await this.set(key, value, { ttl, onlyIfNotExists: true, getOldValue: true });
    return oldValue === null; // Returns true if key didn't exist before
  }

  async replace<T>(key: string, value: T, ttl?: number): Promise<T | null> {
    return this.set(key, value, { ttl, onlyIfExists: true, getOldValue: true });
  }

  // Atomic update with transformation
  async updateField<T>(
    key: string,
    updater: (current: T | null) => T,
    ttl?: number
  ): Promise<T | null> {
    return this.safeExecute(
      async () => {
        const fullKey = this.getKey(key);

        // Use Lua script for atomic read-modify-write
        const luaScript = `
          local key = KEYS[1]
          
          -- Get current value and TTL
          local current_value = redis.call('GET', key)
          local current_ttl = redis.call('TTL', key)
          
          -- Return current value for processing
          return {current_value, current_ttl}
        `;

        const [currentValue, currentTTL] = (await this.redis.eval(luaScript, 1, fullKey)) as [
          string | null,
          number,
        ];

        // Parse current value
        let parsed: T | null = null;
        if (currentValue !== null) {
          try {
            parsed = JSON.parse(currentValue) as T;
          } catch {
            parsed = currentValue as unknown as T;
          }
        }

        // Apply updater function
        const newValue = updater(parsed);

        // Set the new value with appropriate TTL logic
        if (ttl !== undefined) {
          // Use specified TTL
          await this.set(key, newValue, ttl);
        } else if (currentTTL === -2) {
          // Key didn't exist, use default TTL
          await this.set(key, newValue);
        } else {
          // Preserve existing TTL
          await this.set(key, newValue, { preserveTTL: true });
        }

        return parsed;
      },
      null,
      'updateField'
    );
  }
}
