/**
 * Queue Processor - Orchestrates async processing of queue items
 *
 * Manages concurrent processing of Instagram URLs through three phases:
 * 1. Extraction - Browser automation to extract text and thumbnail
 * 2. Parsing - LLM-based recipe extraction
 * 3. Uploading - Automatic upload to Tandoor (if configured)
 *
 * Architecture: Domain Layer (Hexagonal Architecture)
 * - Domain Logic: Orchestrates processing workflow
 * - Uses Ports: extraction.ts, parser.ts, tandoor.ts (secondary adapters)
 */

import { queueManager } from './QueueManager';
import { extractTextAndThumbnail as extractWithPlaywright } from '$lib/server/extraction';
import { extractTextAndThumbnail as extractWithYtDlp } from '$lib/server/instagram-extractor';
import { extractRecipe } from '$lib/server/parser';
import { uploadRecipeWithIngredientsDTO, uploadRecipeImage } from '$lib/server/tandoor';
import { pushNotificationService } from '$lib/server/notifications/PushNotificationService';
import { queueConfig } from './config';
import { logError } from '../utils/logger';
import { env } from '$env/dynamic/private';
import type { ProgressEvent, ExtractedContent, ProgressCallback } from '$lib/server/extraction';
import type { QueueItem } from './types';

// Feature flag: pick which Instagram extractor backend to invoke.
// Default to yt-dlp; set EXTRACTOR_BACKEND=playwright to fall back to the
// legacy stealth scraper while we verify the new path.
const extractTextAndThumbnail = (
	url: string,
	cb?: ProgressCallback
): Promise<ExtractedContent> => {
	const backend = (env.EXTRACTOR_BACKEND ?? 'ytdlp').toLowerCase();
	return backend === 'playwright'
		? extractWithPlaywright(url, cb)
		: extractWithYtDlp(url, cb);
};

/**
 * Queue processor with configurable concurrency
 *
 * Features:
 * - Concurrent processing (default: 2 simultaneous items)
 * - Three-phase pipeline: extraction → parsing → uploading
 * - Error classification (recoverable vs non-recoverable)
 * - Progress tracking via QueueManager
 * - Automatic start on instantiation
 *
 * @example
 * ```typescript
 * import { queueProcessor } from './QueueProcessor';
 *
 * // Processor auto-starts on import
 * // Add items to queue and they'll be processed automatically
 *
 * // Stop processing (e.g., for maintenance)
 * queueProcessor.stop();
 *
 * // Resume processing
 * queueProcessor.start();
 * ```
 */
export class QueueProcessor {
	/** Whether processor is actively running */
	private processing = false;

	/** Maximum number of items to process simultaneously */
	private concurrency = queueConfig.concurrency;

	/** Number of workers currently processing items */
	private activeWorkers = 0;

	/** Unsubscribe function for queue manager subscription */
	private unsubscribeFromQueue?: () => void;

	constructor() {
		// Subscribe to queue updates to process new items immediately
		this.unsubscribeFromQueue = queueManager.subscribe((update) => {
			// Trigger processing when new items are enqueued (status_change to 'pending')
			if (update.type === 'status_change' && update.status === 'pending') {
				console.log(`[QueueProcessor] New item enqueued: ${update.itemId}, triggering processing`);
				// Use immediate processing (no timeout) for newly enqueued items
				setTimeout(() => this.processNextBatch(), 0);
			}
		});
	}

	/**
	 * Start processing queue
	 *
	 * Begins dequeuing and processing items up to concurrency limit.
	 * Safe to call multiple times - will not start duplicates.
	 */
	start(): void {
		if (this.processing) return;
		this.processing = true;
		console.log(`[QueueProcessor] Started with concurrency ${this.concurrency}`);
		this.processNextBatch();
	}

	/**
	 * Stop processing queue
	 *
	 * Prevents new items from being dequeued.
	 * Items currently in progress will complete.
	 */
	stop(): void {
		this.processing = false;
		console.log('[QueueProcessor] Stopped');

		// Cleanup subscription when stopping
		if (this.unsubscribeFromQueue) {
			this.unsubscribeFromQueue();
			this.unsubscribeFromQueue = undefined;
		}
	}

	/**
	 * Process items up to concurrency limit
	 *
	 * Dequeues pending items and starts processing them.
	 * Automatically called recursively to maintain worker pool.
	 */
	private async processNextBatch(): Promise<void> {
		if (!this.processing) return;

		// Start new workers up to concurrency limit
		while (this.activeWorkers < this.concurrency) {
			const item = queueManager.dequeue();
			if (!item) break;

			this.activeWorkers++;
			console.log(
				`[QueueProcessor] Starting item ${item.id} (${this.activeWorkers}/${this.concurrency} active)`
			);

			this.processItem(item).finally(() => {
				this.activeWorkers--;
				console.log(
					`[QueueProcessor] Finished item ${item.id} (${this.activeWorkers}/${this.concurrency} active)`
				);
				// Try to process next item immediately
				setTimeout(() => this.processNextBatch(), 0);
			});
		}

		// Check again after shorter delay if still processing and no active workers
		if (this.processing && this.activeWorkers === 0) {
			setTimeout(() => this.processNextBatch(), 100); // Reduced from 1000ms to 100ms
		}
	}

	/**
	 * Process a single queue item through all phases
	 *
	 * Executes three phases sequentially:
	 * 1. Extraction - Extract content from Instagram
	 * 2. Parsing - Parse recipe from extracted text
	 * 3. Uploading - Upload to Tandoor (if configured)
	 *
	 * On success: marks item as 'success'
	 * On error: marks item as 'unhealthy' (recoverable) or 'error' (non-recoverable)
	 *
	 * @param item - Queue item to process
	 */
	private async processItem(item: QueueItem): Promise<void> {
		try {
			console.log(`[QueueProcessor] Processing ${item.url}`);

			// Phase 1: Extraction
			await this.extractionPhase(item);

			// Phase 2: Parsing
			await this.parsingPhase(item);

			// Phase 3: Tandoor Upload (if enabled)
			await this.uploadPhase(item);

			// Success
			queueManager.updateStatus(item.id, 'success');
			console.log(`[QueueProcessor] ✓ Success: ${item.id}`);

			// Send push notification
			await this.sendPushNotification(item, 'success');
		} catch (error) {
			const errorMsg = error instanceof Error ? error.message : 'Unknown error';
			const recoverable = this.isRecoverableError(error);

			logError(`[QueueProcessor] ${recoverable ? 'Unhealthy' : 'Error'}: ${item.id}`, error);

			queueManager.updateStatus(item.id, recoverable ? 'unhealthy' : 'error', {
				error: {
					phase: item.currentPhase || 'extraction',
					message: errorMsg,
					recoverable,
					timestamp: new Date().toISOString()
				}
			});

			// Send push notification
			await this.sendPushNotification(item, recoverable ? 'unhealthy' : 'error');
		}
	}

	/**
	 * Phase 1: Extract text and thumbnail from Instagram
	 *
	 * Uses browser automation to load Instagram post and extract:
	 * - Recipe text (from caption, comments, etc.)
	 * - Thumbnail image (from meta tags or screenshot)
	 *
	 * Progress events are captured and added to queue item.
	 *
	 * @param item - Queue item being processed
	 * @throws Error if extraction fails
	 */
	private async extractionPhase(item: QueueItem): Promise<void> {
		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'extraction'
		});

		const progressCallback = (event: ProgressEvent) => {
			queueManager.addProgressEvent(item.id, event);
		};

		console.log(`[QueueProcessor] Extracting: ${item.url}`);
		const extracted = await extractTextAndThumbnail(item.url, progressCallback);

		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'extraction',
			extractedText: extracted.bodyText,
			thumbnail: extracted.thumbnail
		});

		console.log(`[QueueProcessor] ✓ Extraction complete: ${item.id}`);
	}

	/**
	 * Phase 2: Parse recipe from extracted text
	 *
	 * Uses LLM to extract structured recipe data:
	 * - Recipe name
	 * - Ingredients with amounts and units
	 * - Instructions/steps
	 * - Servings, times, etc.
	 *
	 * Enriches recipe with metadata (URL, thumbnail).
	 *
	 * @param item - Queue item being processed
	 * @throws Error if parsing fails or no recipe found
	 */
	private async parsingPhase(item: QueueItem): Promise<void> {
		if (!item.extractedText) {
			throw new Error('No extracted text available for parsing');
		}

		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'parsing'
		});

		queueManager.addProgressEvent(item.id, {
			type: 'status',
			message: 'Parsing recipe with LLM...',
			timestamp: new Date().toISOString()
		});

		console.log(`[QueueProcessor] Parsing recipe: ${item.id}`);
		const recipe = await extractRecipe(item.extractedText, (event) => {
			queueManager.addProgressEvent(item.id, event);
		});

		if (!recipe) {
			throw new Error('Failed to parse recipe from extracted text');
		}

		// Enrich recipe with metadata
		if (recipe.description) {
			recipe.description += `\n\nLink: ${item.url}`;
		} else {
			recipe.description = `Link: ${item.url}`;
		}

		if (item.thumbnail) {
			recipe.image = item.thumbnail;
		}

		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'parsing',
			recipe
		});

		console.log(`[QueueProcessor] ✓ Parsing complete: ${item.id} - ${recipe.name}`);
	}

	/**
	 * Phase 3: Upload to Tandoor (automatic)
	 *
	 * If Tandoor is configured (TANDOOR_TOKEN env var set):
	 * - Uploads recipe with ingredients and steps
	 * - Attempts to upload thumbnail/image
	 * - Image upload failure is non-fatal (logged but doesn't fail item)
	 *
	 * If Tandoor not configured: skips silently
	 *
	 * @param item - Queue item being processed
	 * @throws Error if Tandoor upload fails
	 */
	private async uploadPhase(item: QueueItem): Promise<void> {
		// Check if Tandoor is enabled
		if (!queueConfig.tandoor.enabled) {
			// Skip if Tandoor not configured
			queueManager.addProgressEvent(item.id, {
				type: 'status',
				message: 'Tandoor not configured, skipping upload',
				timestamp: new Date().toISOString()
			});
			console.log(`[QueueProcessor] Tandoor not configured, skipping: ${item.id}`);
			return;
		}

		if (!item.recipe) {
			throw new Error('No recipe available for upload');
		}

		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'uploading'
		});

		queueManager.addProgressEvent(item.id, {
			type: 'status',
			message: 'Uploading recipe to Tandoor...',
			timestamp: new Date().toISOString()
		});

		console.log(`[QueueProcessor] Uploading to Tandoor: ${item.id}`);

		// Upload recipe
		const result = await uploadRecipeWithIngredientsDTO(item.recipe);

		if (!result.success) {
			throw new Error(`Tandoor upload failed: ${result.error}`);
		}

		queueManager.updateStatus(item.id, 'in_progress', {
			phase: 'uploading',
			tandoorRecipeId: result.recipeId
		});

		console.log(`[QueueProcessor] ✓ Recipe uploaded: ${item.id} → Tandoor #${result.recipeId}`);

		// Upload image if available
		if (result.recipeId && result.imageUrl) {
			queueManager.addProgressEvent(item.id, {
				type: 'status',
				message: 'Uploading recipe image to Tandoor...',
				timestamp: new Date().toISOString()
			});

			const imageResult = await uploadRecipeImage(result.recipeId, result.imageUrl);

			if (!imageResult.success) {
				// Image upload failure is recoverable - log but don't fail
				console.warn(`[QueueProcessor] Image upload failed for ${item.id}: ${imageResult.error}`);
				queueManager.addProgressEvent(item.id, {
					type: 'status',
					message: `Image upload failed: ${imageResult.error}`,
					timestamp: new Date().toISOString()
				});
			} else {
				console.log(`[QueueProcessor] ✓ Image uploaded: ${item.id}`);
			}
		}

		queueManager.addProgressEvent(item.id, {
			type: 'status',
			message: 'Tandoor upload completed',
			timestamp: new Date().toISOString()
		});
	}

	/**
	 * Determine if error is recoverable
	 *
	 * Recoverable errors (unhealthy):
	 * - Network timeouts
	 * - Connection failures
	 * - Image upload failures
	 * - Thumbnail extraction failures
	 *
	 * Non-recoverable errors (error):
	 * - Invalid URL format
	 * - Authentication failures
	 * - Parsing failures (no recipe found)
	 *
	 * @param error - Error to classify
	 * @returns true if error is recoverable, false otherwise
	 */
	private isRecoverableError(error: unknown): boolean {
		if (!(error instanceof Error)) return false;

		const message = error.message.toLowerCase();

		// Recoverable errors
		const recoverablePatterns = [
			'timeout',
			'network',
			'econnrefused',
			'enotfound',
			'image upload failed',
			'thumbnail',
			'etimeout',
			'fetch failed'
		];

		return recoverablePatterns.some((pattern) => message.includes(pattern));
	}

	/**
	 * Send Web Push notification for queue item completion
	 *
	 * Sends appropriate notification based on processing status:
	 * - success: Recipe extraction complete with details
	 * - error/unhealthy: Extraction failed with retry option
	 *
	 * @param item - Queue item that completed
	 * @param status - Completion status (success, unhealthy, error)
	 */
	private async sendPushNotification(
		item: QueueItem,
		status: 'success' | 'unhealthy' | 'error'
	): Promise<void> {
		try {
			switch (status) {
				case 'success':
					await pushNotificationService.notifySuccess(
						item.id,
						item.results?.recipe?.name,
						item.results?.tandoorUrl
					);
					break;

				case 'error':
				case 'unhealthy':
					const errorMessage = item.error?.message || 'Processing failed';
					await pushNotificationService.notifyError(item.id, errorMessage);
					break;

				default:
					console.warn(`[QueueProcessor] Unknown status for push notification: ${status}`);
			}
		} catch (error) {
			logError('[QueueProcessor] Failed to send push notification', error);
			// Don't let notification failures break processing
		}
	}
}

/**
 * Singleton instance of QueueProcessor
 *
 * Auto-starts on module import to begin processing queue.
 */
export const queueProcessor = new QueueProcessor();

// Auto-start processor
queueProcessor.start();
