import Redis from 'ioredis';
import LoggingService from '#services/file-system/logService';
import { parse as secureJsonParse } from 'secure-json-parse';
import Config from '#config';

class RedisService {
	private static instance: RedisService | null = null;
	private client: Redis | null = null;
	private logger: LoggingService;
	private isConnected: boolean = false;
	private cachedConnectionStatus: boolean = false;
	private lastConnectionCheck: number = 0;
	private readonly CONNECTION_CACHE_TTL = 5000; // Cache for 5 seconds

	private constructor(logger?: LoggingService) {
		this.logger = logger || new LoggingService();
	}

	static getInstance(logger?: LoggingService): RedisService {
		if (!RedisService.instance) {
			RedisService.instance = new RedisService(logger);
		}
		return RedisService.instance;
	}

	/**
	 * Reset the singleton instance - for testing only
	 */
	static resetInstance(): void {
		if (RedisService.instance) {
			RedisService.instance.disconnect().catch((error) => {
				console.error('[RedisService] Failed to disconnect during reset:', error);
			});
		}
		RedisService.instance = null;
	}

	async connect(): Promise<void> {
		if (this.isConnected && this.client) {
			return;
		}

		try {
			const redisHost = Config.redis.host;
			const redisPort = Config.redis.port;
			const redisPassword = Config.redis.password;
			const redisDb = Config.redis.db;

			this.client = new Redis({
				host: redisHost,
				port: redisPort,
				password: redisPassword || undefined,
				db: redisDb,
				retryStrategy: (times) => {
					const delay = Math.min(times * 50, 2000);
					return delay;
				},
				maxRetriesPerRequest: Config.redis.maxRetries,
				reconnectOnError: (err) => {
					// Reconnect on read-only errors or connection loss
					const targetError = 'READONLY';
					if (err.message.includes(targetError)) {
						return true;
					}
					return false;
				},
				// PERF-02: Connection pool configuration for high load
				enableReadyCheck: true,
				lazyConnect: false,
				keepAlive: 30000, // 30 seconds keep-alive
				enableOfflineQueue: true,
				connectTimeout: Config.redis.connectTimeout,
				commandTimeout: Config.redis.commandTimeout,
				// PERF-02: Add connection pool settings
				family: Config.redis.ipFamily, // 4 = IPv4, 6 = IPv6
			});

			this.client.on('connect', () => {
				this.isConnected = true;
				this.logger.logInfoAsync('Redis', 'Redis connected', null);
			});

			this.client.on('ready', () => {
				this.isConnected = true;
				this.logger.logInfoAsync('Redis', 'Redis ready', null);
			});

			this.client.on('error', (error) => {
				this.isConnected = false;
				this.logger.logErrorAsync('Redis', error as Error, null);
			});

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

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

			// Test connection
			await this.client.ping();
			this.isConnected = true;
		} catch (error) {
			this.isConnected = false;
			this.logger.logErrorAsync('Redis', error as Error, null);
			throw error;
		}
	}

	async disconnect(): Promise<void> {
		if (this.client) {
			await this.client.quit();
			this.client = null;
			this.isConnected = false;
		}
	}

	getClient(): Redis | null {
		return this.client;
	}

	/**
	 * Check if Redis is connected with cached status to reduce ping overhead
	 * Cache is valid for 5 seconds, then performs actual ping verification
	 */
	async isRedisConnected(): Promise<boolean> {
		if (!this.client || !this.isConnected) {
			return false;
		}

		const now = Date.now();
		// Return cached status if within TTL
		if (now - this.lastConnectionCheck < this.CONNECTION_CACHE_TTL) {
			return this.cachedConnectionStatus;
		}

		// Perform actual ping and cache result
		try {
			await this.client.ping();
			this.cachedConnectionStatus = true;
			this.lastConnectionCheck = now;
			return true;
		} catch (error) {
			this.isConnected = false;
			this.cachedConnectionStatus = false;
			this.lastConnectionCheck = now;
			return false;
		}
	}

	/**
	 * Synchronous check for connection status (flag-based only)
	 * Use isRedisConnected() for actual connectivity verification
	 */
	isConnectedFlag(): boolean {
		return this.isConnected && this.client !== null;
	}

	async get<T>(key: string): Promise<T | null> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, returning null for get operation', { key })
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return null;
		}

		try {
			const value = await this.client.get(key);
			if (value === null) {
				return null;
			}
			// SEC: Use secure-json-parse to prevent prototype pollution attacks
			return secureJsonParse(value) as T;
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'get' });
			return null;
		}
	}

	async set<T>(key: string, data: T, ttlSeconds?: number): Promise<void> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, skipping set operation', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return;
		}

		try {
			const value = JSON.stringify(data);
			if (ttlSeconds) {
				await this.client.setex(key, ttlSeconds, value);
			} else {
				await this.client.set(key, value);
			}
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'set', ttlSeconds });
		}
	}

	async delete(key: string): Promise<void> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, skipping delete operation', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return;
		}

		try {
			await this.client.del(key);
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'delete' });
		}
	}

	async deletePattern(pattern: string): Promise<void> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, skipping deletePattern operation', { pattern })
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return;
		}

		try {
			// PERF: Use SCAN instead of KEYS to avoid blocking Redis
			// PERF: Delete keys during scan to avoid memory buildup with large key sets
			let cursor = '0';
			const batchSize = 100;

			do {
				const result = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', batchSize);
				cursor = result[0];
				const keys = result[1];
				if (keys.length > 0) {
					// Delete keys immediately during scan to avoid memory buildup
					await this.client.del(...keys);
				}
			} while (cursor !== '0');
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { pattern, operation: 'deletePattern' });
		}
	}

	async increment(key: string, amount = 1): Promise<number> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, returning 0 for increment', { key }).catch((err) => {
				// SEC-08: Fallback to console if logging fails
				console.error('[Redis] Failed to log info:', err);
				console.warn(`[Redis] Not connected, returning 0 for increment: ${key}`);
			});
			return 0;
		}

		try {
			return await this.client.incrby(key, amount);
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'increment', amount });
			return 0;
		}
	}

	async incrementWithExpiry(key: string, ttlSeconds: number): Promise<number> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, returning 0 for incrementWithExpiry', { key })
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return 0;
		}

		try {
			const result = await this.client.incr(key);
			if (result === 1) {
				await this.client.expire(key, ttlSeconds);
			}
			return result;
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'incrementWithExpiry', ttlSeconds });
			return 0;
		}
	}

	async exists(key: string): Promise<boolean> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, returning false for exists', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return false;
		}

		try {
			const result = await this.client.exists(key);
			return result === 1;
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'exists' });
			return false;
		}
	}

	async ttl(key: string): Promise<number> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, returning -1 for ttl', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return -1;
		}

		try {
			return await this.client.ttl(key);
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'ttl' });
			return -1;
		}
	}

	async decrement(key: string, amount = 1): Promise<number> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, returning 0 for decrement', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return 0;
		}

		try {
			return await this.client.decrby(key, amount);
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'decrement', amount });
			return 0;
		}
	}

	async expire(key: string, seconds: number): Promise<boolean> {
		if (!this.client || !this.isConnected) {
			this.logger.logInfoAsync('Redis', 'Redis not connected, returning false for expire', { key }).catch((error) => {
				console.error('[RedisService] Failed to log Redis not connected:', error);
			});
			return false;
		}

		try {
			const result = await this.client.expire(key, seconds);
			return result === 1;
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'expire', seconds });
			return false;
		}
	}

	/**
	 * Set value only if key doesn't exist (atomic operation)
	 */
	async setIfNotExists<T>(key: string, value: T, ttlSeconds?: number): Promise<boolean> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, returning false for setIfNotExists', { key })
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return false;
		}

		try {
			const serialized = JSON.stringify(value);
			if (ttlSeconds) {
				const result = await this.client.set(key, serialized, 'EX', ttlSeconds, 'NX');
				return result === 'OK';
			}
			const result = await this.client.set(key, serialized, 'NX');
			return result === 'OK';
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'setIfNotExists', ttlSeconds });
			return false;
		}
	}

	/**
	 * Get and delete key atomically
	 */
	async getAndDelete<T>(key: string): Promise<T | null> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, returning null for getAndDelete', { key })
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return null;
		}

		try {
			const value = await this.client.get(key);
			if (value === null) {
				return null;
			}
			await this.client.del(key);
			// SEC: Use secure-json-parse to prevent prototype pollution attacks
			return secureJsonParse(value) as T;
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { key, operation: 'getAndDelete' });
			return null;
		}
	}

	/**
	 * Execute multiple commands in a transaction
	 */
	async transaction(commands: Array<(client: Redis) => Promise<any>>): Promise<any[]> {
		if (!this.client || !this.isConnected) {
			this.logger
				.logInfoAsync('Redis', 'Redis not connected, returning empty array for transaction', null)
				.catch((error) => {
					console.error('[RedisService] Failed to log Redis not connected:', error);
				});
			return [];
		}

		try {
			const multi = this.client.multi();
			const results = await multi.exec((err, replies) => {
				if (err) {
					throw err;
				}
				return replies;
			});
			return results || [];
		} catch (error) {
			this.logger.logErrorAsync('Redis', error as Error, { operation: 'transaction', commandCount: commands.length });
			throw error;
		}
	}
}

export default RedisService;
