/**
 * Queue Manager - Core queue operations and event management
 *
 * Manages an in-memory queue of Instagram URL processing jobs.
 * Provides CRUD operations and pub/sub mechanism for queue updates.
 *
 * Architecture: Domain Layer (Hexagonal Architecture)
 * - Port: Defines queue operations interface
 * - Implementation: In-memory Map-based storage
 */

import { v4 as uuidv4 } from 'uuid';
import { tandoorConfig } from '$lib/server/tandoor-config';
import { logError } from '../utils/logger';
import type { QueueItem, QueueItemStatus, QueueStatusUpdate, QueueUpdateCallback } from './types';

/**
 * Singleton queue manager for processing Instagram URLs
 *
 * Features:
 * - FIFO queue with unique IDs
 * - Status tracking and updates
 * - Progress event accumulation
 * - Retry support for failed items
 * - Pub/sub for real-time updates
 *
 * @example
 * ```typescript
 * import { queueManager } from './QueueManager';
 *
 * // Add item to queue
 * const item = queueManager.enqueue('https://instagram.com/p/abc123');
 *
 * // Subscribe to updates
 * const unsubscribe = queueManager.subscribe((update) => {
 *   console.log('Item updated:', update);
 * });
 *
 * // Get all items
 * const items = queueManager.getAll();
 * ```
 */
export class QueueManager {
	/** Map of queue items by ID */
	private items: Map<string, QueueItem> = new Map();

	/** Set of subscriber callbacks */
	private subscribers: Set<QueueUpdateCallback> = new Set();

	/**
	 * Find queue item by URL
	 *
	 * @param url - Instagram URL to search for
	 * @returns Existing queue item or undefined
	 */
	findByUrl(url: string): QueueItem | undefined {
		for (const item of this.items.values()) {
			if (item.url === url) {
				return item;
			}
		}
		return undefined;
	}

	/**
	 * Add URL to processing queue
	 *
	 * @param url - Instagram URL to process
	 * @returns Newly created queue item
	 *
	 * @example
	 * ```typescript
	 * const item = queueManager.enqueue('https://instagram.com/p/abc123');
	 * console.log('Queued with ID:', item.id);
	 * ```
	 */
	enqueue(url: string): QueueItem {
		// Check for duplicate URL
		const existingItem = this.findByUrl(url);
		if (existingItem) {
			console.log(`[QueueManager] Duplicate URL detected: ${url}, returning existing item ${existingItem.id}`);
			return existingItem;
		}

		const now = new Date().toISOString();
		const item: QueueItem = {
			id: uuidv4(),
			url,
			status: 'pending',
			enqueuedAt: now,
			createdAt: now,
			updatedAt: now,
			phases: [
				{ name: 'extraction', status: 'pending' },
				{ name: 'parsing', status: 'pending' },
				{ name: 'uploading', status: 'pending' }
			],
			logs: [],
			progressEvents: [],
			retryCount: 0,
			maxRetries: 3
		};

		this.items.set(item.id, item);
		this.notifySubscribers({
			type: 'status_change',
			itemId: item.id,
			status: 'pending',
			url: item.url,
			timestamp: now,
			progress: item.phases
		});

		return item;
	}

	/**
	 * Get next pending item for processing (FIFO)
	 *
	 * Automatically marks the item as in_progress when dequeued.
	 *
	 * @returns Next pending item, or null if queue is empty
	 *
	 * @example
	 * ```typescript
	 * const item = queueManager.dequeue();
	 * if (item) {
	 *   // Process item
	 *   console.log('Processing:', item.url);
	 * }
	 * ```
	 */
	dequeue(): QueueItem | null {
		for (const item of this.items.values()) {
			if (item.status === 'pending') {
				this.updateStatus(item.id, 'in_progress', { phase: 'extraction' });
				return item;
			}
		}
		return null;
	}

	/**
	 * Update item status and optional data
	 *
	 * Handles status-specific logic:
	 * - Sets startedAt when transitioning to in_progress
	 * - Sets completedAt when transitioning to success/error
	 * - Updates currentPhase for in_progress status
	 *
	 * @param itemId - ID of item to update
	 * @param status - New status
	 * @param data - Optional additional data to merge into item
	 *
	 * @example
	 * ```typescript
	 * queueManager.updateStatus(itemId, 'in_progress', {
	 *   phase: 'parsing'
	 * });
	 *
	 * queueManager.updateStatus(itemId, 'success', {
	 *   recipe: parsedRecipe,
	 *   tandoorRecipeId: 123
	 * });
	 * ```
	 */
	updateStatus(itemId: string, status: QueueItemStatus, data?: any): void {
		const item = this.items.get(itemId);
		if (!item) return;

		const now = new Date().toISOString();
		item.status = status;
		item.updatedAt = now;

		// Update phase progress
		if (status === 'in_progress' && data?.phase) {
			item.currentPhase = data.phase;

			if (!item.startedAt) {
				item.startedAt = now;
			}

			// Update phases array
			const phaseIndex = item.phases.findIndex((p) => p.name === data.phase);
			if (phaseIndex >= 0) {
				// Mark previous phases as completed
				for (let i = 0; i < phaseIndex; i++) {
					if (item.phases[i].status === 'in_progress') {
						item.phases[i].status = 'completed';
						item.phases[i].completedAt = now;
					}
				}
				// Mark current phase as in progress
				item.phases[phaseIndex].status = 'in_progress';
				item.phases[phaseIndex].startedAt = now;
			}
		}

		if (status === 'success') {
			item.completedAt = now;
			// Mark all phases as completed
			item.phases.forEach((phase) => {
				if (phase.status !== 'completed') {
					phase.status = 'completed';
					phase.completedAt = now;
				}
			});
		}

		if (status === 'error' || status === 'unhealthy') {
			item.completedAt = now;
			// Mark current phase as error
			if (item.currentPhase) {
				const phaseIndex = item.phases.findIndex((p) => p.name === item.currentPhase);
				if (phaseIndex >= 0) {
					item.phases[phaseIndex].status = 'error';
					item.phases[phaseIndex].error = data?.error?.message;
				}
			}
		}

		// Wrap results in results object
		if (
			data?.extractedText ||
			data?.thumbnail !== undefined ||
			data?.recipe ||
			data?.tandoorRecipeId
		) {
			if (!item.results) {
				item.results = {};
			}

			if (data.extractedText) {
				item.results.extractedText = data.extractedText;
				item.extractedText = data.extractedText; // Keep legacy
			}
			if (data.thumbnail !== undefined) {
				item.results.thumbnail = data.thumbnail;
				item.thumbnail = data.thumbnail; // Keep legacy
			}
			if (data.recipe) {
				item.results.recipe = data.recipe;
				item.recipe = data.recipe; // Keep legacy
			}
			if (data.tandoorRecipeId) {
				item.results.tandoorRecipeId = data.tandoorRecipeId;
				item.tandoorRecipeId = data.tandoorRecipeId; // Keep legacy

				// Construct Tandoor URL
				if (tandoorConfig.serverUrl) {
					item.results.tandoorUrl = `${tandoorConfig.serverUrl}/view/recipe/${data.tandoorRecipeId}`;
				}
			}
		}

		if (data?.error) {
			item.error = data.error;
		}

		// Notify subscribers with enhanced update
		this.notifySubscribers({
			type: 'status_change',
			itemId,
			status,
			timestamp: now,
			url: item.url,
			phase: item.currentPhase,
			progress: item.phases,
			results: item.results,
			error: item.error,
			...data
		});
	}

	/**
	 * Add progress event to item's history
	 *
	 * Also extracts message into logs array for easy display.
	 *
	 * @param itemId - ID of item
	 * @param event - Progress event to add
	 *
	 * @example
	 * ```typescript
	 * queueManager.addProgressEvent(itemId, {
	 *   type: 'status',
	 *   message: 'Extracting from Instagram...',
	 *   timestamp: new Date().toISOString()
	 * });
	 * ```
	 */
	addProgressEvent(itemId: string, event: any): void {
		const item = this.items.get(itemId);
		if (!item) return;

		item.progressEvents.push(event);
		item.logs.push(event.message);

		this.notifySubscribers({
			type: 'progress',
			itemId,
			status: item.status,
			timestamp: new Date().toISOString(),
			data: { event }
		});
	}

	/**
	 * Remove item from queue
	 *
	 * @param itemId - ID of item to remove
	 * @returns true if item was removed, false if not found
	 *
	 * @example
	 * ```typescript
	 * const removed = queueManager.remove(itemId);
	 * if (removed) {
	 *   console.log('Item removed successfully');
	 * }
	 * ```
	 */
	remove(itemId: string): boolean {
		const deleted = this.items.delete(itemId);
		if (deleted) {
			this.notifySubscribers({
				type: 'status_change',
				itemId,
				status: 'error', // Use error to signal removal
				timestamp: new Date().toISOString(),
				data: { removed: true }
			});
		}
		return deleted;
	}

	/**
	 * Retry a failed or unhealthy item
	 *
	 * Resets item to pending status and clears error state.
	 * Cannot retry items currently in progress.
	 *
	 * @param itemId - ID of item to retry
	 * @returns true if retry was initiated, false otherwise
	 *
	 * @example
	 * ```typescript
	 * const retried = queueManager.retry(itemId);
	 * if (retried) {
	 *   console.log('Item queued for retry');
	 * } else {
	 *   console.log('Cannot retry (item in progress or not found)');
	 * }
	 * ```
	 */
	retry(itemId: string): boolean {
		const item = this.items.get(itemId);
		if (!item || item.status === 'in_progress') return false;

		item.retryCount++;
		item.status = 'pending';
		item.currentPhase = undefined;
		item.error = undefined;
		item.startedAt = undefined;
		item.completedAt = undefined;

		// Reset phases to pending
		item.phases = [
			{ name: 'extraction', status: 'pending' },
			{ name: 'parsing', status: 'pending' },
			{ name: 'uploading', status: 'pending' }
		];

		this.notifySubscribers({
			type: 'status_change',
			itemId,
			status: 'pending',
			timestamp: new Date().toISOString(),
			progress: item.phases,
			data: { retryCount: item.retryCount }
		});

		return true;
	}

	/**
	 * Get all queue items
	 *
	 * @returns Array of all queue items
	 *
	 * @example
	 * ```typescript
	 * const items = queueManager.getAll();
	 * console.log(`Queue has ${items.length} items`);
	 * ```
	 */
	getAll(): QueueItem[] {
		return Array.from(this.items.values());
	}

	/**
	 * Get single item by ID
	 *
	 * @param itemId - ID of item to retrieve
	 * @returns Queue item or undefined if not found
	 *
	 * @example
	 * ```typescript
	 * const item = queueManager.get(itemId);
	 * if (item) {
	 *   console.log('Status:', item.status);
	 * }
	 * ```
	 */
	get(itemId: string): QueueItem | undefined {
		return this.items.get(itemId);
	}

	/**
	 * Subscribe to queue updates
	 *
	 * Callback will be called whenever any item is updated.
	 *
	 * @param callback - Function to call on each update
	 * @returns Unsubscribe function
	 *
	 * @example
	 * ```typescript
	 * const unsubscribe = queueManager.subscribe((update) => {
	 *   console.log('Update:', update.itemId, update.status);
	 * });
	 *
	 * // Later...
	 * unsubscribe();
	 * ```
	 */
	subscribe(callback: QueueUpdateCallback): () => void {
		this.subscribers.add(callback);
		return () => this.subscribers.delete(callback);
	}

	/**
	 * Notify all subscribers of an update
	 *
	 * Handles errors in individual subscribers to prevent one
	 * bad subscriber from affecting others.
	 *
	 * @param update - Update to broadcast
	 */
	private notifySubscribers(update: QueueStatusUpdate): void {
		for (const callback of this.subscribers) {
			try {
				callback(update);
			} catch (err) {
				logError('[QueueManager] Subscriber error', err);
			}
		}
	}
}

/**
 * Singleton instance of QueueManager
 *
 * Use this instance throughout the application to ensure
 * all components interact with the same queue.
 */
export const queueManager = new QueueManager();
