// Distributed lock abstraction with two implementations:
// - memory: per-process key set with TTL (only meaningful within one instance)
// - redis: SET key token NX PX ttl, released with a compare-and-delete Lua
//   script so only the holder can release it
//
// Use acquire/release for long-lived ownership (e.g. a background poller) and
// withLock for a one-shot critical section. Selection is based on REDIS_URL.

import { randomUUID } from 'crypto';
import { getRedis, isRedisEnabled } from '../redis.js';

export interface Lock {
  readonly backend: 'memory' | 'redis';
  // Returns a token when the lock was acquired, or null when already held.
  acquire(key: string, ttlMs: number): Promise<string | null>;
  release(key: string, token: string): Promise<void>;
  // Runs fn while holding the lock; returns fn's result, or null if not acquired.
  withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null>;
}

// ==================== Memory implementation ====================

class MemoryLock implements Lock {
  readonly backend = 'memory' as const;
  private held = new Map<string, { token: string; expiresAt: number }>();

  async acquire(key: string, ttlMs: number): Promise<string | null> {
    const existing = this.held.get(key);
    const now = Date.now();
    if (existing && existing.expiresAt > now) {
      return null;
    }
    const token = randomUUID();
    this.held.set(key, { token, expiresAt: now + ttlMs });
    return token;
  }

  async release(key: string, token: string): Promise<void> {
    const existing = this.held.get(key);
    if (existing && existing.token === token) {
      this.held.delete(key);
    }
  }

  async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
    const token = await this.acquire(key, ttlMs);
    if (!token) return null;
    try {
      return await fn();
    } finally {
      await this.release(key, token);
    }
  }
}

// ==================== Redis implementation ====================

const RELEASE_SCRIPT =
  'if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end';

class RedisLock implements Lock {
  readonly backend = 'redis' as const;

  async acquire(key: string, ttlMs: number): Promise<string | null> {
    const redis = getRedis();
    if (!redis) {
      // Redis configured but unavailable: do not block critical sections.
      return randomUUID();
    }
    const token = randomUUID();
    try {
      const result = await redis.set(`lock:${key}`, token, 'PX', ttlMs, 'NX');
      return result === 'OK' ? token : null;
    } catch (err: any) {
      console.error('[lock] redis acquire error, proceeding without lock:', err?.message || err);
      // Fail open so a Redis outage does not deadlock startup or jobs.
      return randomUUID();
    }
  }

  async release(key: string, token: string): Promise<void> {
    const redis = getRedis();
    if (!redis) return;
    try {
      await redis.eval(RELEASE_SCRIPT, 1, `lock:${key}`, token);
    } catch (err: any) {
      console.error('[lock] redis release error:', err?.message || err);
    }
  }

  async withLock<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T | null> {
    const token = await this.acquire(key, ttlMs);
    if (!token) return null;
    try {
      return await fn();
    } finally {
      await this.release(key, token);
    }
  }
}

// ==================== Selection ====================

let instance: Lock | null = null;

export function getLock(): Lock {
  if (!instance) {
    instance = isRedisEnabled() ? new RedisLock() : new MemoryLock();
  }
  return instance;
}
