import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { Context } from '../../common/log';

@Injectable()
export class RedisService {
  private readonly logger = new Logger(RedisService.name);

  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  /**
   * キーに対応する値を設定する。
   * @param key キー
   * @param value キーに対応する値
   * @param ttl 有効期限（秒）
   */
  async set(
    context: Context,
    key: string,
    value: unknown,
    ttl?: number | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.set.name} | params: { ` +
        `ttl: ${ttl} };`,
    );

    try {
      // cache-manager-redis-store がcache-managerのset形式と不一致な値の渡し方を採用しているため、
      // @types/cache-managerのset形式を使用すると、redisに値が保存されない。
      // そのため、{ttl : ttl} をany型として渡すことで、強引にcache-manager-redis-storeのsetを使用する。
      // https://www.npmjs.com/package/cache-manager
      await this.cacheManager.set(key, value, { ttl: ttl } as any);
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] ${error}`);
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.set.name}`);
    }
  }

  /**
   * 複数のキーとそのキーに対応する値を設定する。
   * @template T
   * @param records キーとそのキーに対応する値のペアの配列
   * @param ttl 有効期限（秒）
   */
  async mset<T>(
    context: Context,
    records: { key: string; value: T }[],
    ttl?: number | undefined,
  ): Promise<void> {
    this.logger.log(
      `[IN] [${context.getTrackingId()}] ${this.mset.name} | params: { ` +
        `ttl: ${ttl} };`,
    );

    try {
      // cache-manager-redis-store のmsetが壊れており、利用できないため、
      // 一つずつsetする。
      for await (const record of records) {
        await this.set(context, record.key, record.value, ttl);
      }
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] ${error}`);
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.mset.name}`);
    }
  }

  /**
   * キーに対応する値を取得する。
   * @template T
   * @param key キー
   * @returns キーに対応する値
   */
  async get<T>(context: Context, key: string): Promise<T | undefined> {
    this.logger.log(`[IN] [${context.getTrackingId()}] ${this.get.name};`);

    try {
      const value = await this.cacheManager.get<T>(key);
      return value;
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] ${error}`);
      return undefined;
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.get.name}`);
    }
  }

  /**
   * 複数のキーに対して、対応する値を取得する。
   * キーに対応する値がなかった場合、valueにはnullがセットされる。
   * @param keys キーの配列
   * @returns キーとそのキーに対応する値のペアの配列
   */
  async mget<T>(
    context: Context,
    keys: string[],
  ): Promise<{ key: string; value: T | null }[]> {
    this.logger.log(`[IN] [${context.getTrackingId()}] ${this.mget.name};`);

    if (keys.length === 0) return []; // mget操作は0件の時エラーとなるため、0件は特別扱いする

    try {
      const records = await this.cacheManager.store.mget(...keys);
      // getで取得した順序とKeysの順序は一致するはずなので、indexを利用してペアになるよう加工する
      return records.map((record, index) => {
        return {
          key: keys[index],
          value: record ? (record as T) : null,
        };
      });
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] ${error}`);
      return [];
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.mget.name}`);
    }
  }

  /**
   * キーに対応する値を削除する。
   * @param key キー
   */
  async del(context: Context, key: string): Promise<void> {
    this.logger.log(`[IN] [${context.getTrackingId()}] ${this.del.name};`);

    try {
      await this.cacheManager.del(key);
    } catch (error) {
      this.logger.error(`[${context.getTrackingId()}] ${error}`);
    } finally {
      this.logger.log(`[OUT] [${context.getTrackingId()}] ${this.del.name}`);
    }
  }
}
