import { Injectable, Logger } from '@nestjs/common';

/**
 * Distributed Redis Lock for cross-instance synchronization.
 * Uses Redis SET NX EX pattern (Redlock simplified for single-node dev).
 *
 * Use cases:
 * - Wallet operations across multiple user-service instances
 * - Coupon inventory reservation
 * - Scheduled job deduplication (only one instance runs cron)
 *
 * Production: upgrade to Redlock algorithm with multiple Redis masters.
 */
@Injectable()
export class RedisLockService {
  private readonly logger = new Logger('RedisLock');
  private redis: any; // ioredis instance, injected via module

  constructor() {}

  setRedis(redis: any) {
    this.redis = redis;
  }

  /**
   * Acquire a distributed lock.
   * @param key Lock key (e.g., 'wallet:lock:{userId}')
   * @param ttlMs Lock TTL in milliseconds (default 10s)
   * @param retries Number of acquisition retries (default 3)
   * @returns Lock token (pass to release()) or null if failed
   */
  async acquire(
    key: string,
    ttlMs = 10000,
    retries = 3,
  ): Promise<string | null> {
    const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;

    for (let i = 0; i < retries; i++) {
      const result = await this.redis.set(
        `lock:${key}`,
        token,
        'PX',
        ttlMs,
        'NX',
      );

      if (result === 'OK') {
        return token;
      }

      // Wait before retry: 50ms, 100ms, 200ms
      await new Promise((r) => setTimeout(r, 50 * Math.pow(2, i)));
    }

    this.logger.warn(`Failed to acquire lock: ${key} after ${retries} retries`);
    return null;
  }

  /**
   * Release a distributed lock.
   * Only releases if the token matches (prevents releasing another caller's lock).
   */
  async release(key: string, token: string): Promise<boolean> {
    // Lua script for atomic check-and-delete
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;

    const result = await this.redis.eval(script, 1, `lock:${key}`, token);
    return result === 1;
  }

  /**
   * Execute operation with distributed lock.
   * Automatically acquires and releases the lock.
   */
  async withLock<T>(
    key: string,
    operation: () => Promise<T>,
    ttlMs = 10000,
  ): Promise<T> {
    const token = await this.acquire(key, ttlMs);
    if (!token) {
      throw new Error(`Failed to acquire distributed lock: ${key}`);
    }

    try {
      return await operation();
    } finally {
      await this.release(key, token);
    }
  }
}
