import { NextFunction } from 'express';
import { randomUUID } from 'crypto';
import { OkParams, ErrorDTO, ResponseDTO, Req, Res } from '#interfaces/IApi';
import LoggingService from '../services/file-system/logService';
import { ErrorProcessor } from '#interfaces/error/violations/IViolations';
import { getHttpStatusFromCode, normalizeLegacyErrorType, getErrorSeverity } from '#constants/errors';
import { extractValidationErrors } from '#interfaces/error/utils/IValidationExtractor';

const logger = new LoggingService();

export default function (_req: Req, res: Res, next: NextFunction): void {
	const errorProcessor = ErrorProcessor.getInstance();
	const traceId = (_req as any).requestId || randomUUID();

	res.error = function (err: unknown) {
		console.log('Error caught:', err instanceof Error ? err.name : typeof err, err);

		// Handle Sequelize exclusion constraint violations (e.g., bed occupancy overlap)
		if (err instanceof Error && err.name === 'SequelizeExclusionConstraintError') {
			const sequelizeErr = err as any;
			const constraint = sequelizeErr.constraint || '';
			const table = sequelizeErr.table || '';

			console.log('SequelizeExclusionConstraintError:', { constraint, table });

			const errorDTOs = [
				new ErrorDTO(
					'bed_id',
					'OCCUPANCY_CONFLICT',
					{
						vi: 'Giường đã có resident sử dụng trong khoảng thời gian này',
						en: 'Bed is already occupied during this time period',
					},
					'low',
					sequelizeErr.stack,
				),
			];

			return res.status(409).json(
				ResponseDTO({
					errors: errorDTOs,
					traceId,
				}),
			);
		}

		// Handle Sequelize foreign key constraint violations
		if (err instanceof Error && err.name === 'SequelizeForeignKeyConstraintError') {
			const sequelizeErr = err as any;
			const constraint = sequelizeErr.index || sequelizeErr.constraint || '';
			const table = sequelizeErr.table || '';
			const detail = sequelizeErr.parent?.detail || '';

			console.log('SequelizeForeignKeyConstraintError:', { constraint, table, detail });

			// Check if this is a DELETE operation (only when detail contains "still referenced")
			const isDeleteOperation = detail.includes('still referenced');

			let field = '';
			let messages: { vi: string; en: string };

			if (isDeleteOperation) {
				// DELETE operation - entity has related records
				field = table;
				messages = {
					vi: `Không thể xóa ${table} vì có dữ liệu liên quan`,
					en: `Cannot delete ${table} because it has related records`,
				};
			} else {
				// INSERT/UPDATE operation - extract field name from constraint name
				const fieldMatch = constraint.match(/_(\w+)_fkey$/);
				field = fieldMatch ? fieldMatch[1] : constraint;
				messages = {
					vi: `Trường ${field} tham chiếu đến bản ghi không tồn tại`,
					en: `Field ${field} references a non-existent record`,
				};
			}

			const errorDTOs = [new ErrorDTO(field, 'FOREIGN_KEY', messages, 'low', sequelizeErr.stack)];

			return res.status(400).json(
				ResponseDTO({
					errors: errorDTOs,
					traceId,
				}),
			);
		}

		// Handle Sequelize unique constraint violations
		if (err instanceof Error && err.name === 'SequelizeUniqueConstraintError') {
			const sequelizeErr = err as any;
			const fields = sequelizeErr.fields || {};
			const duplicateFields = Object.keys(fields);
			const constraint =
				sequelizeErr.constraint ||
				sequelizeErr.index ||
				sequelizeErr.parent?.constraint ||
				sequelizeErr.original?.constraint ||
				'';

			// Map field names to user-friendly names
			const fieldMap: Record<string, string> = {
				email_normalized: 'email',
				phone_normalized: 'phone',
				id_card_normalized: 'id_card',
				user_id: 'user_id',
			};

			// Handle composite unique constraints
			const compositeConstraints: Record<string, { fields: string[]; message: { vi: string; en: string } }> = {
				uq_apartments_code: {
					fields: ['building_id', 'code'],
					message: {
						vi: 'Căn hộ với mã này đã tồn tại trong tòa nhà',
						en: 'Apartment with this code already exists in building',
					},
				},
				uq_rooms_code: {
					fields: ['apartment_id', 'code'],
					message: {
						vi: 'Phòng với mã này đã tồn tại trong căn hộ',
						en: 'Room with this code already exists in apartment',
					},
				},
				uq_beds_code: {
					fields: ['room_id', 'code'],
					message: { vi: 'Giường với mã này đã tồn tại trong phòng', en: 'Bed with this code already exists in room' },
				},
			};

			const compositeConfig = compositeConstraints[constraint];
			let errorDTOs: any[];

			if (compositeConfig) {
				// Use composite constraint message
				const mainField = duplicateFields[0] || 'code';
				const userField = fieldMap[mainField] || mainField;
				errorDTOs = [new ErrorDTO(userField, 'DUPLICATE', compositeConfig.message, 'low', sequelizeErr.stack)];
			} else {
				// Single field constraint - use original mapping
				errorDTOs = duplicateFields.map((field: string) => {
					const userField = fieldMap[field] || field;
					return new ErrorDTO(
						userField,
						'DUPLICATE',
						{ vi: `Trường ${userField} đã tồn tại`, en: `Field ${userField} already exists` },
						'low',
						sequelizeErr.stack,
					);
				});
			}

			return res.status(409).json(
				ResponseDTO({
					errors: errorDTOs,
					traceId,
				}),
			);
		}

		const violations = errorProcessor.process(err);
		const mainViolation = violations[0];
		const businessCode = mainViolation?.code ?? 'INTERNAL_SERVER_ERROR';

		// Normalize legacy error types to catalog codes
		const normalizedCode = normalizeLegacyErrorType(businessCode);

		// Get HTTP status - use the error's own httpStatus if it's a GenericError, otherwise lookup from catalog
		const statusCode = (err as any).httpStatus ?? getHttpStatusFromCode(normalizedCode);

		// Debug logging for business code mapping
		if (statusCode >= 500) {
			console.warn(`HTTP 500 for business code: ${normalizedCode}, type: ${mainViolation?.type}`);
		}

		// Centralised server-error logging — controllers no longer need individual Logger.error calls
		if (statusCode >= 500) {
			logger
				.logErrorAsync(`${_req.method} ${_req.path}`, err instanceof Error ? err : new Error(String(err)), {
					requestId: traceId,
				})
				.catch(() => {});
		}

		// Check if this is a validation error with structured errors in additionalData
		const validationErrors = extractValidationErrors(mainViolation?.additionalData);

		// Get severity for the error
		const severity = getErrorSeverity(normalizedCode);

		// Get stack trace if available (only included in dev mode by ErrorDTO)
		const stack = err instanceof Error ? err.stack : undefined;

		// Convert to ErrorDTO[] - always use ErrorDTO format
		let errorDTOs: ErrorDTO[];
		if (validationErrors && Array.isArray(validationErrors)) {
			// Use structured validation errors with field, code, message
			errorDTOs = validationErrors.map((e: any) => new ErrorDTO(e.field, e.code, e.message, severity, stack));
		} else {
			// Check if additionalData contains duplicateFields (CONFLICT error)
			const additionalData = mainViolation?.additionalData as any;
			const duplicateFields = additionalData?.duplicateFields;

			if (duplicateFields && Array.isArray(duplicateFields) && duplicateFields.length > 0) {
				// Create error DTOs for each duplicate field
				errorDTOs = duplicateFields.map(
					(field: string) =>
						new ErrorDTO(
							field,
							'DUPLICATE',
							{ vi: `Trường ${field} đã tồn tại`, en: `Field ${field} already exists` },
							severity,
							stack,
						),
				);
			} else {
				// Use violation format with field from ValidationResult if available
				const field = additionalData?.field || '';
				errorDTOs = violations.map(
					(v) => new ErrorDTO(field, v.code, v.messages as { vi: string; en: string }, severity, stack),
				);
			}
		}

		return res.status(statusCode).json(
			ResponseDTO({
				errors: errorDTOs,
				traceId,
			}),
		);
	};

	res.sendOk = function ({ data, statusCode }: OkParams) {
		res.status(statusCode ?? 200).json(ResponseDTO({ data, errors: null, traceId }));
	};

	next();
}
