import { inject, injectable } from 'inversify';
import { RedisKeyName } from '../models/redis.model.js';
import { LoggerService, RedisService } from './index.js';
import ms from 'ms';
import INTERNAL_CONFIG from '../config/internal-config.js';

@injectable()
export class ParticipantNameService {
	private readonly MAX_CONCURRENT_NAME_REQUESTS = Number(INTERNAL_CONFIG.PARTICIPANT_MAX_CONCURRENT_NAME_REQUESTS);
	private readonly PARTICIPANT_NAME_TTL = ms(INTERNAL_CONFIG.PARTICIPANT_NAME_RESERVATION_TTL);

	constructor(
		@inject(RedisService) protected redisService: RedisService,
		@inject(LoggerService) protected logger: LoggerService
	) {}

	/**
	 * Reserves a unique participant name for a room using atomic operations.
	 * If the requested name is taken, it generates alternatives with incremental suffixes.
	 *
	 * @param roomId - The room identifier
	 * @param requestedName - The desired participant name
	 * @returns Promise<string> - The reserved unique name
	 * @throws Error if unable to reserve a unique name after max retries
	 */
	async reserveUniqueName(roomId: string, requestedName: string): Promise<string> {
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;

		// Normalize the base name for case-insensitive comparisons
		const normalizedBaseName = requestedName.toLowerCase();

		// First, try to reserve the exact requested name
		const reservedOriginal = await this.tryReserveName(participantsKey, normalizedBaseName);

		if (reservedOriginal) {
			this.logger.verbose(`Reserved original name '${requestedName}' for room '${roomId}'`);
			return requestedName;
		}

		// If original name is taken, generate alternatives with atomic counter
		for (let attempt = 1; attempt <= this.MAX_CONCURRENT_NAME_REQUESTS; attempt++) {
			const alternativeName = await this.generateAlternativeName(roomId, normalizedBaseName, attempt);
			const reserved = await this.tryReserveName(participantsKey, alternativeName);

			if (reserved) {
				this.logger.verbose(
					`Reserved alternative name '${alternativeName}' for room '${roomId}' (attempt ${attempt})`
				);
				// Return alternative name with original case
				const suffix = alternativeName.replace(`${normalizedBaseName}_`, '');
				return `${requestedName}_${suffix}`;
			}
		}

		throw new Error(
			`Unable to reserve unique name for '${requestedName}' in room '${roomId}' after ${this.MAX_CONCURRENT_NAME_REQUESTS} attempts`
		);
	}

	/**
	 * Releases a reserved participant name, making it available for others.
	 *
	 * @param roomId - The room identifier
	 * @param participantName - The name to release
	 */
	/**
	 * Releases a reserved participant name, making it available for others.
	 * Also returns the number suffix to the available pool for reuse.
	 *
	 * @param roomId - The room identifier
	 * @param participantName - The name to release
	 */
	async releaseName(roomId: string, participantName: string): Promise<void> {
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;
		// Normalize the name for case-insensitive checks
		const normalizedName = participantName.toLowerCase();

		try {
			await this.redisService.delete(`${participantsKey}:${normalizedName}`);

			// If this is a numbered variant (e.g., "Alice_2"), return the number to the pool
			const numberMatch = participantName.match(/^(.+)_(\d+)$/);

			if (numberMatch) {
				const baseName = numberMatch[1];
				const number = parseInt(numberMatch[2], 10);
				await this.returnNumberToPool(roomId, baseName, number);
			}

			this.logger.verbose(`Released name '${participantName}' for room '${roomId}'`);
		} catch (error) {
			this.logger.warn(`Error releasing name '${participantName}' for room '${roomId}':`, error);
		}
	}

	/**
	 * Checks if a participant name is currently reserved in a room.
	 *
	 * @param roomId - The room identifier
	 * @param participantName - The name to check
	 * @returns Promise<boolean> - True if the name is reserved
	 */
	async isNameReserved(roomId: string, participantName: string): Promise<boolean> {
		// Normalize the name for case-insensitive checks
		const normalizedName = participantName.toLowerCase();
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;
		return await this.redisService.exists(`${participantsKey}:${normalizedName}`);
	}

	/**
	 * Gets all currently reserved names in a room.
	 *
	 * @param roomId - The room identifier
	 * @returns Promise<string[]> - Array of reserved participant names
	 */
	async getReservedNames(roomId: string): Promise<string[]> {
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;
		const pattern = `${participantsKey}:*`;

		try {
			const keys = await this.redisService.getKeys(pattern);
			return keys.map((key) => key.replace(`${participantsKey}:`, ''));
		} catch (error) {
			this.logger.error(`Error getting reserved names for room '${roomId}':`, error);
			return [];
		}
	}

	/**
	 * Cleans up expired participant reservations for a room.
	 * This should be called periodically or when a room is cleaned up.
	 *
	 * @param roomId - The room identifier
	 */
	async cleanupExpiredReservations(roomId: string): Promise<void> {
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;
		const participantsPoolKey = `${RedisKeyName.PARTICIPANT_NAME_POOL}${roomId}`;
		const pattern = `${participantsKey}:*`;
		const poolPattern = `${participantsPoolKey}:*`;

		try {
			const [participantKeys, poolKeys] = await Promise.all([
				this.redisService.getKeys(pattern),
				this.redisService.getKeys(poolPattern)
			]);
			this.logger.verbose(
				`Found ${participantKeys.length} participant reservations to check for room '${roomId}'`
			);

			// Redis TTL will automatically clean up expired keys, but we can force cleanup if needed
			const promises = participantKeys.map((key) => this.redisService.delete(key));
			await Promise.all(promises);
			this.logger.verbose(
				`Cleaned up ${participantKeys.length} expired participant names reservations for room '${roomId}'`
			);

			// Clean up expired participant name numbers from the pool
			this.logger.verbose(`Found ${poolKeys.length} participant name numbers to check for room '${roomId}'`);
			const poolPromises = poolKeys.map((key) => this.redisService.delete(key));
			await Promise.all(poolPromises);
			this.logger.verbose(`Cleaned up ${poolKeys.length} expired participant name numbers for room '${roomId}'`);
		} catch (error) {
			this.logger.error(`Error cleaning up reservations for room '${roomId}':`, error);
		}
	}

	/**
	 * Attempts to atomically reserve a specific name using Redis SET with NX (not exists) option.
	 *
	 * @private
	 * @param participantsKey - The Redis key prefix for participants
	 * @param name - The name to reserve
	 * @returns Promise<boolean> - True if reservation was successful
	 */
	private async tryReserveName(participantsKey: string, name: string): Promise<boolean> {
		// Normalize the name for case-insensitive checks
		const normalizedName = name.toLowerCase();
		const nameKey = `${participantsKey}:${normalizedName}`;
		const timestamp = Date.now().toString();

		try {
			return await this.redisService.setIfNotExists(nameKey, timestamp, this.PARTICIPANT_NAME_TTL);
		} catch (error) {
			this.logger.warn(`Error trying to reserve name '${name}':`, error);
			return false;
		}
	}

	/**
	 * Generates an alternative name using a pool of available numbers.
	 * First tries to get a number from the available pool, then generates the next sequential number.
	 *
	 * @private
	 * @param roomId - The room identifier
	 * @param baseName - The base name to append number to
	 * @param fallbackSuffix - Fallback suffix if Redis fails
	 * @returns Promise<string> - The generated alternative name
	 */
	private async generateAlternativeName(roomId: string, baseName: string, fallbackSuffix: number): Promise<string> {
		try {
			// Normalize the base name for case-insensitive checks
			const normalizedBaseName = baseName.toLowerCase();

			// First try to get an available number from the pool
			const availableNumber = await this.getNumberFromPool(roomId, normalizedBaseName);

			if (availableNumber !== null) {
				return `${baseName}_${availableNumber}`;
			}

			// If no number available in pool, find the next sequential number
			const nextNumber = await this.findNextAvailableNumber(roomId, baseName);
			return `${baseName}_${nextNumber}`;
		} catch (error) {
			this.logger.warn(`Error generating alternative name, using fallback:`, error);
			// Fallback to simple incremental suffix if Redis fails
			return `${baseName}_${fallbackSuffix}`;
		}
	}

	/**
	 * Gets the smallest available number from the pool for reuse.
	 *
	 * @private
	 * @param roomId - The room identifier
	 * @param baseName - The base name
	 * @returns Promise<number | null> - Available number or null if pool is empty
	 */
	private async getNumberFromPool(roomId: string, baseName: string): Promise<number | null> {
		const poolKey = `${RedisKeyName.PARTICIPANT_NAME_POOL}${roomId}:${baseName}`;

		try {
			// Get the smallest number from the sorted set and remove it atomically
			const results = await this.redisService.popMinFromSortedSet(poolKey, 1);

			if (results.length > 0) {
				const number = parseInt(results[0], 10);
				this.logger.verbose(`Reusing number ${number} from pool for '${baseName}' in room '${roomId}'`);
				return number;
			}

			return null;
		} catch (error) {
			this.logger.warn(`Error getting number from pool:`, error);
			return null;
		}
	}

	/**
	 * Finds the next available sequential number by checking existing participants.
	 *
	 * @private
	 * @param roomId - The room identifier
	 * @param baseName - The base name
	 * @returns Promise<number> - The next available number
	 */
	private async findNextAvailableNumber(roomId: string, baseName: string): Promise<number> {
		const participantsKey = `${RedisKeyName.ROOM_PARTICIPANTS}${roomId}`;
		const pattern = `${participantsKey}:${baseName}_*`;

		try {
			const existingKeys = await this.redisService.getKeys(pattern);
			const usedNumbers = new Set<number>();

			// Extract all used numbers
			for (const key of existingKeys) {
				const name = key.replace(`${participantsKey}:`, '');
				const numberMatch = name.match(/^.+_(\d+)$/);

				if (numberMatch) {
					usedNumbers.add(parseInt(numberMatch[1], 10));
				}
			}

			// Find the smallest available number starting from 1
			let nextNumber = 1;

			while (usedNumbers.has(nextNumber)) {
				nextNumber++;
			}

			this.logger.verbose(`Generated new sequential number ${nextNumber} for '${baseName}' in room '${roomId}'`);
			return nextNumber;
		} catch (error) {
			this.logger.warn(`Error finding next available number:`, error);
			// Fallback to timestamp-based number if everything fails
			return Date.now() % 10000;
		}
	}

	/**
	 * Returns a number to the available pool for reuse.
	 *
	 * @private
	 * @param roomId - The room identifier
	 * @param baseName - The base name
	 * @param number - The number to return to pool
	 */
	private async returnNumberToPool(roomId: string, baseName: string, number: number): Promise<void> {
		const poolKey = `${RedisKeyName.PARTICIPANT_NAME_POOL}${roomId}:${baseName}`;

		try {
			// Add number to sorted set (score = number for natural ordering)
			await this.redisService.addToSortedSet(poolKey, number, number.toString());

			// Set TTL on pool key to prevent memory leaks
			await this.redisService.setExpiration(poolKey, this.PARTICIPANT_NAME_TTL);

			this.logger.verbose(`Returned number ${number} to pool for '${baseName}' in room '${roomId}'`);
		} catch (error) {
			this.logger.warn(`Error returning number to pool:`, error);
		}
	}
}
