/**
 * 文件模拟Redis服务实现
 *
 * 功能描述：
 * - 在本地开发环境中使用文件系统模拟Redis功能
 * - 支持完整的Redis基础操作和过期机制
 * - 提供数据持久化和自动过期清理功能
 * - 适用于开发测试环境的Redis功能模拟
 *
 * 职责分离：
 * - 数据存储：使用JSON文件持久化Redis数据
 * - 过期管理：实现TTL机制和自动过期清理
 * - 接口实现：完整实现IRedisService接口规范
 * - 文件操作：管理数据文件的读写和目录创建
 *
 * 最近修改：
 * - 2025-01-07: 代码规范优化 - 为所有公共方法添加完整的三级注释，包含业务逻辑和示例代码
 * - 2025-01-07: 代码规范优化 - 修复常量命名规范，为主要方法添加完整的三级注释
 * - 2025-01-07: 代码规范优化 - 完善文件头注释和方法注释，添加详细业务逻辑说明
 *
 * @author moyin
 * @version 1.0.3
 * @since 2025-01-07
 * @lastModified 2025-01-07
 */
import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { promises as fs } from 'fs';
import * as path from 'path';
import { IRedisService } from './redis.interface';

/**
 * 文件模拟Redis服务
 *
 * 职责：
 * - 在本地开发环境中使用文件系统模拟Redis功能
 * - 实现完整的Redis操作接口
 * - 管理数据持久化和过期清理
 *
 * 主要方法：
 * - initializeStorage() - 初始化文件存储
 * - loadData/saveData() - 数据文件读写
 * - cleanExpiredKeys() - 过期键清理
 * - set/get/del() - 基础键值操作
 *
 * 使用场景：
 * - 本地开发环境的Redis功能模拟
 * - 单元测试和集成测试
 * - 无需真实Redis服务器的开发场景
 */
@Injectable()
export class FileRedisService implements IRedisService, OnModuleDestroy {
  private readonly logger = new Logger(FileRedisService.name);
  private readonly DATA_DIR = path.join(process.cwd(), 'redis-data');
  private readonly DATA_FILE = path.join(this.DATA_DIR, 'redis.json');
  private readonly CLEANUP_INTERVAL = 60000; // 每分钟清理一次过期键
  private data: Map<string, { value: string; expireAt?: number }> = new Map();
  private cleanupTimer?: NodeJS.Timeout;
  private saveQueue: Promise<void> = Promise.resolve();

  constructor() {
    this.initializeStorage();
  }

  /**
   * 初始化存储
   *
   * 业务逻辑：
   * 1. 创建数据存储目录（如果不存在）
   * 2. 尝试从文件加载现有数据
   * 3. 启动定时过期清理任务
   * 4. 记录初始化状态日志
   *
   * @throws Error 文件系统操作失败时
   *
   * @example
   * ```typescript
   * // 在构造函数中自动调用
   * constructor() {
   *   this.initializeStorage();
   * }
   * ```
   */
  async initializeStorage(): Promise<void> {
    try {
      // 确保数据目录存在
      await fs.mkdir(this.DATA_DIR, { recursive: true });

      // 尝试加载现有数据
      await this.loadData();

      // 启动过期清理任务
      this.startExpirationCleanup();

      this.logger.log('文件Redis服务初始化完成');
    } catch (error) {
      this.logger.error('初始化文件Redis服务失败', error);
    }
  }

  /**
   * 从文件加载数据
   *
   * 业务逻辑：
   * 1. 读取JSON数据文件内容
   * 2. 解析JSON数据并转换为Map结构
   * 3. 检查并过滤已过期的数据项
   * 4. 初始化内存数据存储
   * 5. 记录加载的数据条数
   *
   * @throws Error 文件读取或JSON解析失败时
   *
   * @example
   * ```typescript
   * await this.loadData();
   * console.log(`加载了 ${this.data.size} 条数据`);
   * ```
   */
  private async loadData(): Promise<void> {
    try {
      const fileContent = await fs.readFile(this.DATA_FILE, 'utf-8');
      const jsonData = JSON.parse(fileContent);

      this.data = new Map();
      for (const [key, item] of Object.entries(jsonData)) {
        const typedItem = item as { value: string; expireAt?: number };
        // 检查是否已过期
        if (!typedItem.expireAt || typedItem.expireAt > Date.now()) {
          this.data.set(key, typedItem);
        }
      }

      this.logger.log(`从文件加载了 ${this.data.size} 条Redis数据`);
    } catch (error) {
      // 文件不存在或格式错误，使用空数据
      this.data = new Map();
      this.logger.log('初始化空的Redis数据存储');
    }
  }

  /**
   * 保存数据到文件
   *
   * 业务逻辑：
   * 1. 确保数据目录存在
   * 2. 将内存中的Map数据转换为JSON对象
   * 3. 格式化JSON字符串（缩进2个空格）
   * 4. 异步写入到数据文件
   * 5. 处理文件写入异常
   *
   * @throws Error 文件写入失败时
   *
   * @example
   * ```typescript
   * this.data.set('key', { value: 'data' });
   * await this.saveData();
   * ```
   */
  private async saveData(): Promise<void> {
    const serializedData = JSON.stringify(Object.fromEntries(this.data), null, 2);
    const persistSnapshot = async () => {
      try {
        const dataDir = path.dirname(this.DATA_FILE);
        await fs.mkdir(dataDir, { recursive: true });
        await fs.writeFile(this.DATA_FILE, serializedData);
      } catch (error) {
        this.logger.error('保存Redis数据到文件失败', error);
      }
    };

    this.saveQueue = this.saveQueue.then(persistSnapshot, persistSnapshot);
    await this.saveQueue;
  }

  /**
   * 启动过期清理任务
   *
   * 业务逻辑：
   * 1. 清理现有定时器（如果存在）
   * 2. 设置定时器，每60秒执行一次清理
   * 3. 调用cleanExpiredKeys方法清理过期数据
   * 4. 确保应用运行期间持续清理过期键
   * 5. 保存定时器引用以便后续清理
   *
   * @example
   * ```typescript
   * this.startExpirationCleanup();
   * // 每分钟自动清理过期键
   * ```
   */
  private startExpirationCleanup(): void {
    // 清理现有定时器
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
    }

    this.cleanupTimer = setInterval(async () => {
      await this.cleanExpiredKeys();
    }, this.CLEANUP_INTERVAL);
  }

  /**
   * 清理过期的键
   *
   * 业务逻辑：
   * 1. 获取当前时间戳
   * 2. 遍历所有数据项检查过期时间
   * 3. 删除已过期的键值对
   * 4. 统计清理的键数量
   * 5. 如有清理则保存数据并记录日志
   *
   * @example
   * ```typescript
   * await this.cleanExpiredKeys();
   * // 清理了 3 个过期的Redis键
   * ```
   */
  private async cleanExpiredKeys(): Promise<void> {
    const now = Date.now();
    let cleanedCount = 0;

    for (const [key, item] of this.data.entries()) {
      if (item.expireAt && item.expireAt <= now) {
        this.data.delete(key);
        cleanedCount++;
      }
    }

    if (cleanedCount > 0) {
      this.logger.log(`清理了 ${cleanedCount} 个过期的Redis键`);
      await this.saveData(); // 保存清理后的数据
    }
  }

  /**
   * 设置键值对
   *
   * 业务逻辑：
   * 1. 创建数据项对象，包含值和可选的过期时间
   * 2. 如果设置了TTL，计算过期时间戳
   * 3. 将数据存储到内存Map中
   * 4. 异步保存数据到文件
   * 5. 记录操作日志
   *
   * @param key 键名，不能为空
   * @param value 值，支持字符串类型
   * @param ttl 可选的过期时间（秒），不设置则永不过期
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.set('user:123', 'userData', 3600);
   * ```
   */
  async set(key: string, value: string, ttl?: number): Promise<void> {
    const item: { value: string; expireAt?: number } = { value };

    if (ttl && ttl > 0) {
      item.expireAt = Date.now() + ttl * 1000;
    }

    this.data.set(key, item);
    await this.saveData();

    this.logger.debug(`设置Redis键: ${key}, TTL: ${ttl || '永不过期'}`);
  }

  /**
   * 获取键对应的值
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 检查数据项是否存在
   * 3. 验证数据项是否已过期
   * 4. 如果过期则删除并保存数据
   * 5. 返回有效的值或null
   *
   * @param key 键名，不能为空
   * @returns Promise<string | null> 键对应的值，不存在或已过期返回null
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * const value = await redisService.get('user:123');
   * ```
   */
  async get(key: string): Promise<string | null> {
    const item = this.data.get(key);

    if (!item) {
      return null;
    }

    // 检查是否过期
    if (item.expireAt && item.expireAt <= Date.now()) {
      this.data.delete(key);
      await this.saveData();
      return null;
    }

    return item.value;
  }

  /**
   * 删除指定的键
   *
   * 业务逻辑：
   * 1. 检查键是否存在于内存Map中
   * 2. 从内存Map中删除键
   * 3. 如果键存在则保存数据到文件
   * 4. 记录删除操作日志
   * 5. 返回删除是否成功
   *
   * @param key 键名，不能为空
   * @returns Promise<boolean> 删除成功返回true，键不存在返回false
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * const deleted = await redisService.del('user:123');
   * console.log(deleted ? '删除成功' : '键不存在');
   * ```
   */
  async del(key: string): Promise<boolean> {
    const existed = this.data.has(key);
    this.data.delete(key);

    if (existed) {
      await this.saveData();
      this.logger.debug(`删除Redis键: ${key}`);
    }

    return existed;
  }

  /**
   * 检查键是否存在
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 检查数据项是否存在
   * 3. 验证数据项是否已过期
   * 4. 如果过期则删除并保存数据
   * 5. 返回键的存在状态
   *
   * @param key 键名，不能为空
   * @returns Promise<boolean> 键存在返回true，不存在或已过期返回false
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * const exists = await redisService.exists('user:123');
   * if (exists) {
   *   console.log('用户数据存在');
   * }
   * ```
   */
  async exists(key: string): Promise<boolean> {
    const item = this.data.get(key);

    if (!item) {
      return false;
    }

    // 检查是否过期
    if (item.expireAt && item.expireAt <= Date.now()) {
      this.data.delete(key);
      await this.saveData();
      return false;
    }

    return true;
  }

  /**
   * 设置键的过期时间
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 检查数据项是否存在
   * 3. 计算过期时间戳并设置到数据项
   * 4. 保存更新后的数据到文件
   * 5. 记录过期时间设置日志
   *
   * @param key 键名，不能为空
   * @param ttl 过期时间（秒），必须大于0
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.expire('user:123', 3600); // 1小时后过期
   * ```
   */
  async expire(key: string, ttl: number): Promise<void> {
    const item = this.data.get(key);

    if (item) {
      item.expireAt = Date.now() + ttl * 1000;
      await this.saveData();
      this.logger.debug(`设置Redis键过期时间: ${key}, TTL: ${ttl}秒`);
    }
  }

  /**
   * 获取键的剩余过期时间
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 检查数据项是否存在
   * 3. 检查是否设置了过期时间
   * 4. 计算剩余过期时间
   * 5. 如果已过期则删除键并保存数据
   *
   * @param key 键名，不能为空
   * @returns Promise<number> 剩余时间（秒），-1表示永不过期，-2表示键不存在
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * const ttl = await redisService.ttl('user:123');
   * if (ttl > 0) {
   *   console.log(`还有${ttl}秒过期`);
   * } else if (ttl === -1) {
   *   console.log('永不过期');
   * } else {
   *   console.log('键不存在');
   * }
   * ```
   */
  async ttl(key: string): Promise<number> {
    const item = this.data.get(key);

    if (!item) {
      return -2; // 键不存在
    }

    if (!item.expireAt) {
      return -1; // 永不过期
    }

    const remaining = Math.ceil((item.expireAt - Date.now()) / 1000);

    if (remaining <= 0) {
      // 已过期，删除键
      this.data.delete(key);
      await this.saveData();
      return -2;
    }

    return remaining;
  }

  /**
   * 清空所有数据
   *
   * 业务逻辑：
   * 1. 清空内存Map中的所有数据
   * 2. 保存空数据到文件
   * 3. 记录清空操作日志
   *
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.flushall();
   * console.log('所有数据已清空');
   * ```
   */
  async flushall(): Promise<void> {
    this.data.clear();
    await this.saveData();
    this.logger.log('清空所有Redis数据');
  }

  /**
   * 设置键值对并指定过期时间
   *
   * 业务逻辑：
   * 1. 创建数据项对象，包含值和过期时间戳
   * 2. 计算过期时间戳（当前时间 + TTL秒数）
   * 3. 将数据存储到内存Map中
   * 4. 异步保存数据到文件
   * 5. 记录操作日志
   *
   * @param key 键名，不能为空
   * @param ttl 过期时间（秒），必须大于0
   * @param value 值，支持字符串类型
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.setex('session:abc', 1800, 'sessionData');
   * ```
   */
  async setex(key: string, ttl: number, value: string): Promise<void> {
    const item: { value: string; expireAt?: number } = {
      value,
      expireAt: Date.now() + ttl * 1000,
    };

    this.data.set(key, item);
    await this.saveData();

    this.logger.debug(`设置Redis键(setex): ${key}, TTL: ${ttl}秒`);
  }

  /**
   * 键值自增操作
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 如果键不存在则初始化为1
   * 3. 如果键存在则将值转换为数字并加1
   * 4. 更新数据项的值
   * 5. 保存数据到文件并记录日志
   *
   * @param key 键名，不能为空
   * @returns Promise<number> 自增后的新值
   * @throws Error 当文件操作失败或值不是数字时
   *
   * @example
   * ```typescript
   * const newValue = await redisService.incr('counter');
   * console.log(`计数器新值: ${newValue}`);
   * ```
   */
  async incr(key: string): Promise<number> {
    const item = this.data.get(key);
    let newValue: number;

    if (!item) {
      newValue = 1;
      this.data.set(key, { value: '1' });
    } else {
      newValue = parseInt(item.value, 10) + 1;
      item.value = newValue.toString();
    }

    await this.saveData();
    this.logger.debug(`自增Redis键: ${key}, 新值: ${newValue}`);
    return newValue;
  }

  /**
   * 向集合添加成员
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 如果键不存在则创建新的Set集合
   * 3. 如果键存在则解析JSON数据为Set集合
   * 4. 向集合中添加新成员
   * 5. 将更新后的集合保存到内存Map和文件
   *
   * @param key 集合键名，不能为空
   * @param member 要添加的成员，不能为空
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.sadd('users', 'user123');
   * ```
   */
  async sadd(key: string, member: string): Promise<void> {
    const item = this.data.get(key);
    let members: Set<string>;

    if (!item) {
      members = new Set([member]);
    } else {
      members = new Set(JSON.parse(item.value));
      members.add(member);
    }

    this.data.set(key, { value: JSON.stringify([...members]), expireAt: item?.expireAt });
    await this.saveData();
    this.logger.debug(`添加集合成员: ${key} -> ${member}`);
  }

  /**
   * 从集合移除成员
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 如果键不存在则直接返回
   * 3. 解析JSON数据为Set集合
   * 4. 从集合中移除指定成员
   * 5. 如果集合为空则删除键，否则更新集合数据
   *
   * @param key 集合键名，不能为空
   * @param member 要移除的成员，不能为空
   * @returns Promise<void> 操作完成的Promise
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * await redisService.srem('users', 'user123');
   * ```
   */
  async srem(key: string, member: string): Promise<void> {
    const item = this.data.get(key);

    if (!item) {
      return;
    }

    const members = new Set<string>(JSON.parse(item.value));
    members.delete(member);

    if (members.size === 0) {
      this.data.delete(key);
    } else {
      item.value = JSON.stringify([...members]);
    }

    await this.saveData();
    this.logger.debug(`移除集合成员: ${key} -> ${member}`);
  }

  /**
   * 获取集合的所有成员
   *
   * 业务逻辑：
   * 1. 从内存Map中查找键对应的数据项
   * 2. 如果键不存在则返回空数组
   * 3. 检查数据项是否已过期
   * 4. 如果过期则删除键并保存数据，返回空数组
   * 5. 解析JSON数据并返回成员列表
   *
   * @param key 集合键名，不能为空
   * @returns Promise<string[]> 集合成员列表，集合不存在返回空数组
   * @throws Error 当文件操作失败时
   *
   * @example
   * ```typescript
   * const members = await redisService.smembers('users');
   * console.log('用户列表:', members);
   * ```
   */
  async smembers(key: string): Promise<string[]> {
    const item = this.data.get(key);

    if (!item) {
      return [];
    }

    // 检查是否过期
    if (item.expireAt && item.expireAt <= Date.now()) {
      this.data.delete(key);
      await this.saveData();
      return [];
    }

    return JSON.parse(item.value);
  }

  async keys(pattern: string): Promise<string[]> {
    const matcher = new RegExp(`^${pattern
      .replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
      .replace(/\*/g, '.*')}$`);
    const matches: string[] = [];
    let removedExpiredKey = false;

    for (const [key, item] of this.data.entries()) {
      if (item.expireAt && item.expireAt <= Date.now()) {
        this.data.delete(key);
        removedExpiredKey = true;
        continue;
      }
      if (matcher.test(key)) {
        matches.push(key);
      }
    }

    if (removedExpiredKey) {
      await this.saveData();
    }

    return matches;
  }

  /**
   * 模块销毁时的清理操作
   *
   * 业务逻辑：
   * 1. 清理定时器，防止内存泄漏
   * 2. 保存当前数据到文件
   * 3. 记录清理操作日志
   * 4. 释放相关资源
   *
   * @returns void 无返回值
   *
   * @example
   * ```typescript
   * // NestJS框架会在模块销毁时自动调用
   * onModuleDestroy() {
   *   // 自动清理定时器和保存数据
   * }
   * ```
   */
  onModuleDestroy(): void {
    if (this.cleanupTimer) {
      clearInterval(this.cleanupTimer);
      this.cleanupTimer = undefined;
      this.logger.log('清理定时器已停止');
    }

    // 保存最后的数据
    this.saveData().catch(error => {
      this.logger.error('模块销毁时保存数据失败', error);
    });

    this.logger.log('FileRedisService已清理');
  }
}
