import Redis from 'ioredis';
import type { RedisConfig } from './types';

interface ConnectionConfig {
  name: string;
  singleton?: boolean;
  db?: number;
  redisConfig: RedisConfig;
  logger?: any;
}

/**
 * Redis Connection Manager for managing shared and unique connections
 */
export class RedisConnectionManager {
  private connections = new Map<string, Redis>();
  private static sharedConnections = new Map<string, Redis>();
  private static instance: RedisConnectionManager;
  private logger: any = console;
  private static readyConnections = new Set<string>();

  // Singleton pattern for the manager itself
  static getInstance(): RedisConnectionManager {
    if (!this.instance) {
      this.instance = new RedisConnectionManager();
    }
    return this.instance;
  }

  /**
   * Get or create a Redis connection
   * @param config Connection configuration
   * @returns Redis connection instance
   */
  getConnection(config: ConnectionConfig): Redis {
    const { name, singleton = false, db, redisConfig, logger } = config;
    if (logger) {
      this.logger = logger;
    }

    if (singleton) {
      // Use shared connection across all instances
      if (!RedisConnectionManager.sharedConnections.has(name)) {
        const connection = this.createConnection(name, redisConfig, db, logger);
        RedisConnectionManager.sharedConnections.set(name, connection);
        this.logger.info(`Created shared Redis connection: ${name}`);
      }
      const connection = RedisConnectionManager.sharedConnections.get(name);
      if (!connection) {
        throw new Error(`Expected connection ${name} to exist in shared connections`);
      }
      return connection;
    } else {
      // Create unique connection per instance
      const uniqueName = `${name}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
      const connection = this.createConnection(uniqueName, redisConfig, db, logger);
      this.connections.set(uniqueName, connection);
      this.logger.debug(`Created unique Redis connection: ${uniqueName}`);
      return connection;
    }
  }

  /**
   * Create a new Redis connection with configuration
   */
  private createConnection(name: string, config: RedisConfig, db?: number, logger?: any): Redis {
    const redisOptions = {
      host: config.host,
      port: config.port,
      password: config.password || undefined,
      username: config.username || undefined,
      db: db ?? config.db ?? 0,
      maxRetriesPerRequest: config.maxRetriesPerRequest ?? 3,
      retryDelayOnFailover: config.retryDelayOnFailover ?? 100,
      connectTimeout: config.connectTimeout ?? 10000,
      commandTimeout: config.commandTimeout ?? 5000,
      keepAlive: config.keepAlive ?? 0,
      connectionName: name,
      lazyConnect: false, // Connect immediately instead of waiting for first command
      ...(config.tls && {
        tls: {
          cert: config.tls.cert || undefined,
          key: config.tls.key || undefined,
          ca: config.tls.ca || undefined,
          rejectUnauthorized: config.tls.rejectUnauthorized ?? true,
        },
      }),
    };

    const redis = new Redis(redisOptions);
    
    // Use the provided logger or fall back to instance logger
    const log = logger || this.logger;

    // Setup event handlers
    redis.on('connect', () => {
      log.info(`Redis connection established: ${name}`);
    });

    redis.on('ready', () => {
      log.info(`Redis connection ready: ${name}`);
    });

    redis.on('error', err => {
      log.error(`Redis connection error for ${name}:`, err);
    });

    redis.on('close', () => {
      log.warn(`Redis connection closed: ${name}`);
    });

    redis.on('reconnecting', () => {
      log.warn(`Redis reconnecting: ${name}`);
    });

    return redis;
  }

  /**
   * Close a specific connection
   */
  async closeConnection(connection: Redis): Promise<void> {
    try {
      await connection.quit();
    } catch (error) {
      this.logger.warn('Error closing Redis connection:', error as Error);
    }
  }

  /**
   * Close all connections managed by this instance
   */
  async closeAllConnections(): Promise<void> {
    // Close instance-specific connections
    const instancePromises = Array.from(this.connections.values()).map(conn =>
      this.closeConnection(conn)
    );
    await Promise.all(instancePromises);
    this.connections.clear();

    // Close shared connections (only if this is the last instance)
    if (RedisConnectionManager.instance === this) {
      const sharedPromises = Array.from(RedisConnectionManager.sharedConnections.values()).map(
        conn => this.closeConnection(conn)
      );
      await Promise.all(sharedPromises);
      RedisConnectionManager.sharedConnections.clear();
    }

    this.logger.info('All Redis connections closed');
  }

  /**
   * Get connection statistics
   */
  getConnectionCount(): { shared: number; unique: number } {
    return {
      shared: RedisConnectionManager.sharedConnections.size,
      unique: this.connections.size,
    };
  }

  /**
   * Get all connection names for monitoring
   */
  getConnectionNames(): { shared: string[]; unique: string[] } {
    return {
      shared: Array.from(RedisConnectionManager.sharedConnections.keys()),
      unique: Array.from(this.connections.keys()),
    };
  }

  /**
   * Health check for all connections
   */
  async healthCheck(): Promise<{ healthy: boolean; details: Record<string, boolean> }> {
    const details: Record<string, boolean> = {};
    let allHealthy = true;

    // Check shared connections
    for (const [name, connection] of RedisConnectionManager.sharedConnections) {
      try {
        await connection.ping();
        details[`shared:${name}`] = true;
      } catch {
        details[`shared:${name}`] = false;
        allHealthy = false;
      }
    }

    // Check instance connections
    for (const [name, connection] of this.connections) {
      try {
        await connection.ping();
        details[`unique:${name}`] = true;
      } catch {
        details[`unique:${name}`] = false;
        allHealthy = false;
      }
    }

    return { healthy: allHealthy, details };
  }

  /**
   * Wait for all created connections to be ready
   * @param timeout Maximum time to wait in milliseconds
   * @returns Promise that resolves when all connections are ready
   */
  static async waitForAllConnections(timeout: number = 30000): Promise<void> {
    const instance = this.getInstance();
    const allConnections = new Map([...instance.connections, ...this.sharedConnections]);

    if (allConnections.size === 0) {
      instance.logger.debug('No Redis connections to wait for');
      return;
    }

    instance.logger.info(`Waiting for ${allConnections.size} Redis connections to be ready...`);

    const connectionPromises = Array.from(allConnections.entries()).map(([name, redis]) =>
      instance.waitForConnection(redis, name, timeout)
    );

    try {
      await Promise.all(connectionPromises);
      instance.logger.info('All Redis connections are ready');
    } catch (error) {
      instance.logger.error('Failed to establish all Redis connections:', error);
      throw error;
    }
  }

  /**
   * Wait for a specific connection to be ready
   */
  private async waitForConnection(redis: Redis, name: string, timeout: number): Promise<void> {
    return new Promise((resolve, reject) => {
      const timeoutId = setTimeout(() => {
        reject(new Error(`Redis connection ${name} failed to be ready within ${timeout}ms`));
      }, timeout);

      const onReady = () => {
        clearTimeout(timeoutId);
        RedisConnectionManager.readyConnections.add(name);
        this.logger.debug(`Redis connection ready: ${name}`);
        resolve();
      };

      const onError = (err: Error) => {
        clearTimeout(timeoutId);
        this.logger.error(`Redis connection failed for ${name}:`, err);
        reject(err);
      };

      if (redis.status === 'ready') {
        onReady();
      } else {
        redis.once('ready', onReady);
        redis.once('error', onError);
      }
    });
  }

  /**
   * Check if all connections are ready
   */
  static areAllConnectionsReady(): boolean {
    const instance = this.getInstance();
    const allConnections = new Map([...instance.connections, ...this.sharedConnections]);

    return (
      allConnections.size > 0 &&
      Array.from(allConnections.keys()).every(name => this.readyConnections.has(name))
    );
  }
}

export default RedisConnectionManager;
