import { Request, NextFunction } from 'express';
import { Res } from '#interfaces/IApi';
import RedisService from '#services/storage/redisService';
import { GenericError } from '#interfaces/error/generic';
import Logger from '#utils/logger';
import Config from '#config';

interface RateLimitOptions {
	windowMs: number; // Time window in milliseconds
	max: number; // Maximum number of requests per window
	keyGenerator?: (req: Request) => string;
	skipSuccessfulRequests?: boolean;
	skipFailedRequests?: boolean;
	failClosed?: boolean; // If true, reject requests when Redis is unavailable (security-critical endpoints)
}

interface SimpleRateLimitOptions {
	windowMs: number;
	max: number;
}

// Track all cleanup intervals for proper cleanup in tests
const cleanupIntervals: NodeJS.Timeout[] = [];

/**
 * Cleanup all rate limiter intervals (call this in test teardown)
 */
export function cleanupRateLimiterIntervals(): void {
	for (const interval of cleanupIntervals) {
		clearInterval(interval);
	}
	cleanupIntervals.length = 0;
}

/**
 * Simple rate limiter for non-Express contexts (e.g., background workers)
 * Returns a function that checks if a key is within rate limits
 */
export function createSimpleRateLimit(options: SimpleRateLimitOptions) {
	const { windowMs, max } = options;
	const redis = RedisService.getInstance();
	const ttlSeconds = Math.ceil(windowMs / 1000);
	const inMemoryFallback = new Map<string, { count: number; resetTime: number }>();

	// SEC-07: Maximum entries for simple rate limiter fallback to prevent memory leaks
	const MAX_SIMPLE_FALLBACK_SIZE = Config.featureFlags.maxSimpleFallbackSize;

	// Skip interval creation in test environment to prevent hanging
	// eslint-disable-next-line no-restricted-syntax
	const isTestEnv = process.env.NODE_ENV === 'test';

	// Cleanup expired entries periodically to prevent memory leaks
	let cleanupInterval: NodeJS.Timeout | undefined;
	if (!isTestEnv) {
		cleanupInterval = setInterval(() => {
			const now = Date.now();
			for (const [key, data] of inMemoryFallback.entries()) {
				if (now > data.resetTime) {
					inMemoryFallback.delete(key);
				}
			}

			// SEC-07: Enforce size limit to prevent memory leaks
			if (inMemoryFallback.size > MAX_SIMPLE_FALLBACK_SIZE) {
				// Remove oldest entries (FIFO)
				const entries = Array.from(inMemoryFallback.entries());
				const entriesToDelete = entries.slice(0, inMemoryFallback.size - MAX_SIMPLE_FALLBACK_SIZE);
				for (const [key] of entriesToDelete) {
					inMemoryFallback.delete(key);
				}
			}
		}, 60000); // Run cleanup every 60 seconds

		// Track interval for cleanup
		if (cleanupInterval) {
			cleanupIntervals.push(cleanupInterval);
		}

		// Clear interval on process exit to prevent memory leaks
		if (typeof process !== 'undefined') {
			process.on('beforeExit', () => {
				if (cleanupInterval) clearInterval(cleanupInterval);
			});
		}
	}

	return async (key: string): Promise<boolean> => {
		// Skip rate limiting if disabled via config
		if (!Config.featureFlags.enableRateLimit) {
			return true;
		}

		// Fallback to in-memory if Redis is not connected
		const isConnected = await redis.isRedisConnected();
		if (!isConnected) {
			const now = Date.now();
			const windowData = inMemoryFallback.get(key);

			if (!windowData || now > windowData.resetTime) {
				inMemoryFallback.set(key, { count: 1, resetTime: now + windowMs });
				return true;
			}

			if (windowData.count >= max) {
				return false;
			}

			windowData.count++;
			inMemoryFallback.set(key, windowData);
			return true;
		}

		try {
			const current = await redis.incrementWithExpiry(`ratelimit:${key}`, ttlSeconds);
			return current <= max;
		} catch {
			// Fallback to in-memory on Redis error
			const now = Date.now();
			const windowData = inMemoryFallback.get(key);

			if (!windowData || now > windowData.resetTime) {
				inMemoryFallback.set(key, { count: 1, resetTime: now + windowMs });
				return true;
			}

			if (windowData.count >= max) {
				return false;
			}

			windowData.count++;
			inMemoryFallback.set(key, windowData);
			return true;
		}
	};
}

// SEC-CRIT-02: In-memory fallback for distributed rate limiter when failClosed=false
const inMemoryFallbackMaps = new Map<string, Map<string, { count: number; resetTime: number }>>();

// SEC-07: Maximum entries per fallback map to prevent memory leaks
const MAX_FALLBACK_MAP_SIZE = Config.featureFlags.maxFallbackMapSize;

// Cleanup expired entries periodically to prevent memory leaks
let _fallbackCleanupInterval: NodeJS.Timeout | undefined;
// eslint-disable-next-line no-restricted-syntax
if (process.env.NODE_ENV !== 'test') {
	_fallbackCleanupInterval = setInterval(() => {
		const now = Date.now();
		for (const [mapKey, map] of inMemoryFallbackMaps.entries()) {
			for (const [key, data] of map.entries()) {
				if (now > data.resetTime) {
					map.delete(key);
				}
			}

			// SEC-07: Enforce size limit to prevent memory leaks
			if (map.size > MAX_FALLBACK_MAP_SIZE) {
				// Remove oldest entries (FIFO) - convert to array, slice, and delete
				const entries = Array.from(map.entries());
				const entriesToDelete = entries.slice(0, map.size - MAX_FALLBACK_MAP_SIZE);
				for (const [key] of entriesToDelete) {
					map.delete(key);
				}
			}

			if (map.size === 0) {
				inMemoryFallbackMaps.delete(mapKey);
			}
		}
	}, 60000); // Run every minute
}

export function createDistributedRateLimit(options: RateLimitOptions) {
	const {
		windowMs,
		max,
		keyGenerator,
		skipSuccessfulRequests = false,
		skipFailedRequests = false,
		failClosed = false,
	} = options;
	const redis = RedisService.getInstance();
	const ttlSeconds = Math.ceil(windowMs / 1000);
	const limiterId = Math.random().toString(36).substring(7);

	return async (req: Request, res: Res, next: NextFunction): Promise<void> => {
		// Skip rate limiting if disabled via config
		if (!Config.featureFlags.enableRateLimit) {
			return next();
		}

		const key = keyGenerator ? keyGenerator(req) : `ratelimit:${req.ip || 'unknown'}`;

		// SEC-CRIT-02: Check Redis connection status
		const redisConnected = await redis.isRedisConnected();

		if (!redisConnected) {
			// LOG-01: Alert when Redis is unavailable in production
			// eslint-disable-next-line no-restricted-syntax
			if (process.env.NODE_ENV === 'production') {
				Logger.error(
					'[RateLimiter] Redis unavailable - using in-memory fallback. This is not distributed across servers.',
				);
			}

			if (failClosed) {
				// Fail-closed: reject when Redis is unavailable
				const error = new GenericError('SERVICE_UNAVAILABLE');
				return res.error(error);
			} else {
				// Fail-open: use in-memory fallback
				const fallbackMap = inMemoryFallbackMaps.get(limiterId) || new Map();
				inMemoryFallbackMaps.set(limiterId, fallbackMap);

				const now = Date.now();
				const windowData = fallbackMap.get(key);

				if (!windowData || now > windowData.resetTime) {
					fallbackMap.set(key, { count: 1, resetTime: now + windowMs });
					return next();
				}

				if (windowData.count >= max) {
					const error = new GenericError('TOO_MANY_REQUESTS');
					res.setHeader('Retry-After', ttlSeconds.toString());
					res.setHeader('X-RateLimit-Limit', max.toString());
					res.setHeader('X-RateLimit-Remaining', '0');
					res.setHeader('X-RateLimit-Reset', windowData.resetTime.toString());
					return res.error(error);
				}

				windowData.count++;
				fallbackMap.set(key, windowData);
				res.setHeader('X-RateLimit-Limit', max.toString());
				res.setHeader('X-RateLimit-Remaining', (max - windowData.count).toString());
				res.setHeader('X-RateLimit-Reset', windowData.resetTime.toString());
				return next();
			}
		}

		try {
			const current = await redis.incrementWithExpiry(key, ttlSeconds);

			if (current > max) {
				const error = new GenericError('TOO_MANY_REQUESTS');
				res.setHeader('Retry-After', ttlSeconds.toString());
				res.setHeader('X-RateLimit-Limit', max.toString());
				res.setHeader('X-RateLimit-Remaining', '0');
				res.setHeader('X-RateLimit-Reset', (Date.now() + windowMs).toString());
				return res.error(error);
			}

			res.setHeader('X-RateLimit-Limit', max.toString());
			res.setHeader('X-RateLimit-Remaining', (max - current).toString());
			res.setHeader('X-RateLimit-Reset', (Date.now() + windowMs).toString());

			// Store original json method to intercept response
			const originalJson = (res as any).json.bind(res);
			res.json = function (data: any) {
				const isSuccess = res.statusCode < 400;

				if (skipSuccessfulRequests && isSuccess) {
					redis.delete(key).catch((error) => {
						console.error('[RateLimiter] Failed to delete rate limit key on success:', error);
					});
				} else if (skipFailedRequests && !isSuccess) {
					redis.delete(key).catch((error) => {
						console.error('[RateLimiter] Failed to delete rate limit key:', error);
					});
				}

				return originalJson(data);
			};

			next();
		} catch (error) {
			// LOG-02: Alert on Redis error in production
			// eslint-disable-next-line no-restricted-syntax
			if (process.env.NODE_ENV === 'production') {
				Logger.error('[RateLimiter] Redis error - using in-memory fallback:', error);
			}

			// SEC-CRIT-02: On Redis error, use in-memory fallback if failClosed=false
			if (!failClosed) {
				const fallbackMap = inMemoryFallbackMaps.get(limiterId) || new Map();
				inMemoryFallbackMaps.set(limiterId, fallbackMap);

				const now = Date.now();
				const windowData = fallbackMap.get(key);

				if (!windowData || now > windowData.resetTime) {
					fallbackMap.set(key, { count: 1, resetTime: now + windowMs });
					return next();
				}

				if (windowData.count >= max) {
					const error = new GenericError('TOO_MANY_REQUESTS');
					res.setHeader('Retry-After', ttlSeconds.toString());
					res.setHeader('X-RateLimit-Limit', max.toString());
					res.setHeader('X-RateLimit-Remaining', '0');
					res.setHeader('X-RateLimit-Reset', windowData.resetTime.toString());
					return res.error(error);
				}

				windowData.count++;
				fallbackMap.set(key, windowData);
				res.setHeader('X-RateLimit-Limit', max.toString());
				res.setHeader('X-RateLimit-Remaining', (max - windowData.count).toString());
				res.setHeader('X-RateLimit-Reset', windowData.resetTime.toString());
				return next();
			}

			// Fail-closed: reject on Redis error
			console.error('[RateLimiter] Redis error:', error);
			const errorResponse = new GenericError('SERVICE_UNAVAILABLE');
			return res.error(errorResponse);
		}
	};
}

// Pre-configured rate limiters for common use cases
export const authRateLimit = createDistributedRateLimit({
	windowMs: 15 * 60 * 1000, // 15 minutes
	max: 5, // 5 attempts per 15 minutes
	failClosed: false, // SEC-03: Use in-memory fallback when Redis is unavailable. Rate limiting still works but is not distributed. Set to true in production if you prefer to block requests instead of degrading gracefully.
});

export const apiRateLimit = createDistributedRateLimit({
	windowMs: 60 * 1000, // 1 minute
	max: 100, // 100 requests per minute
});

export const strictRateLimit = createDistributedRateLimit({
	windowMs: 60 * 1000, // 1 minute
	max: 10, // 10 requests per minute
});

/** SEC-04: Upload-specific rate limit — per authenticated user (falls back to IP) */
export const uploadRateLimit = createDistributedRateLimit({
	windowMs: 60 * 1000, // 1 minute
	max: 20, // 20 upload operations per minute
	keyGenerator: (req) => `upload:${(req as any).user?.id || req.ip || 'unknown'}`,
});
