/**
 * Valkey-backed presign URL cache (v3).
 *
 * Replaces the in-process Map from v2. All presign URLs are stored in Valkey
 * (Redis-compatible) with native TTL, so:
 *   - Cache survives UI process restarts.
 *   - Cache is shared across multiple UI replicas (if scaled horizontally).
 *   - No manual sweep timer needed — Valkey expires entries automatically.
 *
 * MinIO presigned audio URLs are valid for 1 hour (set by the backend).
 * We cache them for 50 minutes so the browser always gets a URL with at
 * least 10 minutes of remaining validity.
 *
 * Voice-sample URLs use the same cache with key "sample:<voice>".
 *
 * Connection:
 *   VALKEY_URL env var (default: redis://valkey:6379)
 *   ioredis handles reconnection automatically.
 */

import Redis from 'ioredis';

const AUDIO_TTL_S = 50 * 60; // 50 minutes in seconds (Valkey TTL is in seconds)

// Lazily-initialised singleton client.
let _client: Redis | null = null;

function client(): Redis {
	if (!_client) {
		const url = process.env.VALKEY_URL ?? 'redis://valkey:6379';
		_client = new Redis(url, {
			// Reconnect automatically with exponential backoff (ioredis default).
			// lazyConnect: false means the connection is established immediately.
			lazyConnect: false,
			// Log connection errors to stderr; do not crash the process.
			enableOfflineQueue: true,
			maxRetriesPerRequest: 2,
		});
		_client.on('error', (err: Error) => {
			console.error('[presignCache] Valkey error:', err.message);
		});
	}
	return _client;
}

// ── Key helpers ───────────────────────────────────────────────────────────────

/** Cache key for a chapter audio presigned URL. */
export function audioKey(slug: string, n: number, voice: string): string {
	return `audio:${slug}:${n}:${voice}`;
}

/** Cache key for a voice-sample presigned URL. */
export function sampleKey(voice: string): string {
	return `sample:${voice}`;
}

// ── Public API ────────────────────────────────────────────────────────────────

/** Return the cached URL for key, or null if absent / expired. */
export async function get(key: string): Promise<string | null> {
	try {
		return await client().get(key);
	} catch (err) {
		console.error('[presignCache] get error:', err);
		return null;
	}
}

/** Store a presigned URL under key for ttlSeconds seconds. */
export async function set(key: string, url: string, ttlSeconds = AUDIO_TTL_S): Promise<void> {
	try {
		await client().set(key, url, 'EX', ttlSeconds);
	} catch (err) {
		console.error('[presignCache] set error:', err);
	}
}

/** Invalidate a specific key (e.g. after audio generation to force refresh). */
export async function invalidate(key: string): Promise<void> {
	try {
		await client().del(key);
	} catch (err) {
		console.error('[presignCache] invalidate error:', err);
	}
}

/**
 * Disconnect from Valkey — called on graceful shutdown.
 * ioredis queues commands during reconnects; calling quit() drains the queue
 * and closes the connection cleanly.
 */
export async function drain(): Promise<void> {
	if (_client) {
		try {
			await _client.quit();
		} catch {
			// ignore — process is exiting anyway
		}
		_client = null;
	}
}

/**
 * Returns the approximate number of keys matching the libnovel presign prefix.
 * Used for health/debug only — not called in the hot path.
 */
export async function size(): Promise<number> {
	try {
		// DBSIZE returns the total key count in the current DB.
		// For a precise count of just our keys, use SCAN with a pattern.
		const keys = await client().keys('audio:*');
		const sampleKeys = await client().keys('sample:*');
		return keys.length + sampleKeys.length;
	} catch {
		return -1;
	}
}
