/**
 * student-bff Redis 缓存模块.
 *
 * 仲裁依据:
 *   - coord-final-decisions §2 B6 (Redis 5-30s 短缓存)
 *   - coord-final-decisions §1 G9 (优雅关闭 Redis 连接)
 *   - president-final-rulings §2.6 (降级模式方案 B: degraded=true + degradedReason)
 *   - 02-architecture-design.md §3.1 (Redis 缓存 Schema)
 *
 * 缓存 Key 规范:
 *   student:dashboard:{userId}                  TTL 15s
 *   student:exams:{userId}:{classId}            TTL 30s
 *   student:homework:{userId}:{classId}         TTL 30s
 *   student:grades:{userId}:{page}              TTL 60s
 *   student:notifications:{userId}:{page}       TTL 15s
 *   student:textbooks:{gradeId}:{subjectId}     TTL 300s
 *   student:chapters:{textbookId}               TTL 300s
 *   student:analytics:weakness:{userId}         TTL 300s
 *   student:analytics:trend:{userId}:{range}    TTL 600s
 *   student:viewports:{userId}                  TTL 300s
 */
import { Global, Module, OnModuleDestroy } from "@nestjs/common";
import { Redis } from "ioredis";
import { env } from "../../config/env.js";
import { logger } from "../observability/logger.js";
import { recordCacheAccess } from "../observability/metrics.js";

export const REDIS_CLIENT = Symbol("REDIS_CLIENT");

/**
 * 缓存 TTL 预设 (秒).
 */
export const CacheTTL = {
  DASHBOARD: 15,
  EXAMS: 30,
  HOMEWORK: 30,
  GRADES: 60,
  NOTIFICATIONS: 15,
  TEXTBOOKS: 300,
  CHAPTERS: 300,
  ANALYTICS_WEAKNESS: 300,
  ANALYTICS_TREND: 600,
  VIEWPORTS: 300,
} as const;

/**
 * 缓存 Key 构建器 (统一前缀 + 规范化).
 */
export function buildCacheKey(
  pattern: string,
  ...parts: (string | number)[]
): string {
  const suffix = parts.map(String).join(":");
  return `${env.REDIS_KEY_PREFIX}${pattern}:${suffix}`;
}

@Global()
@Module({
  providers: [
    {
      provide: REDIS_CLIENT,
      useFactory: (): Redis => {
        const client = new Redis(env.REDIS_URL, {
          lazyConnect: false,
          maxRetriesPerRequest: 3,
          enableReadyCheck: true,
          retryStrategy: (times) => Math.min(times * 100, 2000),
        });
        client.on("error", (err) => {
          logger.error({ err }, "Redis client error");
        });
        client.on("connect", () => {
          logger.info({ url: env.REDIS_URL }, "Redis connected");
        });
        return client;
      },
    },
  ],
  exports: [REDIS_CLIENT],
})
export class CacheModule implements OnModuleDestroy {
  constructor() {}

  async onModuleDestroy(): Promise<void> {
    // G9 优雅关闭: 由 main.ts SIGTERM handler 统一调用 disconnectAll
    logger.info("CacheModule destroyed (Redis disconnect handled by main.ts)");
  }
}

/**
 * 缓存服务 - 封装 Redis get/set + 降级模式.
 *
 * 使用方式:
 *   const cached = await cacheService.get<StudentDashboard>("dashboard", userId);
 *   if (cached) return cached;
 *   const fresh = await aggregateDashboard(userId);
 *   await cacheService.set("dashboard", userId, fresh, CacheTTL.DASHBOARD);
 */
export class CacheService {
  constructor(private readonly redis: Redis) {}

  /**
   * 读取缓存, 自动 JSON 反序列化.
   * 失败时返回 null (不抛异常, 上层走降级模式).
   */
  async get<T>(
    pattern: string,
    ...keyParts: (string | number)[]
  ): Promise<T | null> {
    const key = buildCacheKey(pattern, ...keyParts);
    try {
      const raw = await this.redis.get(key);
      if (raw) {
        recordCacheAccess(pattern, true);
        return JSON.parse(raw) as T;
      }
      recordCacheAccess(pattern, false);
      return null;
    } catch (err) {
      logger.warn({ err, key, pattern }, "Cache get failed, returning null");
      recordCacheAccess(pattern, false);
      return null;
    }
  }

  /**
   * 写入缓存, 自动 JSON 序列化.
   * 失败时仅记录日志, 不影响主流程 (降级模式).
   */
  async set(
    pattern: string,
    value: unknown,
    ttlSeconds: number,
    ...keyParts: (string | number)[]
  ): Promise<void> {
    const key = buildCacheKey(pattern, ...keyParts);
    try {
      const serialized = JSON.stringify(value);
      // TTL 加 ±20% 随机抖动, 避免缓存雪崩 (02 §8.1)
      const jitter = Math.floor(ttlSeconds * 0.2 * (Math.random() * 2 - 1));
      const ttl = Math.max(1, ttlSeconds + jitter);
      await this.redis.set(key, serialized, "EX", ttl);
    } catch (err) {
      logger.warn({ err, key, pattern }, "Cache set failed, skipping");
    }
  }

  /**
   * 失效缓存 (按 pattern 通配符删除).
   * 用于写操作后主动失效相关缓存.
   */
  async invalidate(
    pattern: string,
    ...keyParts: (string | number)[]
  ): Promise<void> {
    const key = buildCacheKey(pattern, ...keyParts);
    try {
      // 如果 keyParts 含通配符, 用 SCAN 删除
      if (key.includes("*")) {
        const stream = this.redis.scanStream({
          match: key,
          count: 100,
        });
        const pipeline = this.redis.pipeline();
        stream.on("data", (keys: string[]) => {
          if (keys.length > 0) {
            keys.forEach((k) => pipeline.del(k));
          }
        });
        await new Promise<void>((resolve) => {
          stream.on("end", () => {
            pipeline.exec().finally(() => resolve());
          });
        });
      } else {
        await this.redis.del(key);
      }
    } catch (err) {
      logger.warn({ err, key, pattern }, "Cache invalidate failed");
    }
  }

  /**
   * 批量失效 (如成绩发布后失效学生所有成绩缓存).
   */
  async invalidateByPrefix(prefix: string): Promise<void> {
    const pattern = `${env.REDIS_KEY_PREFIX}${prefix}*`;
    try {
      const stream = this.redis.scanStream({
        match: pattern,
        count: 100,
      });
      const pipeline = this.redis.pipeline();
      stream.on("data", (keys: string[]) => {
        if (keys.length > 0) {
          keys.forEach((k) => pipeline.del(k));
        }
      });
      await new Promise<void>((resolve) => {
        stream.on("end", () => {
          pipeline.exec().finally(() => resolve());
        });
      });
    } catch (err) {
      logger.warn({ err, prefix }, "Cache invalidateByPrefix failed");
    }
  }

  /**
   * 健康检查 (供 /readyz 探针使用, §2.4).
   */
  async ping(): Promise<boolean> {
    try {
      const result = await this.redis.ping();
      return result === "PONG";
    } catch {
      return false;
    }
  }
}
