/**
 * Job Queue Implementation
 * Database-backed async job processing with configurable concurrency
 */

import { v4 as uuidv4 } from 'uuid';
import { prisma } from '@/lib/db';
import { Job, JobHandler, JobPriority, JobStatus, QueueConfig, JobQueueStats } from './types';
import { VOCAB } from '@/lib/white-label/constants';

const DEFAULT_CONFIG: QueueConfig = {
  name: 'default',
  concurrency: 3,
  pollInterval: 1000,
  defaultMaxAttempts: 3,
  retryDelay: (attempts) => Math.min(1000 * Math.pow(2, attempts), 60000), // Exponential backoff, max 1 min
};

export class JobQueue {
  private config: QueueConfig;
  private handlers: Map<string, JobHandler> = new Map();
  private processing: Set<string> = new Set();
  private isRunning = false;
  private pollTimer: NodeJS.Timeout | null = null;

  constructor(config: Partial<QueueConfig> = {}) {
    this.config = { ...DEFAULT_CONFIG, ...config };
  }

  /**
   * Register a job handler for a specific job type
   */
  registerHandler<T, R>(handler: JobHandler<T, R>): void {
    this.handlers.set(handler.type, handler as JobHandler);
    console.log(`[JobQueue:${this.config.name}] Registered handler for: ${handler.type}`);
  }

  /**
   * Add a job to the queue
   */
  async enqueue<T>(
    type: string,
    payload: T,
    options: {
      priority?: JobPriority;
      processAfter?: Date;
      maxAttempts?: number;
      metadata?: Record<string, unknown>;
    } = {}
  ): Promise<string> {
    const jobId = `job-${uuidv4()}`;
    const handler = this.handlers.get(type);
    
    await prisma.queuedJob.create({
      data: {
        id: jobId,
        type,
        payload: JSON.parse(JSON.stringify(payload)),
        priority: this.priorityToNumber(options.priority || 'normal'),
        status: 'pending',
        attempts: 0,
        maxAttempts: options.maxAttempts || handler?.maxAttempts || this.config.defaultMaxAttempts,
        processAfter: options.processAfter,
        metadata: options.metadata ? JSON.parse(JSON.stringify(options.metadata)) : undefined,
      },
    });

    console.log(`[JobQueue:${this.config.name}] Enqueued job ${jobId} (${type})`);
    return jobId;
  }

  /**
   * Start processing jobs
   */
  start(): void {
    if (this.isRunning) return;
    this.isRunning = true;
    console.log(`[JobQueue:${this.config.name}] Starting with concurrency ${this.config.concurrency}`);
    this.poll();
  }

  /**
   * Stop processing jobs
   */
  stop(): void {
    this.isRunning = false;
    if (this.pollTimer) {
      clearTimeout(this.pollTimer);
      this.pollTimer = null;
    }
    console.log(`[JobQueue:${this.config.name}] Stopped`);
  }

  /**
   * Process a single batch of jobs (for API-triggered processing)
   */
  async processBatch(): Promise<number> {
    const availableSlots = this.config.concurrency - this.processing.size;
    if (availableSlots <= 0) return 0;

    const jobs = await this.fetchPendingJobs(availableSlots);
    
    await Promise.all(jobs.map(job => this.processJob(job)));
    
    return jobs.length;
  }

  /**
   * Get job by ID
   */
  async getJob(jobId: string): Promise<Job | null> {
    const job = await prisma.queuedJob.findUnique({ where: { id: jobId } });
    return job ? this.mapToJob(job) : null;
  }

  /**
   * Cancel a pending job
   */
  async cancel(jobId: string): Promise<boolean> {
    const result = await prisma.queuedJob.updateMany({
      where: { id: jobId, status: 'pending' },
      data: { status: 'cancelled' },
    });
    return result.count > 0;
  }

  /**
   * Get queue statistics
   */
  async getStats(): Promise<JobQueueStats> {
    const [pending, processing, completed, failed, avgTime] = await Promise.all([
      prisma.queuedJob.count({ where: { status: 'pending' } }),
      prisma.queuedJob.count({ where: { status: 'processing' } }),
      prisma.queuedJob.count({ where: { status: 'completed' } }),
      prisma.queuedJob.count({ where: { status: 'failed' } }),
      prisma.$queryRaw<[{ avg: number }]>`
        SELECT AVG(EXTRACT(EPOCH FROM ("completedAt" - "startedAt"))) as avg
        FROM "QueuedJob" 
        WHERE status = 'completed' AND "completedAt" IS NOT NULL
      `.then(r => r[0]?.avg || 0).catch(() => 0),
    ]);

    return { pending, processing, completed, failed, avgProcessingTime: avgTime };
  }

  // Private methods

  private async poll(): Promise<void> {
    if (!this.isRunning) return;

    try {
      await this.processBatch();
    } catch (error) {
      console.error(`[JobQueue:${this.config.name}] Poll error:`, error);
    }

    this.pollTimer = setTimeout(() => this.poll(), this.config.pollInterval);
  }

  private async fetchPendingJobs(limit: number): Promise<Job[]> {
    const now = new Date();
    
    const jobs = await prisma.queuedJob.findMany({
      where: {
        status: 'pending',
        OR: [
          { processAfter: null },
          { processAfter: { lte: now } },
        ],
      },
      orderBy: [
        { priority: 'desc' },
        { createdAt: 'asc' },
      ],
      take: limit,
    });

    // Mark as processing atomically
    const jobIds = jobs.map(j => j.id);
    if (jobIds.length > 0) {
      await prisma.queuedJob.updateMany({
        where: { id: { in: jobIds }, status: 'pending' },
        data: { status: 'processing', startedAt: now },
      });
    }

    return jobs.map(j => this.mapToJob(j));
  }

  private async processJob(job: Job): Promise<void> {
    this.processing.add(job.id);
    const handler = this.handlers.get(job.type);

    if (!handler) {
      console.error(`[JobQueue:${this.config.name}] No handler for job type: ${job.type}`);
      await this.failJob(job.id, new Error(`No handler for job type: ${job.type}`));
      this.processing.delete(job.id);
      return;
    }

    try {
      console.log(`[JobQueue:${this.config.name}] Processing job ${job.id} (${job.type}) attempt ${job.attempts + 1}`);
      
      // Execute with optional timeout
      const result = handler.timeout
        ? await Promise.race([
            handler.process(job.payload, job),
            new Promise((_, reject) => 
              setTimeout(() => reject(new Error('Job timeout')), handler.timeout)
            ),
          ])
        : await handler.process(job.payload, job);

      await this.completeJob(job.id, result);
      
      if (handler.onComplete) {
        await handler.onComplete(job, result);
      }

      console.log(`[JobQueue:${this.config.name}] Completed job ${job.id}`);
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      console.error(`[JobQueue:${this.config.name}] Job ${job.id} failed:`, err.message);
      
      const newAttempts = job.attempts + 1;
      if (newAttempts < job.maxAttempts) {
        // Retry with exponential backoff
        const retryDelay = this.config.retryDelay(newAttempts);
        await this.retryJob(job.id, newAttempts, retryDelay);
        console.log(`[JobQueue:${this.config.name}] Retrying job ${job.id} in ${retryDelay}ms`);
      } else {
        await this.failJob(job.id, err);
        if (handler.onFail) {
          await handler.onFail(job, err);
        }
      }
    } finally {
      this.processing.delete(job.id);
    }
  }

  private async completeJob(jobId: string, result: unknown): Promise<void> {
    await prisma.queuedJob.update({
      where: { id: jobId },
      data: {
        status: 'completed',
        completedAt: new Date(),
        result: result ? JSON.parse(JSON.stringify(result)) : null,
      },
    });
  }

  private async failJob(jobId: string, error: Error): Promise<void> {
    await prisma.queuedJob.update({
      where: { id: jobId },
      data: {
        status: 'failed',
        completedAt: new Date(),
        error: error.message,
      },
    });
  }

  private async retryJob(jobId: string, attempts: number, delayMs: number): Promise<void> {
    await prisma.queuedJob.update({
      where: { id: jobId },
      data: {
        status: 'pending',
        attempts,
        processAfter: new Date(Date.now() + delayMs),
      },
    });
  }

  private priorityToNumber(priority: JobPriority): number {
    switch (priority) {
      case 'critical': return 100;
      case 'high': return 75;
      case 'normal': return 50;
      case 'low': return 25;
    }
  }

  private mapToJob(dbJob: any): Job {
    return {
      id: dbJob.id,
      type: dbJob.type,
      payload: dbJob.payload,
      priority: this.numberToPriority(dbJob.priority),
      status: dbJob.status as JobStatus,
      attempts: dbJob.attempts,
      maxAttempts: dbJob.maxAttempts,
      createdAt: dbJob.createdAt,
      startedAt: dbJob.startedAt,
      completedAt: dbJob.completedAt,
      processAfter: dbJob.processAfter,
      result: dbJob.result,
      error: dbJob.error,
      metadata: dbJob.metadata,
    };
  }

  private numberToPriority(num: number): JobPriority {
    if (num >= 100) return 'critical';
    if (num >= 75) return 'high';
    if (num >= 50) return 'normal';
    return 'low';
  }
}

// Singleton instance for the ${VOCAB.factory} queue
export const darkFactoryQueue = new JobQueue({
  name: 'dark-factory',
  concurrency: 2,  // Limit concurrent code generation tasks
  pollInterval: 2000,
  defaultMaxAttempts: 3,
});
