import { LocalCache, CacheInstance, FetchingFunction, CachableValue, RedisCache } from 'cachette';
import crypto from 'crypto';
import Logger from './logger.js';

/**
 * The Cache class provides caching capabilities that can be used across your integration.
 * It can be backed by a Redis instance (by passing it a URL to the instance) or a local cache.
 *
 * @see {@link Cache.create}
 */
export class Cache {
  private cacheInstance: CacheInstance;

  private constructor(cacheInstance: CacheInstance) {
    this.cacheInstance = cacheInstance;
  }

  /**
   * Get or fetch a value
   *
   * @param key     The key of the value to get
   * @param ttl     The time to live of the value in seconds.
   * @param fetchFn The function that can retrieve the original value
   * @param lockTtl Global distributed lock TTL (in seconds) protecting fetching.
   *                If undefined, 0 or falsy, locking is not preformed
   * @param shouldCacheError A callback being passed errors, controlling whether
   *                         to cache or not errors. Defaults to never cache.
   *
   * @returns       The cached or fetched value
   */
  public getOrFetchValue<F extends FetchingFunction = FetchingFunction>(
    key: string,
    ttl: number,
    fetcher: F,
    lockTtl?: number,
    shouldCacheError?: (err: Error) => boolean,
  ): Promise<ReturnType<F>> {
    return this.cacheInstance.getOrFetchValue(key, ttl, fetcher, lockTtl, shouldCacheError);
  }

  /**
   * Get a value from the cache.
   *
   * @param key   The key of the value to get.
   *
   * @return      The value associated with the key, or undefined if
   *              no such value exists.
   */
  public getValue(key: string): Promise<CachableValue> {
    return this.cacheInstance.getValue(key);
  }

  /**
   * Set a value in the cache.
   *
   * @param key        The key of the value to set.
   * @param value      The value to set.
   * @param ttl        The time to live of the value in seconds.
   *                   By default, the value will not expire
   *
   * @return true if the value was stored, false otherwise.
   */
  public setValue(key: string, value: CachableValue, ttl?: number): Promise<boolean> {
    return this.cacheInstance.setValue(key, value, ttl);
  }

  /**
   * Delete a value from the cache.
   * @param key — The key of the value to set.
   */
  public delValue(key: string): Promise<void> {
    return this.cacheInstance.delValue(key);
  }

  /**
   * Get the TTL of an entry, in ms
   *
   * @param key   The key of the entry whose ttl to retrieve
   *
   * @return      The remaining TTL on the entry, in ms.
   *              undefined if the entry does not exist.
   *              0 if the entry does not expire.
   */
  public getTtl(key: string): Promise<number | undefined> {
    return this.cacheInstance.getTtl(key);
  }

  /**
   * Initializes a Cache backed by the Redis instance at the provided url if present, or a LocalCache otherwise.
   *
   * @param redisUrl - The redis url to connect to (optional).
   * @returns A cache instance.
   */
  public static create(redisUrl?: string): Cache {
    const cacheInstance: CacheInstance = redisUrl ? new RedisCache(redisUrl) : new LocalCache();

    // Intended: the correlation id will be the same for all logs of Cachette.
    const correlationId = crypto.randomUUID();

    const logger = new Logger({ correlation_id: correlationId });

    cacheInstance
      .on('info', message => {
        logger.info(message);
      })
      .on('warn', message => {
        logger.warn(message);
      })
      .on('error', message => {
        logger.error(message);
      });

    return new Cache(cacheInstance);
  }
}
