import { randomUUID } from 'crypto';
import { redis, redisConfigured } from '@/lib/redis';
import { logger } from '@/lib/logger';

export type JobType = 'demo_reminder' | 'trial_reminder' | 'email' | 'web_vitals_alert' | 'drift_check' | 'deposit_reminder' | 'lead_nurture' | 'lead_score_decay' | 'quotation_reminder' | 'quotation_recurring_gen';

export type JobPriority = 'high' | 'normal' | 'low';

export interface Job<T = Record<string, unknown>> {
  id: string;
  type: JobType;
  payload: T;
  attempts: number;
  maxAttempts: number;
  createdAt: string;
  nextRunAt?: string;
  priority?: JobPriority;
  error?: string;
  lastError?: string;
  completedAt?: string;
}

const READY_KEY = 'jobs:ready';
const READY_HIGH_KEY = 'jobs:ready:high';
const DELAYED_KEY = 'jobs:delayed';
const DLQ_KEY = 'jobs:dlq';
const PROCESSED_KEY = 'jobs:processed';

const MAX_ATTEMPTS_DEFAULT = 4;
const BASE_BACKOFF_MS = 30_000; // 30s
const MAX_BACKOFF_MS = 10 * 60_000; // 10m

function backoffDelay(attempt: number): number {
  const delay = BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
  return Math.min(delay, MAX_BACKOFF_MS);
}

export async function enqueueJob<T>(
  type: JobType,
  payload: T,
  opts?: { maxAttempts?: number; delayMs?: number; priority?: JobPriority }
): Promise<string> {
  if (!redisConfigured) {
    throw new Error('Redis is not configured for job queueing');
  }

  const priority = opts?.priority ?? 'normal';
  const job: Job<T> = {
    id: randomUUID(),
    type,
    payload,
    attempts: 0,
    maxAttempts: opts?.maxAttempts ?? MAX_ATTEMPTS_DEFAULT,
    createdAt: new Date().toISOString(),
    priority,
  };

  if (opts?.delayMs && opts.delayMs > 0) {
    await redis.zadd(DELAYED_KEY, Date.now() + opts.delayMs, JSON.stringify(job));
  } else {
    // High priority jobs go to a separate queue that's processed first
    const queueKey = priority === 'high' ? READY_HIGH_KEY : READY_KEY;
    await redis.lpush(queueKey, JSON.stringify(job));
  }

  logger.debug('Job enqueued', { jobId: job.id, type, priority });
  return job.id;
}

async function promoteDelayed(now: number, limit = 50): Promise<void> {
  if (!redisConfigured) return;
  const due = await redis.zrangebyscore(DELAYED_KEY, 0, now, { offset: 0, count: limit });
  for (const member of due) {
    await redis.zrem(DELAYED_KEY, member);
    await redis.lpush(READY_KEY, member);
  }
}

export async function processJobs(
  handler: (job: Job) => Promise<void>,
  opts?: { maxJobs?: number }
): Promise<{ processed: number; succeeded: number; failed: number; requeued: number; dlq: number }> {
  if (!redisConfigured) {
    throw new Error('Redis is not configured for job queueing');
  }

  const maxJobs = opts?.maxJobs ?? 50;
  let processed = 0;
  let succeeded = 0;
  let failed = 0;
  let requeued = 0;
  let dlq = 0;

  await promoteDelayed(Date.now(), maxJobs);

  while (processed < maxJobs) {
    // Process high priority jobs first
    let raw = await redis.rpop(READY_HIGH_KEY);
    if (!raw) {
      raw = await redis.rpop(READY_KEY);
    }
    if (!raw) break;

    processed++;
    let job: Job;

    try {
      job = JSON.parse(raw) as Job;
    } catch (parseError) {
      logger.error('Job parse failed, discarding', parseError);
      continue;
    }

    const startTime = Date.now();

    try {
      await handler(job);
      succeeded++;

      // Track successful job completion (keep last 100 for stats)
      const completedJob = { ...job, completedAt: new Date().toISOString() };
      await redis.lpush(PROCESSED_KEY, JSON.stringify(completedJob));
      await redis.ltrim(PROCESSED_KEY, 0, 99);

      logger.debug('Job completed', { jobId: job.id, type: job.type, durationMs: Date.now() - startTime });
    } catch (error) {
      failed++;
      const nextAttempt = job.attempts + 1;
      const errorMessage = error instanceof Error ? error.message : String(error);

      if (nextAttempt >= job.maxAttempts) {
        dlq++;
        const dlqJob = { ...job, attempts: nextAttempt, error: errorMessage, lastError: errorMessage };
        await redis.lpush(DLQ_KEY, JSON.stringify(dlqJob));
        logger.error('Job moved to DLQ', { jobId: job.id, type: job.type, error: errorMessage });
      } else {
        requeued++;
        const delay = backoffDelay(nextAttempt);
        const requeuedJob = { ...job, attempts: nextAttempt, lastError: errorMessage };
        await redis.zadd(DELAYED_KEY, Date.now() + delay, JSON.stringify(requeuedJob));
        logger.warn('Job requeued with backoff', { jobId: job.id, type: job.type, attempt: nextAttempt, delayMs: delay, error: errorMessage });
      }
    }
  }

  return { processed, succeeded, failed, requeued, dlq };
}

export interface QueueDepthResult {
  ready: number;
  readyHigh: number;
  delayed: number;
  dlq: number;
  processed: number;
}

export async function queueDepth(): Promise<QueueDepthResult> {
  if (!redisConfigured) {
    return { ready: 0, readyHigh: 0, delayed: 0, dlq: 0, processed: 0 };
  }

  const [ready, readyHigh, delayed, dead, processed] = await Promise.all([
    redis.llen(READY_KEY).catch(() => 0),
    redis.llen(READY_HIGH_KEY).catch(() => 0),
    redis.zcard(DELAYED_KEY).catch(() => 0),
    redis.llen(DLQ_KEY).catch(() => 0),
    redis.llen(PROCESSED_KEY).catch(() => 0),
  ]);

  return { ready, readyHigh, delayed, dlq: dead, processed };
}

/**
 * Get jobs from the Dead Letter Queue
 */
export async function getDLQJobs(limit = 50): Promise<Job[]> {
  if (!redisConfigured) return [];

  const raw = await redis.lrange(DLQ_KEY, 0, limit - 1);
  return raw.map(item => {
    try {
      return JSON.parse(item) as Job;
    } catch {
      return null;
    }
  }).filter((job): job is Job => job !== null);
}

/**
 * Retry a specific job from the DLQ
 */
export async function retryDLQJob(jobId: string): Promise<boolean> {
  if (!redisConfigured) return false;

  const raw = await redis.lrange(DLQ_KEY, 0, -1);
  for (const item of raw) {
    try {
      const job = JSON.parse(item) as Job;
      if (job.id === jobId) {
        // Remove from DLQ
        await redis.lrem(DLQ_KEY, 1, item);
        // Reset attempts and re-enqueue
        const retriedJob = { ...job, attempts: 0, lastError: undefined, error: undefined };
        await redis.lpush(READY_KEY, JSON.stringify(retriedJob));
        logger.info('DLQ job retried', { jobId });
        return true;
      }
    } catch {
      continue;
    }
  }
  return false;
}

/**
 * Remove a job from the DLQ
 */
export async function removeDLQJob(jobId: string): Promise<boolean> {
  if (!redisConfigured) return false;

  const raw = await redis.lrange(DLQ_KEY, 0, -1);
  for (const item of raw) {
    try {
      const job = JSON.parse(item) as Job;
      if (job.id === jobId) {
        await redis.lrem(DLQ_KEY, 1, item);
        logger.info('DLQ job removed', { jobId });
        return true;
      }
    } catch {
      continue;
    }
  }
  return false;
}

/**
 * Clear all jobs from the DLQ
 */
export async function clearDLQ(): Promise<number> {
  if (!redisConfigured) return 0;

  const count = await redis.llen(DLQ_KEY);
  await redis.del(DLQ_KEY);
  logger.warn('DLQ cleared', { count });
  return count;
}

/**
 * Get recent processed jobs for stats
 */
export async function getRecentProcessedJobs(limit = 20): Promise<Job[]> {
  if (!redisConfigured) return [];

  const raw = await redis.lrange(PROCESSED_KEY, 0, limit - 1);
  return raw.map(item => {
    try {
      return JSON.parse(item) as Job;
    } catch {
      return null;
    }
  }).filter((job): job is Job => job !== null);
}
