import { AuditStrategy } from './strategies/auditStrategyService';
import { AuditPayloadMasker } from '#utils/auditPayloadMasker';
import { randomUUID } from 'crypto';
import { SystemAuditLog } from '#models/SystemAuditLog';
import { EntityChangeLog } from '#models/EntityChangeLog';
import { AuditOutbox } from '#models/AuditOutbox';
import auditMetrics from '#utils/auditMetrics';
import { Req } from '#interfaces/IApi';
import Config from '#config';
import { createAuditStrategy } from './strategies/auditStrategyFactoryService';

export class AuditLogService {
	constructor(private strategy: AuditStrategy) {}

	// Singleton instance for backward compatibility with static methods
	private static instance: AuditLogService | null = null;

	private static getInstance(): AuditLogService {
		if (!this.instance) {
			const strategy = createAuditStrategy(Config.audit.mode);
			this.instance = new AuditLogService(strategy);
		}
		return this.instance;
	}

	/**
	 * Extract common audit context from request object
	 */
	private extractAuditContext(req: Req) {
		return {
			requestId: req.requestId || '',
			traceId: req.traceId || '',
			actorId: req.user?.id || '',
			actorName: req.user?.username || req.user?.email || 'Unknown',
			actorEmail: req.user?.email || '',
			actorRole: req.user?.roles?.[0] || 'USER',
			ipAddress: req.ip || '',
			userAgent: req.headers['user-agent'] || '',
		};
	}
	/**
	 * Enqueue audit event to outbox (called within transaction)
	 * This is the ONLY method to be called from business logic
	 */
	async enqueueSystemAudit(params: {
		requestId: string;
		traceId: string;
		sessionId?: string;
		tenantId?: string;
		actorId: string;
		actorName: string;
		actorEmail: string;
		actorRole: string;
		action: string;
		module: string;
		entityId?: string;
		entityType?: string;
		batchId?: string;
		description?: string;
		severity?: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
		ipAddress?: string;
		userAgent?: string;
		payload?: object;
		success?: boolean;
		failureReason?: string;
	}) {
		// Use strategy pattern to enqueue audit event
		await this.strategy.logSystemAudit({
			action: params.action,
			module: params.module,
			actor_name: params.actorName,
			actor_id: params.actorId,
			severity: params.severity || 'LOW',
			metadata: {
				requestId: params.requestId,
				traceId: params.traceId,
				sessionId: params.sessionId,
				tenantId: params.tenantId,
				actorEmail: params.actorEmail,
				actorRole: params.actorRole,
				entityId: params.entityId,
				entityType: params.entityType,
				batchId: params.batchId,
				description: params.description,
				ipAddress: params.ipAddress,
				userAgent: params.userAgent,
				payload: params.payload,
				success: params.success,
				failureReason: params.failureReason,
			},
		});

		// Record metric
		try {
			auditMetrics.auditEventsEnqueued.inc({ event_type: 'SYSTEM_AUDIT', module: params.module });
		} catch {
			// Metrics optional, don't fail if not available
		}
	}

	// Static wrapper for backward compatibility
	static async enqueueSystemAudit(params: any) {
		return this.getInstance().enqueueSystemAudit(params);
	}

	/**
	 * Enqueue entity change event to outbox (called within transaction)
	 */
	async enqueueEntityChange(params: {
		requestId: string;
		traceId: string;
		entityType: string;
		entityId: string;
		actorId: string;
		actorName: string;
		actorEmail: string;
		changeType: string;
		payload: { before: any; after: any; changed_fields: string[] };
		reason?: string;
	}) {
		// Use strategy pattern to enqueue entity change event
		await this.strategy.logEntityChange({
			entity_type: params.entityType,
			entity_id: params.entityId,
			change_type: params.changeType,
			actor_name: params.actorName,
			actor_id: params.actorId,
			actor_email: params.actorEmail,
			changes: params.payload,
		});

		// Record metric
		try {
			auditMetrics.auditEventsEnqueued.inc({ event_type: 'ENTITY_CHANGE', module: params.entityType });
		} catch {
			// Metrics optional, don't fail if not available
		}
	}

	// Static wrapper for backward compatibility
	static async enqueueEntityChange(params: any) {
		return this.getInstance().enqueueEntityChange(params);
	}

	/**
	 * Write entity change directly to entity_change_logs (bypass outbox)
	 * Temporary solution for testing while OutboxPoller is being fixed
	 */
	async writeEntityChangeDirect(params: {
		requestId: string;
		traceId: string;
		entityType: string;
		entityId: string;
		actorId: string;
		actorName: string;
		actorEmail: string;
		changeType: string;
		payload: { before: any; after: any; changed_fields: string[] };
		reason?: string;
	}) {
		// Helper to extract plain data from Sequelize model or plain object
		const toPlainData = (data: any): any => {
			if (!data) return data;
			if (typeof data.toJSON === 'function') return data.toJSON();
			if (typeof data.get === 'function') return data.get({ plain: true });
			// If it's already a plain object with internal Sequelize structure
			if (typeof data === 'object') {
				// Check if it has dataValues (Sequelize model wrapped in plain object)
				if (data.dataValues && typeof data.dataValues === 'object') {
					return data.dataValues;
				}
				// Filter out internal Sequelize fields and fix date serialization
				const filtered: any = {};
				const internalFields = ['uniqno', '_changed', '_options', 'dataValues', 'isNewRecord', '_previousDataValues'];
				for (const key in data) {
					if (!internalFields.includes(key)) {
						// Convert Date objects to ISO strings
						const value = data[key];
						if (value instanceof Date) {
							filtered[key] = value.toISOString();
						} else if (value && typeof value === 'object' && !(value instanceof Array)) {
							// Recursively process nested objects
							filtered[key] = toPlainData(value);
						} else {
							filtered[key] = value;
						}
					}
				}
				return filtered;
			}
			return data;
		};

		const plainPayload = {
			before: toPlainData(params.payload.before),
			after: toPlainData(params.payload.after),
			changed_fields: Array.isArray(params.payload.changed_fields)
				? params.payload.changed_fields
				: Object.values(params.payload.changed_fields || {}),
		};

		const validatedPayload = AuditPayloadMasker.validateAndTruncate(plainPayload);
		const maskedPayload = AuditPayloadMasker.mask(validatedPayload);
		const payloadSize = AuditPayloadMasker.calculateSize(validatedPayload);

		const standardizedPayload = {
			meta: {
				source: 'api',
				version: 1,
				timestamp: new Date().toISOString(),
			},
			before: maskedPayload.before,
			after: maskedPayload.after,
			changed_fields: maskedPayload.changed_fields,
			context: {},
		};

		await EntityChangeLog.create({
			id: randomUUID(),
			outbox_event_id: randomUUID(),
			request_id: params.requestId,
			trace_id: params.traceId,
			entity_type: params.entityType,
			entity_id: params.entityId,
			actor_id: params.actorId,
			actor_name: params.actorName,
			actor_email: params.actorEmail,
			change_type: params.changeType,
			payload: standardizedPayload,
			payload_size: payloadSize,
			reason: params.reason ?? null,
			event_version: 1,
			created_at: new Date(),
		});

		// Record metric
		try {
			auditMetrics.auditEventsEnqueued.inc({ event_type: 'ENTITY_CHANGE', module: params.entityType });
		} catch {
			// Metrics optional, don't fail if not available
		}
	}

	// Static wrapper for backward compatibility
	static async writeEntityChangeDirect(params: any) {
		return this.getInstance().writeEntityChangeDirect(params);
	}

	/**
	 * Bulk enqueue audit events for large imports
	 * Avoids transaction bloat by chunking
	 */
	async bulkEnqueueSystemAudit(events: any[], req: any) {
		const CHUNK_SIZE = 1000; // Process 1000 events at a time

		for (let i = 0; i < events.length; i += CHUNK_SIZE) {
			const chunk = events.slice(i, i + CHUNK_SIZE);

			// Bulk insert to outbox
			const outboxRecords = chunk.map((event) => {
				const validatedPayload = event.payload ? AuditPayloadMasker.validateAndTruncate(event.payload) : null;

				const maskedPayload = validatedPayload ? AuditPayloadMasker.mask(validatedPayload) : null;
				const payloadSize = validatedPayload ? AuditPayloadMasker.calculateSize(validatedPayload) : 0;
				const outboxEventId = randomUUID();

				const standardizedPayload = {
					meta: {
						source: 'bulk_import',
						version: 1,
						timestamp: new Date().toISOString(),
					},
					data: maskedPayload,
					context: {},
				};

				return {
					id: outboxEventId,
					request_id: req.requestId,
					trace_id: req.traceId,
					event_type: 'SYSTEM_AUDIT',
					payload: {
						...event,
						outbox_event_id: outboxEventId,
						payload: standardizedPayload,
						payload_size: payloadSize,
						event_version: 1,
						actorId: req.user.id,
						actorName: `${req.user.first_name} ${req.user.last_name}`,
						actorEmail: req.user.email,
						actorRole: 'ADMIN',
					},
					payload_size: payloadSize,
					status: 'PENDING' as const,
					created_at: new Date(),
					updated_at: new Date(),
					retry_count: 0,
					max_retries: 3,
				};
			});

			await AuditOutbox.bulkCreate(outboxRecords);
		}

		// Log bulk operation summary
		await AuditOutbox.create({
			id: randomUUID(),
			request_id: req.requestId,
			trace_id: req.traceId,
			event_type: 'SYSTEM_AUDIT',
			payload: {
				action: 'BULK_IMPORT',
				module: 'CRM',
				description: `Bulk import ${events.length} residents`,
				severity: 'HIGH',
				payload: { count: events.length },
			},
			payload_size: JSON.stringify({ count: events.length }).length,
			status: 'PENDING' as const,
			created_at: new Date(),
			updated_at: new Date(),
			retry_count: 0,
			max_retries: 3,
		});
	}

	/**
	 * Query system audit logs with filters
	 */
	async queryAuditLogs(params: {
		filters?: any;
		sortField?: string;
		sortOrder?: string;
		page?: number;
		pageSize?: number;
	}) {
		const { page = 1, pageSize = 50, filters, sortField, sortOrder } = params;
		const offset = (page - 1) * pageSize;

		// Use filters from queryModifier middleware
		const where: any = filters || {};

		// Validate sortField to prevent SQL injection
		const allowedSortFields = ['created_at', 'action', 'module', 'actor_name', 'severity', 'event_version'];
		const validSortField = allowedSortFields.includes(sortField || '') ? sortField : 'created_at';
		const validSortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC';

		const order: any = [[validSortField, validSortOrder]];

		const { count, rows } = await SystemAuditLog.findAndCountAll({
			where,
			order,
			limit: pageSize,
			offset,
		});

		return { count, rows, page, pageSize };
	}

	// Static wrapper for backward compatibility
	static async queryAuditLogs(params: any) {
		return this.getInstance().queryAuditLogs(params);
	}

	/**
	 * Get entity change history
	 */
	async getEntityChangeHistory(
		entityType: string,
		entityId: string,
		options?: { page?: number; pageSize?: number; sortField?: string; sortOrder?: 'ASC' | 'DESC' | undefined },
	) {
		const { page = 1, pageSize = 10, sortField = 'created_at', sortOrder } = options || {};
		const offset = (page - 1) * pageSize;

		// Validate sortField to prevent SQL injection
		const allowedSortFields = ['created_at', 'change_type', 'actor_name', 'event_version'];
		const validSortField = allowedSortFields.includes(sortField) ? sortField : 'created_at';
		const validSortOrder = sortOrder === 'ASC' ? 'ASC' : 'DESC';

		const { count, rows } = await EntityChangeLog.findAndCountAll({
			where: { entity_type: entityType, entity_id: entityId },
			order: [[validSortField, validSortOrder]],
			limit: pageSize,
			offset,
		});

		return {
			count,
			rows,
			page,
			pageSize,
		};
	}

	// Static wrapper for backward compatibility
	static async getEntityChangeHistory(entityType: string, entityId: string, options?: any) {
		return this.getInstance().getEntityChangeHistory(entityType, entityId, options);
	}

	/**
	 * Enqueue audit event from request (simplified version)
	 * Automatically extracts common context from request object
	 */
	async enqueueSystemAuditFromRequest(
		req: Req,
		params: {
			action: string;
			module: string;
			entityId?: string;
			entityType?: string;
			batchId?: string;
			description?: string;
			severity?: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
			payload?: object;
			success?: boolean;
			failureReason?: string;
		},
	) {
		return await this.enqueueSystemAudit({
			...this.extractAuditContext(req),
			...params,
		});
	}

	// Static wrapper for backward compatibility
	static async enqueueSystemAuditFromRequest(req: any, params: any) {
		return this.getInstance().enqueueSystemAuditFromRequest(req, params);
	}

	/**
	 * Enqueue entity change event from request (simplified version)
	 * Automatically extracts common context from request object
	 */
	async enqueueEntityChangeFromRequest(
		req: Req,
		params: {
			entityType: string;
			entityId: string;
			changeType: string;
			payload: { before: any; after: any; changed_fields: string[] };
			reason?: string;
		},
	) {
		// Temporary: use direct write instead of outbox
		return await this.writeEntityChangeDirect({
			...this.extractAuditContext(req),
			...params,
		});
	}

	// Static wrapper for backward compatibility
	static async enqueueEntityChangeFromRequest(req: any, params: any) {
		return this.getInstance().enqueueEntityChangeFromRequest(req, params);
	}

	/**
	 * Get audit logs by trace ID (for workflow correlation)
	 * PERF-05: Add limit to prevent loading excessive rows
	 */
	async getByTraceId(traceId: string, limit = 100) {
		const rows = await SystemAuditLog.findAll({
			where: { trace_id: traceId },
			order: [['created_at', 'ASC']],
			limit,
		});
		return { count: rows.length, rows };
	}

	// Static wrapper for backward compatibility
	static async getByTraceId(traceId: string, limit?: number) {
		return this.getInstance().getByTraceId(traceId, limit);
	}

	/**
	 * Get audit logs by batch ID (for bulk operations)
	 * PERF-05: Add limit to prevent loading excessive rows
	 */
	async getByBatchId(batchId: string, limit = 100) {
		const rows = await SystemAuditLog.findAll({
			where: { batch_id: batchId },
			order: [['created_at', 'ASC']],
			limit,
		});
		return { count: rows.length, rows };
	}

	// Static wrapper for backward compatibility
	static async getByBatchId(batchId: string, limit?: number) {
		return this.getInstance().getByBatchId(batchId, limit);
	}
}
