/**
 * Nonce Manager - Handles distributed nonce management using Redis locks
 */

import Redis from 'ioredis';
import { BesuClient } from '../clients/besu-client';

export class NonceManager {
  private redis: Redis;
  private besuClient: BesuClient;
  private keyPrefix: string;
  private chainId: number;
  private lockTimeout: number = 30000; // 30 seconds

  constructor(redis: Redis, besuClient: BesuClient, keyPrefix: string, chainId: number) {
    this.redis = redis;
    this.besuClient = besuClient;
    this.keyPrefix = keyPrefix;
    this.chainId = chainId;
  }

  private getLockKey(address: string): string {
    return `${this.keyPrefix}:lock:${this.chainId}:${address.toLowerCase()}`;
  }

  private getNonceKey(address: string): string {
    return `${this.keyPrefix}:nonce:${this.chainId}:${address.toLowerCase()}`;
  }

  private async acquireLock(address: string): Promise<string> {
    const lockKey = this.getLockKey(address);
    const lockValue = `${Date.now()}-${Math.random()}`;
    const lockTTL = Math.floor(this.lockTimeout / 1000); // Convert to seconds

    // Try to acquire lock with SET NX EX
    const result = await this.redis.set(lockKey, lockValue, 'EX', lockTTL, 'NX');

    if (result !== 'OK') {
      // Lock already exists, wait a bit and retry (with exponential backoff)
      await new Promise(resolve => setTimeout(resolve, 100));
      return this.acquireLock(address);
    }

    return lockValue;
  }

  private async releaseLock(address: string, lockValue: string): Promise<void> {
    const lockKey = this.getLockKey(address);

    // Use Lua script to ensure we only delete our own lock
    const luaScript = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;

    await this.redis.eval(luaScript, 1, lockKey, lockValue);
  }

  async getNextNonce(address: string): Promise<number> {
    const lockValue = await this.acquireLock(address);
    const nonceKey = this.getNonceKey(address);

    try {
      // Get pending nonce from chain
      const pendingNonceHex = await this.besuClient.callRpc('eth_getTransactionCount', [
        address,
        'pending',
      ]);

      const pendingNonce = parseInt(pendingNonceHex, 16);

      // Get cached nonce from Redis
      const cachedNonceStr = await this.redis.get(nonceKey);
      const cachedNonce = cachedNonceStr ? parseInt(cachedNonceStr, 10) : -1;

      // Use the maximum of pending and cached nonce
      const nextNonce = Math.max(pendingNonce, cachedNonce + 1);

      // Update cache (but don't commit yet - that happens after successful send)
      await this.redis.set(nonceKey, nextNonce.toString(), 'EX', 3600); // 1 hour TTL

      return nextNonce;
    } finally {
      await this.releaseLock(address, lockValue);
    }
  }

  async commitNonce(address: string, nonce: number): Promise<void> {
    const lockValue = await this.acquireLock(address);
    const nonceKey = this.getNonceKey(address);

    try {
      // Update cache to nonce + 1 for next transaction
      await this.redis.set(nonceKey, (nonce + 1).toString(), 'EX', 3600);
    } finally {
      await this.releaseLock(address, lockValue);
    }
  }

  async clearNonceCache(address: string): Promise<void> {
    const nonceKey = this.getNonceKey(address);
    await this.redis.del(nonceKey);
  }
}
