import { Injectable, Logger } from '@nestjs/common';
import { RedisService } from '@core/cache/redis/redis.service';

/**
 * 紧急豁免机制（规则 §5.6 实施要求 P2 - 紧急豁免）
 *
 * 场景：生产 PermissionsGuard 出现 bug 大面积 403，需要运维快速降级。
 * 设计：Redis 白名单 + 4h 自动过期，避免"永久降级"变成隐藏后门。
 *
 * 使用：
 *   // 运维工具通过 Redis CLI 或专用 API 启用
 *   emergencyBypass.enableFor('POST /users', 'incident-2026-04-25', 3600);
 *   // PermissionsGuard 遇到命中的端点直接放行 + 强制审计日志
 *
 * 自我约束：
 * - 豁免最长 4 小时（TTL 硬限制），需人工再启用才能续命
 * - 每次启用必带 reason，进入 IamAuditLog
 * - 豁免状态命中时走审计日志记录"endpoint X 被豁免"，不隐身
 */
type BypassInfo = { reason: string; enabledAt: string; expiresAt: string };

@Injectable()
export class EmergencyBypassService {
  private readonly logger = new Logger(EmergencyBypassService.name);
  private readonly MAX_TTL_SEC = 4 * 60 * 60; // 4h

  // 进程内 mini-cache：豁免不命中是热路径常态（每请求 1 次 Redis GET 是可观察开销）。
  // 短 TTL 保证启用/禁用感知延迟可控；命中时会回查 Redis 拿真实 expiresAt。
  private readonly NEGATIVE_CACHE_TTL_MS = 5_000; // 5s
  private readonly negativeCache = new Map<string, number>(); // endpoint -> 失效时间戳

  constructor(private readonly redis: RedisService) {}

  private key(endpoint: string): string {
    return `emergency_bypass:${endpoint}`;
  }

  /**
   * 查询端点是否被豁免（PermissionsGuard 每请求调用）
   * 优先使用 getBypassInfo —— 单次 Redis GET 既判定也拿到 reason / expiresAt。
   */
  async isBypassed(endpoint: string): Promise<boolean> {
    return (await this.getBypassInfo(endpoint)) !== null;
  }

  /**
   * 启用豁免
   * @param endpoint 'METHOD PATH' 格式，如 'POST /api/v1/users'
   * @param reason 事故单号或原因（必填）
   * @param ttlSec 时长（秒），不得超过 4 小时
   */
  async enableFor(
    endpoint: string,
    reason: string,
    ttlSec: number,
  ): Promise<void> {
    if (!reason?.trim()) {
      throw new Error('紧急豁免必须提供 reason');
    }
    if (ttlSec > this.MAX_TTL_SEC) {
      throw new Error(
        `紧急豁免 TTL 最长 ${this.MAX_TTL_SEC}s（4h），不接受更长`,
      );
    }
    await this.redis.setJson(this.key(endpoint), ttlSec, {
      reason,
      enabledAt: new Date().toISOString(),
      expiresAt: new Date(Date.now() + ttlSec * 1000).toISOString(),
    });
    this.negativeCache.delete(endpoint);
    this.logger.warn(
      `🚨 紧急豁免启用 endpoint="${endpoint}" reason="${reason}" ttl=${ttlSec}s`,
    );
  }

  async disable(endpoint: string): Promise<void> {
    await this.redis.del(this.key(endpoint));
    this.negativeCache.delete(endpoint);
    this.logger.log(`紧急豁免解除 endpoint="${endpoint}"`);
  }

  /**
   * 列出当前所有生效的豁免（IAM 后台用）。
   * 用 Redis SCAN 扫 `emergency_bypass:*`，本场景上限 < 100 条 endpoint。
   */
  async listActive(): Promise<Array<BypassInfo & { endpoint: string }>> {
    const client = this.redis.getClient();
    const prefix = 'emergency_bypass:';
    const out: Array<BypassInfo & { endpoint: string }> = [];
    let cursor = '0';
    do {
      const [next, keys] = await client.scan(
        cursor,
        'MATCH',
        `${prefix}*`,
        'COUNT',
        200,
      );
      cursor = next;
      if (keys.length === 0) continue;
      const values = await client.mget(...keys);
      values.forEach((raw, i) => {
        if (!raw) return;
        try {
          const info = JSON.parse(raw) as BypassInfo;
          out.push({ endpoint: keys[i].slice(prefix.length), ...info });
        } catch {
          this.logger.warn(`emergency_bypass key ${keys[i]} 不是合法 JSON，已跳过`);
        }
      });
    } while (cursor !== '0');
    return out;
  }

  async getBypassInfo(endpoint: string): Promise<BypassInfo | null> {
    // 负缓存命中：跳过 Redis GET（豁免不命中是 99.99% 路径，每请求省 1 次 RTT）
    const expireAt = this.negativeCache.get(endpoint);
    if (expireAt !== undefined && expireAt > Date.now()) {
      return null;
    }
    const info = await this.redis.getJson<BypassInfo>(this.key(endpoint));
    if (info === null) {
      this.negativeCache.set(endpoint, Date.now() + this.NEGATIVE_CACHE_TTL_MS);
    } else {
      // 命中真实豁免时清掉负缓存（之前可能误存），下次也重新查 expires
      this.negativeCache.delete(endpoint);
    }
    return info;
  }
}
