/**
 * Redis Client
 *
 * Centralized Redis connection for caching, rate limiting, and token blacklist.
 * Uses ioredis for robust Redis connection handling.
 * 
 * SECURITY FIX: Token blacklist now implements fail-closed behavior
 * to prevent authentication bypass when Redis is unavailable.
 */

import Redis from 'ioredis';
import { logger } from '../utils/logger';
import crypto from 'crypto';

/**
 * Redis configuration
 */
const REDIS_HOST = process.env.REDIS_HOST || 'localhost';
const REDIS_PORT = parseInt(process.env.REDIS_PORT || '6379', 10);
const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;

/**
 * Circuit breaker configuration for Redis operations
 */
const CIRCUIT_BREAKER_THRESHOLD = 5;
const CACHE_TTL_MS = 60000; // 1 minute
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 100;

// Circuit breaker state
let consecutiveFailures = 0;

// In-memory cache for token blacklist fallback
const inMemoryBlacklist = new Map<string, number>();

// Metrics tracking
const metrics = {
  redisBlacklistFailures: 0,
  redisBlacklistConsecutiveFailures: 0,
  redisBlacklistSuccesses: 0,
  circuitBreakerOpens: 0
};

/**
 * Custom error for authentication failures
 */
class AuthenticationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AuthenticationError';
    Object.setPrototypeOf(this, AuthenticationError.prototype);
  }
}

/**
 * Sleep utility for retry backoff
 */
function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

/**
 * Exponential backoff retry wrapper
 */
async function withRetry<T>(
  operation: () => Promise<T>,
  maxRetries: number = MAX_RETRIES,
  delayMs: number = RETRY_DELAY_MS
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await operation();
      return result;
    } catch (error) {
      if (attempt === maxRetries) throw error;
      logger.warn(
        { attempt, maxRetries, nextDelay: delayMs * attempt },
        'Redis operation failed, retrying with backoff'
      );
      await sleep(delayMs * attempt);
    }
  }
  throw new Error('Retry loop exhausted');
}

/**
 * Redis Client Singleton
 */
class RedisClientManager {
  private static instance: RedisClientManager;
  private client: Redis | null = null;
  private isConnected = false;

  private constructor() {
    this.initialize();
  }

  /**
   * Get singleton instance
   */
  public static getInstance(): RedisClientManager {
    if (!RedisClientManager.instance) {
      RedisClientManager.instance = new RedisClientManager();
    }
    return RedisClientManager.instance;
  }

  /**
   * Initialize Redis connection
   */
  private initialize(): void {
    try {
      this.client = new Redis({
        host: REDIS_HOST,
        port: REDIS_PORT,
        password: REDIS_PASSWORD,
        maxRetriesPerRequest: 3,
        retryStrategy: (times: number) => {
          if (times > 3) {
            logger.error('Redis connection retry limit exceeded');
            return null;
          }
          return Math.min(times * 100, 3000);
        },
        enableReadyCheck: true,
        lazyConnect: false,
      });

      this.client.on('connect', () => {
        this.isConnected = true;
        consecutiveFailures = 0; // Reset circuit breaker on connection
        metrics.redisBlacklistConsecutiveFailures = 0;
        logger.info({ host: REDIS_HOST, port: REDIS_PORT }, 'Redis client connected');
      });

      this.client.on('error', (error) => {
        logger.error({ error }, 'Redis client error');
      });

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

      this.client.on('reconnecting', () => {
        logger.info('Redis client reconnecting');
      });

    } catch (error) {
      logger.error({ error }, 'Failed to initialize Redis client');
      throw error;
    }
  }

  /**
   * Get the Redis client instance
   */
  public getClient(): Redis {
    if (!this.client) {
      this.initialize();
    }
    return this.client as Redis;
  }

  /**
   * Check if Redis is connected
   */
  public isReady(): boolean {
    return this.isConnected && this.client?.status === 'ready';
  }

  /**
   * Disconnect from Redis
   */
  public async disconnect(): Promise<void> {
    if (this.client) {
      await this.client.quit();
      this.isConnected = false;
      logger.info('Redis client disconnected');
    }
  }
}

// Export singleton instance
export const redisClient = RedisClientManager.getInstance();

// Export convenience functions
export function getRedis(): Redis {
  return redisClient.getClient();
}

export function isRedisReady(): boolean {
  return redisClient.isReady();
}

export async function disconnectRedis(): Promise<void> {
  await redisClient.disconnect();
}

/**
 * Get current metrics for monitoring
 */
export function getBlacklistMetrics(): typeof metrics {
  return { ...metrics };
}

/**
 * Reset metrics (useful for testing)
 */
export function resetBlacklistMetrics(): void {
  metrics.redisBlacklistFailures = 0;
  metrics.redisBlacklistConsecutiveFailures = 0;
  metrics.redisBlacklistSuccesses = 0;
  metrics.circuitBreakerOpens = 0;
}

// Token blacklist helpers (TTL = 7 days = 604800 seconds)
const TOKEN_BLACKLIST_PREFIX = 'token_blacklist:';
const TOKEN_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days

/**
 * Check in-memory cache for token blacklist
 * Uses SHA256 hash for storage (never stores full token)
 */
function checkInMemoryBlacklist(token: string): boolean {
  const hash = crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
  const entry = inMemoryBlacklist.get(hash);
  
  if (entry && Date.now() - entry < CACHE_TTL_MS) {
    return true; // Token in blacklist
  }
  
  return false; // Cannot verify, allow with risk (logged)
}

/**
 * Add token to in-memory cache
 */
function addToMemoryBlacklist(token: string): void {
  const hash = crypto.createHash('sha256').update(token).digest('hex').substring(0, 16);
  inMemoryBlacklist.set(hash, Date.now());
  
  // Clean up old entries periodically
  if (inMemoryBlacklist.size > 1000) {
    const now = Date.now();
    for (const [key, timestamp] of inMemoryBlacklist.entries()) {
      if (now - timestamp > CACHE_TTL_MS) {
        inMemoryBlacklist.delete(key);
      }
    }
  }
}

export async function blacklistToken(token: string): Promise<void> {
  const startTime = Date.now();
  
  try {
    const redis = getRedis();
    const key = `${TOKEN_BLACKLIST_PREFIX}${token}`;
    
    await withRetry(async () => {
      await redis.setex(key, TOKEN_TTL_SECONDS, '1');
    });
    
    // Add to in-memory cache as backup
    addToMemoryBlacklist(token);
    
    metrics.redisBlacklistSuccesses++;
    consecutiveFailures = 0;
    metrics.redisBlacklistConsecutiveFailures = 0;
    
    logger.debug(
      { 
        tokenPrefix: token.substring(0, 10),
        duration: Date.now() - startTime 
      }, 
      'Token blacklisted in Redis'
    );
  } catch (error) {
    consecutiveFailures++;
    metrics.redisBlacklistFailures++;
    metrics.redisBlacklistConsecutiveFailures = consecutiveFailures;
    
    // SECURITY: Always add to memory cache even if Redis fails
    addToMemoryBlacklist(token);
    
    logger.error(
      { 
        error: (error as Error).message,
        consecutiveFailures,
        tokenPrefix: token.substring(0, 10),
        timestamp: new Date().toISOString(),
        service: 'token-blacklist'
      }, 
      'Redis unavailable - token stored in memory cache only'
    );
  }
}

/**
 * Check if token is blacklisted
 * 
 * SECURITY CRITICAL: This function implements FAIL-CLOSED behavior.
 * When Redis is unavailable, it throws an AuthenticationError instead
 * of allowing the request to proceed.
 */
export async function isTokenBlacklisted(token: string): Promise<boolean> {
  const startTime = Date.now();
  
  try {
    const redis = getRedis();
    const key = `${TOKEN_BLACKLIST_PREFIX}${token}`;
    
    const result = await withRetry(async () => {
      return await redis.exists(key);
    });
    
    // Success - reset circuit breaker
    consecutiveFailures = 0;
    metrics.redisBlacklistConsecutiveFailures = 0;
    metrics.redisBlacklistSuccesses++;
    
    logger.debug(
      {
        tokenPrefix: token.substring(0, 10),
        blacklisted: result === 1,
        duration: Date.now() - startTime
      },
      'Token blacklist check completed'
    );
    
    return result === 1;
    
  } catch (error) {
    consecutiveFailures++;
    metrics.redisBlacklistFailures++;
    metrics.redisBlacklistConsecutiveFailures = consecutiveFailures;
    
    // Circuit breaker: after N failures, try in-memory cache
    if (consecutiveFailures >= CIRCUIT_BREAKER_THRESHOLD) {
      metrics.circuitBreakerOpens++;
      
      logger.warn(
        {
          consecutiveFailures,
          circuitBreakerThreshold: CIRCUIT_BREAKER_THRESHOLD,
          tokenPrefix: token.substring(0, 10)
        },
        'Circuit breaker opened - checking in-memory cache'
      );
      
      const inMemoryResult = checkInMemoryBlacklist(token);
      
      if (inMemoryResult) {
        logger.info(
          { tokenPrefix: token.substring(0, 10) },
          'Token found in in-memory blacklist'
        );
        return true;
      }
      
      // If not in memory cache and circuit breaker is open, we can't verify
      // Still fail-closed for security
      logger.error(
        {
          error: (error as Error).message,
          consecutiveFailures,
          tokenPrefix: token.substring(0, 10),
          timestamp: new Date().toISOString(),
          service: 'token-blacklist',
          circuitBreakerOpen: true
        },
        'Redis unavailable - SECURITY: unable to verify token status'
      );
      
      throw new AuthenticationError('Unable to verify token status. Service temporarily unavailable.');
    }
    
    // Fail-closed: if we can't verify, assume it's blocked
    logger.error(
      {
        error: (error as Error).message,
        consecutiveFailures,
        tokenPrefix: token.substring(0, 10),
        timestamp: new Date().toISOString(),
        service: 'token-blacklist',
        circuitBreakerOpen: false
      },
      'Redis unavailable - SECURITY: blocking token'
    );
    
    throw new AuthenticationError('Unable to verify token status. Service temporarily unavailable.');
  }
}

export default redisClient;
