/**
 * Redis client singleton for SentryAgent.ai AgentIdP.
 * Used for token revocation tracking, rate limiting, and monthly token counts.
 */

import { createClient, RedisClientType } from 'redis';
import { redisCommandDurationSeconds } from '../metrics/registry.js';

/**
 * Wraps a Redis client method to record its duration in Prometheus.
 * The cast to `T` is safe: the wrapper is async with identical parameters and
 * resolves to the same value. TypeScript cannot infer this through the generic
 * constraint alone, so we assert the type explicitly.
 *
 * @param fn - The bound Redis method to wrap.
 * @param command - The command label used in the Prometheus histogram.
 * @returns The wrapped method with identical signature.
 */
function instrumentRedisMethod<TArgs extends unknown[], TReturn>(
  fn: (...args: TArgs) => Promise<TReturn>,
  command: string,
): (...args: TArgs) => Promise<TReturn> {
  return async (...args: TArgs): Promise<TReturn> => {
    const end = redisCommandDurationSeconds.startTimer({ command });
    try {
      return await fn(...args);
    } finally {
      end();
    }
  };
}

let redisClient: RedisClientType | null = null;

/**
 * Returns the singleton Redis client instance.
 * Initialises and connects the client on first call using REDIS_URL from env.
 *
 * @returns Promise resolving to the connected Redis client.
 * @throws Error if REDIS_URL is not set or connection fails.
 */
export async function getRedisClient(): Promise<RedisClientType> {
  if (!redisClient) {
    const url = process.env['REDIS_URL'];
    if (!url) {
      throw new Error('REDIS_URL environment variable is required');
    }

    redisClient = createClient({ url }) as RedisClientType;

    redisClient.on('error', (err: Error) => {
      // eslint-disable-next-line no-console
      console.error('Redis client error', err);
    });

    await redisClient.connect();

    // Wrap high-frequency commands to record durations in Prometheus
    redisClient.get    = instrumentRedisMethod(redisClient.get.bind(redisClient),    'get');
    redisClient.set    = instrumentRedisMethod(redisClient.set.bind(redisClient),    'set');
    redisClient.incr   = instrumentRedisMethod(redisClient.incr.bind(redisClient),   'incr');
    redisClient.expire = instrumentRedisMethod(redisClient.expire.bind(redisClient), 'expire');
    redisClient.ping   = instrumentRedisMethod(redisClient.ping.bind(redisClient),   'ping');
  }
  return redisClient;
}

/**
 * Disconnects the Redis client and resets the singleton.
 * Used for graceful shutdown and tests.
 *
 * @returns Promise that resolves when the client is disconnected.
 */
export async function closeRedisClient(): Promise<void> {
  if (redisClient) {
    await redisClient.quit();
    redisClient = null;
  }
}
