import { EventEmitter } from 'events';
import path from 'path';
import { logger, StructuredLogger } from '../utils/logger';
import { config } from '../config/env';
import { ProcessingOptions } from './documentProcessingService';
import { unifiedDocumentProcessor } from './unifiedDocumentProcessor';
import { uploadMonitoringService } from './uploadMonitoringService';

export interface Job {
  id: string;
  type: 'document_processing';
  data: {
    documentId: string;
    userId: string;
    options?: ProcessingOptions;
  };
  status: 'pending' | 'processing' | 'completed' | 'failed' | 'retrying';
  priority: number;
  attempts: number;
  maxAttempts: number;
  createdAt: Date;
  startedAt?: Date;
  completedAt?: Date;
  error?: string;
  result?: any;
}

export interface JobQueueConfig {
  maxConcurrentJobs: number;
  defaultMaxAttempts: number;
  retryDelayMs: number;
  maxRetryDelayMs: number;
  cleanupIntervalMs: number;
  maxJobAgeMs: number;
}

class JobQueueService extends EventEmitter {
  private queue: Job[] = [];
  private processing: Job[] = [];
  private config: JobQueueConfig;
  private isRunning = false;
  private cleanupInterval: any = null;

  constructor(config: Partial<JobQueueConfig> = {}) {
    super();
    this.config = {
      maxConcurrentJobs: 3,
      defaultMaxAttempts: 3,
      retryDelayMs: 5000,
      maxRetryDelayMs: 300000, // 5 minutes
      cleanupIntervalMs: 300000, // 5 minutes
      maxJobAgeMs: 24 * 60 * 60 * 1000, // 24 hours
      ...config,
    };

    this.startCleanupInterval();
  }

  /**
   * Add a job to the queue
   */
  async addJob(
    type: 'document_processing',
    data: { documentId: string; userId: string; options?: ProcessingOptions },
    priority: number = 0,
    maxAttempts?: number
  ): Promise<string> {
    const jobId = `job_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    const structuredLogger = new StructuredLogger();
    
    const job: Job = {
      id: jobId,
      type,
      data,
      status: 'pending',
      priority,
      attempts: 0,
      maxAttempts: maxAttempts || this.config.defaultMaxAttempts,
      createdAt: new Date(),
    };

    this.queue.push(job);
    this.queue.sort((a, b) => b.priority - a.priority); // Higher priority first

    // Track job queue operation
    uploadMonitoringService.trackUploadEvent({
      userId: data.userId,
      fileInfo: {
        originalName: `document_${data.documentId}`,
        size: 0,
        mimetype: 'application/octet-stream',
      },
      status: 'started',
      stage: 'job_queued',
      correlationId: jobId,
    });

    structuredLogger.jobQueueOperation('add_job', jobId, 'pending');

    logger.info(`Job added to queue: ${jobId}`, {
      type,
      documentId: data.documentId,
      userId: data.userId,
      priority,
      queueLength: this.queue.length,
    });

    this.emit('job:added', job);
    this.processNextJob();

    return jobId;
  }

  /**
   * Process the next job in the queue
   */
  private async processNextJob(): Promise<void> {
    if (!this.isRunning || this.processing.length >= this.config.maxConcurrentJobs) {
      return;
    }

    const job = this.queue.shift();
    if (!job) {
      return;
    }

    this.processing.push(job);
    job.status = 'processing';
    job.startedAt = new Date();
    job.attempts++;

    logger.info(`Starting job processing: ${job.id}`, {
      type: job.type,
      attempts: job.attempts,
      processingCount: this.processing.length,
    });

    this.emit('job:started', job);

    try {
      const result = await this.executeJob(job);
      
      job.status = 'completed';
      job.completedAt = new Date();
      job.result = result;

      const processingTime = job.completedAt.getTime() - job.startedAt!.getTime();

      // Track job completion
      uploadMonitoringService.trackUploadEvent({
        userId: job.data.userId,
        fileInfo: {
          originalName: `document_${job.data.documentId}`,
          size: 0,
          mimetype: 'application/octet-stream',
        },
        status: 'success',
        stage: 'job_completed',
        processingTime,
        correlationId: job.id,
      });

      const structuredLogger = new StructuredLogger();
      structuredLogger.jobQueueOperation('job_completed', job.id, 'completed');

      logger.info(`Job completed successfully: ${job.id}`, {
        processingTime,
      });

      this.emit('job:completed', job);
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : 'Unknown error';
      
      job.error = errorMessage;
      job.status = 'failed';

      const processingTime = job.startedAt ? Date.now() - job.startedAt.getTime() : 0;

      // Track job failure
      uploadMonitoringService.trackUploadEvent({
        userId: job.data.userId,
        fileInfo: {
          originalName: `document_${job.data.documentId}`,
          size: 0,
          mimetype: 'application/octet-stream',
        },
        status: 'failed',
        stage: 'job_failed',
        error: {
          message: errorMessage,
          type: 'job_processing_error',
        },
        processingTime,
        correlationId: job.id,
      });

      const structuredLogger = new StructuredLogger();
      structuredLogger.jobQueueOperation('job_failed', job.id, 'failed', error);

      logger.error(`Job failed: ${job.id}`, {
        error: errorMessage,
        attempts: job.attempts,
        maxAttempts: job.maxAttempts,
      });

      this.emit('job:failed', job);

      // Retry logic
      if (job.attempts < job.maxAttempts) {
        await this.retryJob(job);
      } else {
        logger.error(`Job exceeded max attempts: ${job.id}`, {
          attempts: job.attempts,
          maxAttempts: job.maxAttempts,
        });
        this.emit('job:max_attempts_exceeded', job);
      }
    } finally {
      // Remove from processing array
      const index = this.processing.findIndex(j => j.id === job.id);
      if (index !== -1) {
        this.processing.splice(index, 1);
      }

      // Process next job
      setImmediate(() => this.processNextJob());
    }
  }

  /**
   * Execute a specific job
   */
  private async executeJob(job: Job): Promise<any> {
    // Add timeout handling to prevent stuck jobs
    const timeoutMs = 15 * 60 * 1000; // 15 minutes timeout
    
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(`Job ${job.id} timed out after ${timeoutMs / 1000 / 60} minutes`));
      }, timeoutMs);
    });

    const jobPromise = (async () => {
      switch (job.type) {
        case 'document_processing':
          return await this.processDocumentJob(job);
        default:
          throw new Error(`Unknown job type: ${job.type}`);
      }
    })();

    try {
      return await Promise.race([jobPromise, timeoutPromise]);
    } catch (error) {
      logger.error(`Job ${job.id} failed or timed out`, {
        jobId: job.id,
        error: error instanceof Error ? error.message : 'Unknown error'
      });
      throw error;
    }
  }

  /**
   * Process a document processing job
   */
  private async processDocumentJob(job: Job): Promise<any> {
    const { documentId, userId, options } = job.data;
    
    // Update job status in database
    await this.updateJobStatus(job.id, 'processing');

    // Use unified processor for strategy-aware processing
    const strategy = options?.strategy || config.processingStrategy;
    logger.info('Processing document job with strategy', { documentId, strategy, jobId: job.id, configStrategy: config.processingStrategy });

    try {
      const result = await unifiedDocumentProcessor.processDocument(
        documentId,
        userId,
        '', // text will be extracted by the processor
        { strategy, ...options }
      );

      // Update document with processing results
      const { DocumentModel } = await import('../models/DocumentModel');
      const updateData: any = {
        status: 'completed',
        processing_completed_at: new Date().toISOString()
      };

      // Save analysis data if available
      if (result.analysisData) {
        updateData.analysis_data = result.analysisData;
      }

      // Save generated summary if available
      if (result.summary) {
        updateData.generated_summary = result.summary;
      }

      // Generate PDF from the summary if available
      if (result.summary) {
        try {
          const { pdfGenerationService } = await import('./pdfGenerationService');
          const timestamp = Date.now();
          const pdfPath = `uploads/summaries/${documentId}_${timestamp}.pdf`;
          const fullPdfPath = path.join(process.cwd(), pdfPath);
          
          const pdfGenerated = await pdfGenerationService.generatePDFFromMarkdown(
            result.summary,
            fullPdfPath
          );
          
          if (pdfGenerated) {
            updateData.summary_pdf_path = pdfPath;
            logger.info(`PDF generated successfully for document: ${documentId}`, { pdfPath });
          } else {
            logger.warn(`Failed to generate PDF for document: ${documentId}`);
          }
        } catch (error) {
          logger.error(`Error generating PDF for document: ${documentId}`, { error });
        }
      }

      await DocumentModel.updateById(documentId, updateData);

      logger.info(`Document ${documentId} processing completed successfully`, { 
        jobId: job.id, 
        processingTime: result.processingTime,
        strategy: result.processingStrategy 
      });

      // Update job status in database
      await this.updateJobStatus(job.id, 'completed');

      return result;
    } catch (error) {
      // Update document status to failed
      const { DocumentModel } = await import('../models/DocumentModel');
      await DocumentModel.updateById(documentId, {
        status: 'failed',
        error_message: error instanceof Error ? error.message : 'Processing failed'
      });

      logger.error(`Document ${documentId} processing failed`, { 
        jobId: job.id, 
        error: error instanceof Error ? error.message : 'Unknown error' 
      });

      // Update job status to failed
      await this.updateJobStatus(job.id, 'failed');

      throw error;
    }
  }

  /**
   * Retry a failed job
   */
  private async retryJob(job: Job): Promise<void> {
    const delay = Math.min(
      this.config.retryDelayMs * Math.pow(2, job.attempts - 1),
      this.config.maxRetryDelayMs
    );

    job.status = 'retrying';

    logger.info(`Scheduling job retry: ${job.id}`, {
      delay,
      attempts: job.attempts,
      maxAttempts: job.maxAttempts,
    });

    this.emit('job:retrying', job);

    setTimeout(() => {
      job.status = 'pending';
      this.queue.push(job);
      this.queue.sort((a, b) => b.priority - a.priority);
      this.processNextJob();
    }, delay);
  }

  /**
   * Get job status
   */
  getJobStatus(jobId: string): Job | null {
    // Check processing jobs
    const processingJob = this.processing.find(j => j.id === jobId);
    if (processingJob) {
      return processingJob;
    }

    // Check queued jobs
    const queuedJob = this.queue.find(j => j.id === jobId);
    if (queuedJob) {
      return queuedJob;
    }

    return null;
  }

  /**
   * Get all jobs
   */
  getAllJobs(): { queue: Job[]; processing: Job[] } {
    return {
      queue: [...this.queue],
      processing: [...this.processing],
    };
  }

  /**
   * Clear stuck jobs that have been processing for too long
   */
  clearStuckJobs(): number {
    const stuckThreshold = 20 * 60 * 1000; // 20 minutes
    const now = new Date();
    let clearedCount = 0;

    this.processing = this.processing.filter(job => {
      if (job.startedAt && (now.getTime() - job.startedAt.getTime()) > stuckThreshold) {
        logger.warn(`Clearing stuck job: ${job.id}`, {
          jobId: job.id,
          startedAt: job.startedAt,
          processingTime: now.getTime() - job.startedAt.getTime()
        });
        clearedCount++;
        return false;
      }
      return true;
    });

    return clearedCount;
  }

  /**
   * Get queue statistics
   */
  getQueueStats(): {
    queueLength: number;
    processingCount: number;
    totalJobs: number;
    completedJobs: number;
    failedJobs: number;
  } {
    return {
      queueLength: this.queue.length,
      processingCount: this.processing.length,
      totalJobs: this.queue.length + this.processing.length,
      completedJobs: 0, // TODO: Track completed jobs
      failedJobs: 0, // TODO: Track failed jobs
    };
  }

  /**
   * Get queue statistics for a specific user
   */
  getUserQueueStats(userId?: string): {
    pending: number;
    processing: number;
    completed: number;
    failed: number;
  } {
    if (!userId) {
      return {
        pending: this.queue.length,
        processing: this.processing.length,
        completed: 0,
        failed: 0
      };
    }

    const userQueueJobs = this.queue.filter(job => job.data.userId === userId);
    const userProcessingJobs = this.processing.filter(job => job.data.userId === userId);

    return {
      pending: userQueueJobs.length,
      processing: userProcessingJobs.length,
      completed: 0, // TODO: Track completed jobs per user
      failed: 0 // TODO: Track failed jobs per user
    };
  }

  /**
   * Cancel a job
   */
  cancelJob(jobId: string): boolean {
    // Check processing jobs
    const processingIndex = this.processing.findIndex(j => j.id === jobId);
    if (processingIndex !== -1) {
      const job = this.processing[processingIndex];
      if (job) {
        job.status = 'failed';
        job.error = 'Job cancelled';
        this.processing.splice(processingIndex, 1);
      }
      
      logger.info(`Job cancelled: ${jobId}`);
      this.emit('job:cancelled', job);
      return true;
    }

    // Check queued jobs
    const queueIndex = this.queue.findIndex(j => j.id === jobId);
    if (queueIndex !== -1) {
      const job = this.queue[queueIndex];
      if (job) {
        job.status = 'failed';
        job.error = 'Job cancelled';
        this.queue.splice(queueIndex, 1);
      }
      
      logger.info(`Job cancelled: ${jobId}`);
      this.emit('job:cancelled', job);
      return true;
    }

    return false;
  }

  /**
   * Start the job queue
   */
  start(): void {
    if (this.isRunning) {
      return;
    }

    this.isRunning = true;
    logger.info('Job queue started', {
      maxConcurrentJobs: this.config.maxConcurrentJobs,
    });

    this.emit('queue:started');
    this.processNextJob();
  }



  /**
   * Pause the job queue
   */
  pause(): void {
    this.isRunning = false;
    logger.info('Job queue paused');
    this.emit('queue:paused');
  }

  /**
   * Resume the job queue
   */
  resume(): void {
    this.isRunning = true;
    logger.info('Job queue resumed');
    this.emit('queue:resumed');
    this.processNextJob();
  }

  /**
   * Clear the queue
   */
  clearQueue(): number {
    const count = this.queue.length;
    this.queue = [];
    
    logger.info(`Queue cleared: ${count} jobs removed`);
    this.emit('queue:cleared', count);
    
    return count;
  }

  /**
   * Start cleanup interval
   */
  private startCleanupInterval(): void {
    this.cleanupInterval = setInterval(() => {
      this.cleanupOldJobs();
    }, this.config.cleanupIntervalMs);
  }

  /**
   * Clean up old completed/failed jobs
   */
  private cleanupOldJobs(): void {
    const cutoffTime = Date.now() - this.config.maxJobAgeMs;
    let cleanedCount = 0;

    // Clear stuck jobs first
    const stuckJobsCleared = this.clearStuckJobs();
    cleanedCount += stuckJobsCleared;

    // Clean up processing jobs that are too old
    this.processing = this.processing.filter(job => {
      if (job.createdAt.getTime() < cutoffTime) {
        cleanedCount++;
        logger.info(`Cleaned up old processing job: ${job.id}`);
        return false;
      }
      return true;
    });

    // Clean up queued jobs that are too old
    this.queue = this.queue.filter(job => {
      if (job.createdAt.getTime() < cutoffTime) {
        cleanedCount++;
        logger.info(`Cleaned up old queued job: ${job.id}`);
        return false;
      }
      return true;
    });

    if (cleanedCount > 0) {
      logger.info(`Cleaned up ${cleanedCount} old/stuck jobs (${stuckJobsCleared} stuck)`);
      this.emit('queue:cleaned', cleanedCount);
    }
  }

  /**
   * Update job status in database
   */
  private async updateJobStatus(jobId: string, status: string): Promise<void> {
    // Note: Job queue service manages jobs in memory, database jobs are separate
    // This method is kept for potential future integration but currently disabled
    // to avoid warnings about missing job_id values in database
    logger.debug(`Job queue status update (in-memory): ${jobId} -> ${status}`);
  }

  /**
   * Stop the service and cleanup
   */
  stop(): void {
    this.isRunning = false;
    
    if (this.cleanupInterval) {
      clearInterval(this.cleanupInterval);
      this.cleanupInterval = null;
    }
    
    this.queue = [];
    this.processing = [];
    this.removeAllListeners();
    
    logger.info('Job queue service stopped');
  }
}

export const jobQueueService = new JobQueueService();
export default jobQueueService; 