import archiver from 'archiver';
import { Request, Response } from 'express';
import { Readable } from 'stream';
import { container } from '../config/index.js';
import INTERNAL_CONFIG from '../config/internal-config.js';
import { RecordingHelper } from '../helpers/index.js';
import {
	errorRecordingNotFound,
	errorRecordingsNotFromSameRoom,
	handleError,
	internalError,
	rejectRequestFromMeetError
} from '../models/error.model.js';
import { LoggerService, MeetStorageService, RecordingService } from '../services/index.js';

export const startRecording = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);
	const { roomId } = req.body;
	logger.info(`Starting recording in room '${roomId}'`);

	try {
		const recordingInfo = await recordingService.startRecording(roomId);
		res.setHeader(
			'Location',
			`${req.protocol}://${req.get('host')}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingInfo.recordingId}`
		);

		return res.status(201).json(recordingInfo);
	} catch (error) {
		handleError(res, error, `starting recording in room '${roomId}'`);
	}
};

export const getRecordings = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);
	const queryParams = req.query;

	// If recording token is present, retrieve only recordings for the room associated with the token
	const payload = req.session?.tokenClaims;

	if (payload && payload.video) {
		const roomId = payload.video.room;
		queryParams.roomId = roomId;
		logger.info(`Getting recordings for room '${roomId}'`);
	} else {
		logger.info('Getting all recordings');
	}

	try {
		const { recordings, isTruncated, nextPageToken } = await recordingService.getAllRecordings(queryParams);
		const maxItems = Number(queryParams.maxItems);

		return res.status(200).json({
			recordings,
			pagination: {
				isTruncated,
				nextPageToken,
				maxItems
			}
		});
	} catch (error) {
		handleError(res, error, 'getting recordings');
	}
};

export const bulkDeleteRecordings = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);
	const { recordingIds } = req.query;

	// If recording token is present, delete only recordings for the room associated with the token
	const payload = req.session?.tokenClaims;
	let roomId: string | undefined;

	if (payload && payload.video) {
		roomId = payload.video.room;
	}

	logger.info(`Deleting recordings: ${recordingIds}`);

	try {
		const recordingIdsArray = (recordingIds as string).split(',');
		const { deleted, failed } = await recordingService.bulkDeleteRecordingsAndAssociatedFiles(
			recordingIdsArray,
			roomId
		);

		// All recordings were successfully deleted
		if (deleted.length > 0 && failed.length === 0) {
			return res.status(200).json({ message: 'All recordings deleted successfully', deleted });
		}

		// Some or all recordings could not be deleted
		return res.status(400).json({ message: `${failed.length} recording(s) could not be deleted`, deleted, failed });
	} catch (error) {
		handleError(res, error, 'deleting recordings');
	}
};

export const getRecording = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);
	const recordingId = req.params.recordingId;
	const fields = req.query.fields as string | undefined;

	logger.info(`Getting recording '${recordingId}'`);

	try {
		const recordingInfo = await recordingService.getRecording(recordingId, fields);
		return res.status(200).json(recordingInfo);
	} catch (error) {
		handleError(res, error, `getting recording '${recordingId}'`);
	}
};

export const stopRecording = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingId = req.params.recordingId;

	try {
		logger.info(`Stopping recording '${recordingId}'`);
		const recordingService = container.get(RecordingService);

		const recordingInfo = await recordingService.stopRecording(recordingId);
		res.setHeader(
			'Location',
			`${req.protocol}://${req.get('host')}${INTERNAL_CONFIG.API_BASE_PATH_V1}/recordings/${recordingId}`
		);
		return res.status(202).json(recordingInfo);
	} catch (error) {
		handleError(res, error, `stopping recording '${recordingId}'`);
	}
};

export const deleteRecording = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);
	const recordingId = req.params.recordingId;
	logger.info(`Deleting recording '${recordingId}'`);

	try {
		await recordingService.deleteRecording(recordingId);
		return res.status(200).json({ message: `Recording '${recordingId}' deleted successfully` });
	} catch (error) {
		handleError(res, error, `deleting recording '${recordingId}'`);
	}
};

/**
 * Get recording media
 *
 * This controller endpoint retrieves a recording by its ID and streams it as a video/mp4 file.
 * It supports HTTP range requests, allowing for features like video seeking and partial downloads.
 *
 */
export const getRecordingMedia = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);

	const recordingId = req.params.recordingId;
	const range = req.headers.range;
	let fileStream: Readable | undefined;

	try {
		logger.info(`Streaming recording '${recordingId}'`);
		const recordingService = container.get(RecordingService);

		const result = await recordingService.getRecordingAsStream(recordingId, range);
		const { fileSize, start, end } = result;
		fileStream = result.fileStream;

		fileStream.on('error', (streamError) => {
			logger.error(`Error streaming recording '${recordingId}': ${streamError.message}`);

			if (!res.headersSent) {
				const error = internalError(`streaming recording '${recordingId}'`);
				rejectRequestFromMeetError(res, error);
			}

			res.end();
		});

		// Handle client disconnection
		req.on('close', () => {
			if (fileStream && !fileStream.destroyed) {
				logger.debug(`Client closed connection for recording media '${recordingId}'`);
				fileStream.destroy();
			}
		});

		// Handle partial requests (HTTP Range requests)
		if (range && fileSize && start !== undefined && end !== undefined) {
			const contentLength = end - start + 1;

			// Set headers for partial content response
			res.writeHead(206, {
				'Content-Range': `bytes ${start}-${end}/${fileSize}`,
				'Accept-Ranges': 'bytes',
				'Content-Length': contentLength,
				'Content-Type': 'video/mp4',
				'Cache-Control': 'public, max-age=3600'
			});
		} else {
			// Set headers for full content response
			res.writeHead(200, {
				'Accept-Ranges': 'bytes',
				'Content-Type': 'video/mp4',
				'Content-Length': fileSize || undefined,
				'Cache-Control': 'public, max-age=3600'
			});
		}

		fileStream
			.pipe(res)
			.on('finish', () => {
				logger.debug(`Finished streaming recording '${recordingId}'`);
				res.end();
			})
			.on('error', (err) => {
				logger.error(`Error in response stream for recording '${recordingId}': ${err.message}`);

				if (!res.headersSent) {
					res.status(500).end();
				}
			});
	} catch (error) {
		if (fileStream && !fileStream.destroyed) {
			fileStream.destroy();
		}

		handleError(res, error, `streaming recording '${recordingId}'`);
	}
};

export const getRecordingUrl = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingId = req.params.recordingId;
	const privateAccess = req.query.privateAccess === 'true';

	logger.info(`Getting URL for recording '${recordingId}'`);

	try {
		const storageService = container.get(MeetStorageService);
		const recordingSecrets = await storageService.getAccessRecordingSecrets(recordingId);

		if (!recordingSecrets) {
			const error = errorRecordingNotFound(recordingId);
			return rejectRequestFromMeetError(res, error);
		}

		const secret = privateAccess ? recordingSecrets.privateAccessSecret : recordingSecrets.publicAccessSecret;
		const recordingUrl = `${req.protocol}://${req.get('host')}/recording/${recordingId}?secret=${secret}`;

		return res.status(200).json({ url: recordingUrl });
	} catch (error) {
		handleError(res, error, `getting URL for recording '${recordingId}'`);
	}
};

export const downloadRecordingsZip = async (req: Request, res: Response) => {
	const logger = container.get(LoggerService);
	const recordingService = container.get(RecordingService);

	const recordingIds = req.query.recordingIds as string;
	const recordingIdsArray = (recordingIds as string).split(',');

	// If recording token is present, download only recordings for the room associated with the token
	const payload = req.session?.tokenClaims;
	let roomId: string | undefined;

	if (payload && payload.video) {
		roomId = payload.video.room;
	}

	// Filter recording IDs if a room ID is provided
	let validRecordingIds = recordingIdsArray;

	if (roomId) {
		validRecordingIds = recordingIdsArray.filter((recordingId) => {
			const { roomId: recRoomId } = RecordingHelper.extractInfoFromRecordingId(recordingId);
			const isValid = recRoomId === roomId;

			if (!isValid) {
				logger.warn(`Skipping recording '${recordingId}' as it does not belong to room '${roomId}'`);
			}

			return isValid;
		});
	}

	if (validRecordingIds.length === 0) {
		logger.warn(`None of the provided recording IDs belong to room '${roomId}'`);
		const error = errorRecordingsNotFromSameRoom(roomId!);
		return rejectRequestFromMeetError(res, error);
	}

	logger.info(`Creating ZIP for recordings: ${recordingIds}`);

	res.setHeader('Content-Type', 'application/zip');
	res.setHeader('Content-Disposition', 'attachment; filename="recordings.zip"');

	const archive = archiver('zip', { zlib: { level: 0 } });

	// Handle errors in the archive
	archive.on('error', (err) => {
		logger.error(`ZIP archive error: ${err.message}`);
		res.status(500).end();
	});

	// Pipe the archive to the response
	archive.pipe(res);

	for (const recordingId of validRecordingIds) {
		try {
			logger.debug(`Adding recording '${recordingId}' to ZIP`);
			const result = await recordingService.getRecordingAsStream(recordingId);
			const recordingInfo = await recordingService.getRecording(recordingId, 'filename');

			const filename = recordingInfo.filename || `${recordingId}.mp4`;
			archive.append(result.fileStream, { name: filename });
		} catch (error) {
			logger.error(`Error adding recording '${recordingId}' to ZIP: ${error}`);
		}
	}

	// Finalize the archive
	archive.finalize();
};
