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

/**
 * Simplified Redis-based cache provider
 */
export class RedisCache implements CacheProvider {
  private redis: Redis;
  private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } =
    console;
  private defaultTTL: number;
  private keyPrefix: string;
  private stats: CacheStats = {
    hits: 0,
    misses: 0,
    errors: 0,
    hitRate: 0,
    total: 0,
    uptime: 0,
  };
  private startTime = Date.now();

  constructor(options: CacheOptions) {
    this.defaultTTL = options.ttl ?? CACHE_DEFAULTS.TTL;
    this.keyPrefix = options.keyPrefix ?? CACHE_DEFAULTS.KEY_PREFIX;
    this.logger = options.logger || console;

    const manager = RedisConnectionManager.getInstance();
    const name = options.name || 'CACHE';

    this.redis = manager.getConnection({
      name: `${name}-SERVICE`,
      singleton: options.shared ?? true,
      redisConfig: options.redisConfig,
      logger: this.logger,
    });
  }

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

  private updateStats(hit: boolean, error = false): void {
    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;
  }

  async get<T>(key: string): Promise<T | null> {
    try {
      const value = await this.redis.get(this.getKey(key));
      if (value === null) {
        this.updateStats(false);
        return null;
      }
      this.updateStats(true);
      return JSON.parse(value);
    } catch {
      this.updateStats(false, true);
      return null;
    }
  }

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

      let oldValue: T | null = null;
      if (opts.getOldValue) {
        const existing = await this.redis.get(fullKey);
        if (existing) {
          oldValue = JSON.parse(existing);
        }
      }

      const ttl = opts.ttl ?? this.defaultTTL;

      if (opts.onlyIfExists) {
        const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'XX');
        if (!result) {
          return oldValue;
        }
      } else if (opts.onlyIfNotExists) {
        const result = await this.redis.set(fullKey, serialized, 'EX', ttl, 'NX');
        if (!result) {
          return oldValue;
        }
      } else if (opts.preserveTTL) {
        const currentTTL = await this.redis.ttl(fullKey);
        if (currentTTL > 0) {
          await this.redis.setex(fullKey, currentTTL, serialized);
        } else {
          await this.redis.setex(fullKey, ttl, serialized);
        }
      } else {
        await this.redis.setex(fullKey, ttl, serialized);
      }

      return oldValue;
    } catch (error) {
      this.updateStats(false, true);
      throw error;
    }
  }

  async del(key: string): Promise<void> {
    try {
      await this.redis.del(this.getKey(key));
    } catch (error) {
      this.updateStats(false, true);
      throw error;
    }
  }

  async exists(key: string): Promise<boolean> {
    try {
      return (await this.redis.exists(this.getKey(key))) === 1;
    } catch {
      this.updateStats(false, true);
      return false;
    }
  }

  async clear(): Promise<void> {
    try {
      const stream = this.redis.scanStream({
        match: `${this.keyPrefix}*`,
        count: CACHE_DEFAULTS.SCAN_COUNT,
      });

      const pipeline = this.redis.pipeline();
      stream.on('data', (keys: string[]) => {
        if (keys.length) {
          keys.forEach(key => pipeline.del(key));
        }
      });

      await new Promise((resolve, reject) => {
        stream.on('end', resolve);
        stream.on('error', reject);
      });

      await pipeline.exec();
    } catch (error) {
      this.updateStats(false, true);
      throw error;
    }
  }

  async keys(pattern: string): Promise<string[]> {
    try {
      const keys: string[] = [];
      const stream = this.redis.scanStream({
        match: `${this.keyPrefix}${pattern}`,
        count: CACHE_DEFAULTS.SCAN_COUNT,
      });

      await new Promise((resolve, reject) => {
        stream.on('data', (resultKeys: string[]) => {
          keys.push(...resultKeys.map(k => k.replace(this.keyPrefix, '')));
        });
        stream.on('end', resolve);
        stream.on('error', reject);
      });

      return keys;
    } catch {
      this.updateStats(false, true);
      return [];
    }
  }

  async health(): Promise<boolean> {
    try {
      return (await this.redis.ping()) === 'PONG';
    } catch {
      return false;
    }
  }

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

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

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

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

  isReady(): boolean {
    return this.redis.status === 'ready';
  }
}
