import { Controller, Get, HttpCode, HttpStatus, Inject } from "@nestjs/common";
import type { IamClient } from "../../clients/iam.client.js";
import type { CoreEduClient } from "../../clients/core-edu.client.js";
import type { DataAnaClient } from "../../clients/data-ana.client.js";
import { safeRedis } from "../cache/redis.client.js";
import { logger } from "../observability/logger.js";

const SERVICE_NAME = "parent-bff";
const PROBE_TIMEOUT_MS = 1000;

interface ProbeResult {
  status: "up" | "down";
  latency_ms: number;
  error?: string;
}

interface ReadyzResponse {
  status: "ready" | "degraded";
  service: string;
  timestamp: string;
  checks: Record<string, ProbeResult>;
}

/**
 * 健康检查端点（对齐 02-architecture-design.md §6.6 + workline P4.7）。
 *
 * - GET /healthz：liveness，仅返回进程存活（不检查下游）。
 * - GET /readyz：readiness，探针 4 项依赖：
 *   iam gRPC + core-edu gRPC + data-ana gRPC + Redis，超时 1s/服务。
 *   任一失败返回 503 + status=degraded。
 *
 * 不需要鉴权，必须在路由白名单中放行。
 */
@Controller()
export class HealthController {
  constructor(
    @Inject("IAM_CLIENT") private readonly iamClient: IamClient,
    @Inject("CORE_EDU_CLIENT") private readonly coreEduClient: CoreEduClient,
    @Inject("DATA_ANA_CLIENT") private readonly dataAnaClient: DataAnaClient,
  ) {}

  @Get("healthz")
  liveness(): { status: string; service: string; timestamp: string } {
    return {
      status: "ok",
      service: SERVICE_NAME,
      timestamp: new Date().toISOString(),
    };
  }

  @Get("readyz")
  @HttpCode(HttpStatus.OK)
  async readiness(): Promise<ReadyzResponse> {
    const checks: Record<string, ProbeResult> = {};

    // 并行探针 4 项依赖
    const [iam, coreEdu, dataAna, redis] = await Promise.all([
      this.probeIam(),
      this.probeCoreEdu(),
      this.probeDataAna(),
      this.probeRedis(),
    ]);

    checks["iam"] = iam;
    checks["core-edu"] = coreEdu;
    checks["data-ana"] = dataAna;
    checks["redis"] = redis;

    const allUp = Object.values(checks).every((c) => c.status === "up");

    return {
      status: allUp ? "ready" : "degraded",
      service: SERVICE_NAME,
      timestamp: new Date().toISOString(),
      checks,
    };
  }

  private async probeIam(): Promise<ProbeResult> {
    return this.withTimeout("iam", async () => {
      await this.iamClient.getUserInfo("healthcheck");
    });
  }

  private async probeCoreEdu(): Promise<ProbeResult> {
    return this.withTimeout("core-edu", async () => {
      await this.coreEduClient.listGradesByStudent("healthcheck");
    });
  }

  private async probeDataAna(): Promise<ProbeResult> {
    return this.withTimeout("data-ana", async () => {
      await this.dataAnaClient.getStudentWeakness("healthcheck", "");
    });
  }

  private async probeRedis(): Promise<ProbeResult> {
    const startedAt = Date.now();
    try {
      const result = await Promise.race([
        safeRedis((client) => client.ping(), "PONG"),
        this.timeoutPromise(),
      ]);
      if (result !== "PONG") {
        return {
          status: "down",
          latency_ms: Date.now() - startedAt,
          error: `unexpected ping response: ${result}`,
        };
      }
      return { status: "up", latency_ms: Date.now() - startedAt };
    } catch (err) {
      return {
        status: "down",
        latency_ms: Date.now() - startedAt,
        error: err instanceof Error ? err.message : String(err),
      };
    }
  }

  /**
   * 通用探针：执行 fn 并在 PROBE_TIMEOUT_MS 内完成。
   */
  private async withTimeout(
    name: string,
    fn: () => Promise<unknown>,
  ): Promise<ProbeResult> {
    const startedAt = Date.now();
    try {
      await Promise.race([fn(), this.timeoutPromise()]);
      return { status: "up", latency_ms: Date.now() - startedAt };
    } catch (err) {
      const error = err instanceof Error ? err.message : String(err);
      logger.warn({ probe: name, err }, "Ready probe failed");
      return {
        status: "down",
        latency_ms: Date.now() - startedAt,
        error,
      };
    }
  }

  private timeoutPromise(): Promise<never> {
    return new Promise((_, reject) =>
      setTimeout(
        () => reject(new Error(`timeout after ${PROBE_TIMEOUT_MS}ms`)),
        PROBE_TIMEOUT_MS,
      ),
    );
  }
}
