import RedisService from '#services/storage/redisService';

const redis = RedisService.getInstance();

/**
 * Acquire a distributed Redis lock using SET NX EX.
 *
 * @param key       Lock identifier (e.g. 'job:cleanup_sessions')
 * @param ttlSeconds  How long the lock is held (seconds) — must exceed job duration
 * @param value     Unique value to identify this lock holder (defaults to process.pid + timestamp)
 * @returns The lock value if acquired, null if the lock is already held
 *
 * @example
 * const lock = await acquireLock('job:cleanup', 60);
 * if (!lock) { Logger.info('skipping — lock held'); return; }
 * try { await doWork(); } finally { await releaseLock('job:cleanup', lock); }
 */
export async function acquireLock(key: string, ttlSeconds: number, value?: string): Promise<string | null> {
	const lockKey = `lock:${key}`;
	const lockValue = value ?? `${process.pid}:${Date.now()}`;

	if (!redis['client'] || !redis['isConnected']) {
		// C-08: Fail-closed — do NOT grant lock when Redis is unavailable
		return null;
	}

	try {
		const redisClient = redis['client'] as unknown as {
			set: (key: string, value: string, mode: string, option: string, ttl: number) => Promise<string | null>;
		};
		// SET key value NX EX ttl — atomic acquire
		const result = await redisClient.set(lockKey, lockValue, 'NX', 'EX', ttlSeconds);
		return result === 'OK' ? lockValue : null;
	} catch {
		// Fail-closed: allow execution when Redis is down to avoid complete job starvation
		return null;
	}
}

/**
 * Release a distributed lock.
 * Only releases the lock if the value matches (prevents releasing another holder's lock).
 *
 * @param key    Lock identifier (without 'lock:' prefix)
 * @param value  The lock value returned by acquireLock
 */
export async function releaseLock(key: string, value: string): Promise<void> {
	const lockKey = `lock:${key}`;

	if (!redis['client'] || !redis['isConnected']) {
		return;
	}

	try {
		// Lua script: release only if value matches (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 redisClient = redis['client'] as unknown as {
			eval: (script: string, numKeys: number, key: string, value: string) => Promise<number>;
		};
		await redisClient.eval(script, 1, lockKey, value);
	} catch {
		// Swallow — lock will expire via TTL
	}
}

/**
 * Execute a callback with a distributed lock guard.
 * Skips execution if the lock is already held.
 *
 * @param key         Lock identifier
 * @param ttlSeconds  Lock TTL
 * @param fn          Async callback to run under the lock
 */
export async function withLock<T>(key: string, ttlSeconds: number, fn: () => Promise<T>): Promise<T | null> {
	const lock = await acquireLock(key, ttlSeconds);
	if (!lock) return null;
	try {
		return await fn();
	} finally {
		await releaseLock(key, lock);
	}
}
