import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Queue, Worker, type JobsOptions, type Processor } from 'bullmq';
import IORedis from 'ioredis';
import { getAppConfig } from '../config/env';

@Injectable()
export class RedisQueueService implements OnModuleDestroy {
  private readonly useMemoryStore = getAppConfig().redisUrl === 'memory';
  private readonly connection = this.useMemoryStore
    ? null
    : new IORedis(getAppConfig().redisUrl, {
        maxRetriesPerRequest: null,
      });
  private readonly queues = new Map<string, Queue>();
  private readonly workers: Worker[] = [];
  private readonly memoryCounters = new Map<string, { value: number; expiresAt: number }>();

  getQueue(name: string) {
    if (!this.connection) {
      return null;
    }

    const existing = this.queues.get(name);
    if (existing) {
      return existing;
    }

    const queue = new Queue(name, {
      connection: this.connection,
    });
    this.queues.set(name, queue);
    return queue;
  }

  async enqueue(name: string, jobName: string, data: Record<string, unknown>, options?: JobsOptions) {
    const queue = this.getQueue(name);
    if (!queue) {
      return;
    }

    await queue.add(jobName, data, options);
  }

  async getCounter(key: string) {
    if (!this.connection) {
      const item = this.memoryCounters.get(key);
      if (!item || item.expiresAt <= Date.now()) {
        this.memoryCounters.delete(key);
        return 0;
      }

      return item.value;
    }

    const value = await this.connection.get(key);
    return value ? Number(value) : 0;
  }

  async getTtlSeconds(key: string) {
    if (!this.connection) {
      const item = this.memoryCounters.get(key);
      if (!item || item.expiresAt <= Date.now()) {
        this.memoryCounters.delete(key);
        return 0;
      }

      return Math.ceil((item.expiresAt - Date.now()) / 1000);
    }

    const ttl = await this.connection.ttl(key);
    return ttl > 0 ? ttl : 0;
  }

  async incrementCounter(key: string, ttlSeconds: number) {
    if (!this.connection) {
      const now = Date.now();
      const existing = this.memoryCounters.get(key);
      const nextValue = existing && existing.expiresAt > now ? existing.value + 1 : 1;
      this.memoryCounters.set(key, {
        value: nextValue,
        expiresAt: existing && existing.expiresAt > now ? existing.expiresAt : now + ttlSeconds * 1000,
      });
      return nextValue;
    }

    const nextCount = await this.connection.incr(key);
    if (nextCount === 1) {
      await this.connection.expire(key, ttlSeconds);
    }

    return nextCount;
  }

  async deleteKey(key: string) {
    if (!this.connection) {
      this.memoryCounters.delete(key);
      return;
    }

    await this.connection.del(key);
  }

  createWorker(name: string, processor: Processor) {
    if (!this.connection) {
      return null;
    }

    const worker = new Worker(name, processor, {
      connection: this.connection,
      concurrency: 5,
    });
    this.workers.push(worker);
    return worker;
  }

  async onModuleDestroy() {
    await Promise.all(this.workers.map((worker) => worker.close()));
    await Promise.all(Array.from(this.queues.values()).map((queue) => queue.close()));
    await this.connection?.quit();
  }
}
