/**
 * Distributed Lock Service
 *
 * Redis-based distributed locking for MPC operations.
 * Prevents race conditions when multiple instances access shared resources.
 */

import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { v4 as uuidv4 } from 'uuid';

export interface LockOptions {
  ttl?: number;       // Lock TTL in milliseconds
  retryCount?: number;
  retryDelay?: number;
}

export interface Lock {
  key: string;
  token: string;
  release: () => Promise<boolean>;
}

@Injectable()
export class DistributedLockService implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(DistributedLockService.name);
  private redis: Redis;
  private readonly keyPrefix = 'mpc:lock:';
  private readonly defaultTTL = 30000; // 30 seconds
  private readonly defaultRetryCount = 3;
  private readonly defaultRetryDelay = 100;

  // Lua script for safe release (only release if token matches)
  private readonly releaseScript = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `;

  constructor(private readonly configService: ConfigService) {}

  async onModuleInit() {
    const host = this.configService.get<string>('REDIS_HOST') || 'localhost';
    const port = this.configService.get<number>('REDIS_PORT') || 6379;
    const password = this.configService.get<string>('REDIS_PASSWORD');
    const db = this.configService.get<number>('REDIS_DB') || 5;

    this.redis = new Redis({
      host,
      port,
      password,
      db,
    });

    this.redis.on('connect', () => {
      this.logger.log('Redis lock service connected');
    });
  }

  async onModuleDestroy() {
    await this.redis.quit();
  }

  /**
   * Acquire a distributed lock
   */
  async acquire(key: string, options?: LockOptions): Promise<Lock | null> {
    const fullKey = this.keyPrefix + key;
    const token = uuidv4();
    const ttl = options?.ttl || this.defaultTTL;
    const retryCount = options?.retryCount || this.defaultRetryCount;
    const retryDelay = options?.retryDelay || this.defaultRetryDelay;

    for (let attempt = 0; attempt < retryCount; attempt++) {
      // Try to acquire lock with NX (only if not exists) and PX (expire in milliseconds)
      const result = await this.redis.set(fullKey, token, 'PX', ttl, 'NX');

      if (result === 'OK') {
        this.logger.debug(`Lock acquired: ${key}`);

        return {
          key,
          token,
          release: () => this.release(key, token),
        };
      }

      // Wait before retry
      if (attempt < retryCount - 1) {
        await new Promise(resolve => setTimeout(resolve, retryDelay));
      }
    }

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

  /**
   * Release a lock
   */
  async release(key: string, token: string): Promise<boolean> {
    const fullKey = this.keyPrefix + key;

    try {
      const result = await this.redis.eval(
        this.releaseScript,
        1,
        fullKey,
        token,
      );

      const released = result === 1;
      if (released) {
        this.logger.debug(`Lock released: ${key}`);
      }

      return released;
    } catch (error) {
      this.logger.error(`Failed to release lock: ${key}`, error);
      return false;
    }
  }

  /**
   * Extend lock TTL
   */
  async extend(key: string, token: string, ttl?: number): Promise<boolean> {
    const fullKey = this.keyPrefix + key;
    const extendTTL = ttl || this.defaultTTL;

    // Lua script to extend only if token matches
    const extendScript = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("pexpire", KEYS[1], ARGV[2])
      else
        return 0
      end
    `;

    try {
      const result = await this.redis.eval(
        extendScript,
        1,
        fullKey,
        token,
        extendTTL.toString(),
      );

      return result === 1;
    } catch (error) {
      this.logger.error(`Failed to extend lock: ${key}`, error);
      return false;
    }
  }

  /**
   * Check if a lock is held
   */
  async isLocked(key: string): Promise<boolean> {
    const fullKey = this.keyPrefix + key;
    return (await this.redis.exists(fullKey)) === 1;
  }

  /**
   * Execute a function with a lock
   */
  async withLock<T>(
    key: string,
    fn: () => Promise<T>,
    options?: LockOptions,
  ): Promise<T> {
    const lock = await this.acquire(key, options);

    if (!lock) {
      throw new Error(`Failed to acquire lock: ${key}`);
    }

    try {
      return await fn();
    } finally {
      await lock.release();
    }
  }

  /**
   * Lock for share operations
   */
  async lockShare(shareId: string, options?: LockOptions): Promise<Lock | null> {
    return this.acquire(`share:${shareId}`, options);
  }

  /**
   * Lock for session operations
   */
  async lockSession(sessionId: string, options?: LockOptions): Promise<Lock | null> {
    return this.acquire(`session:${sessionId}`, options);
  }

  /**
   * Lock for party operations
   */
  async lockParty(partyId: string, options?: LockOptions): Promise<Lock | null> {
    return this.acquire(`party:${partyId}`, options);
  }
}
