import { Injectable, Inject } from "@nestjs/common";
import type { IamClient } from "../clients/iam.client.js";
import type { ChildDto } from "../clients/dtos.js";
import { ChildNotBoundError } from "../shared/errors/application-error.js";
import { env } from "../config/env.js";
import { logger } from "../shared/observability/logger.js";
import { childGuardBlocksTotal } from "../shared/observability/metrics.js";
import { safeRedis } from "../shared/cache/redis.client.js";
import { CacheKeys } from "../shared/cache/cache-key.builder.js";
import {
  cacheHitsTotal,
  cacheMissesTotal,
} from "../shared/observability/metrics.js";

/**
 * ChildGuard：DataScope=CHILDREN 越权校验（对齐 02-architecture-design.md §2.3）。
 *
 * 校验逻辑：
 * 1. 查 Redis 缓存（bff:parent:child-bindings:{parentId}，30s TTL）
 * 2. 缓存命中 → 检查 childId ∈ 绑定列表
 * 3. 缓存未命中 → 调 iam.getChildrenByParent(parentId)（singleflight 防击穿）
 * 4. 写入缓存 + 检查 childId
 * 5. 不在列表 → 抛 ChildNotBoundError(403) + 记录 metrics
 *
 * singleflight：同一 parentId 的并发请求只调 iam 1 次，
 * 其余等待 Promise 复用（02 §14 #5）。
 */
@Injectable()
export class ChildGuard {
  private readonly iamClient: IamClient;
  /** singleflight：parentId → in-flight Promise<ChildDto[]> */
  private readonly inflight = new Map<string, Promise<ChildDto[]>>();

  constructor(@Inject("IAM_CLIENT") iamClient: IamClient) {
    this.iamClient = iamClient;
  }

  /**
   * 校验家长是否有权访问指定孩子。
   *
   * @throws ChildNotBoundError 如果 childId 不在家长绑定列表
   */
  async validateChildAccess(parentId: string, childId: string): Promise<void> {
    const children = await this.getBoundChildren(parentId);
    const isBound = children.some((c) => c.id === childId);

    if (!isBound) {
      childGuardBlocksTotal.inc();
      logger.warn(
        {
          parentId,
          requestedChildId: childId,
          boundChildren: children.map((c) => c.id),
        },
        "ChildGuard access denied",
      );
      throw new ChildNotBoundError(
        parentId,
        childId,
        children.map((c) => c.id),
      );
    }
  }

  /**
   * 获取家长绑定的孩子列表（带缓存 + singleflight）。
   */
  async getBoundChildren(parentId: string): Promise<ChildDto[]> {
    const cacheKey = CacheKeys.childBindings(parentId);

    // 1. 尝试 Redis 缓存
    const cached = await safeRedis(
      (client) => client.get(cacheKey),
      null as string | null,
    );

    if (cached) {
      cacheHitsTotal.inc({ cache_key_pattern: "child-bindings" });
      try {
        return JSON.parse(cached) as ChildDto[];
      } catch {
        // JSON 解析失败，继续走下游
      }
    }

    cacheMissesTotal.inc({ cache_key_pattern: "child-bindings" });

    // 2. Singleflight：检查是否有 in-flight 请求
    const existing = this.inflight.get(parentId);
    if (existing) {
      return existing;
    }

    // 3. 发起 iam 调用
    const promise = this.iamClient.getChildrenByParent(parentId).finally(() => {
      this.inflight.delete(parentId);
    });

    this.inflight.set(parentId, promise);

    const children = await promise;

    // 4. 写入缓存（30s TTL）
    await safeRedis(
      (client) =>
        client.set(
          cacheKey,
          JSON.stringify(children),
          "EX",
          env.CHILD_GUARD_CACHE_TTL_SECONDS,
        ),
      undefined,
    );

    return children;
  }
}
