/**
 * Kafka EventSubscriber - P5 事件订阅 + 推送通道.
 *
 * 仲裁依据:
 *   - coord-final-decisions §2 B7 (P2-P4 不订阅 Kafka, P5 后订阅)
 *   - president-final-rulings §2.4 (/readyz 软失败: Kafka 失败仅告警)
 *   - workline §5.4 (P5.4 Kafka EventSubscriber)
 *
 * 订阅 topic (G16 命名规范: edu.<domain>.<aggregate>.<action>):
 *   - edu.teaching.homework.assigned   教师布置作业
 *   - edu.teaching.homework.graded     作业批改完成
 *   - edu.teaching.exam.published      考试发布
 *   - edu.teaching.exam.updated        考试更新
 *   - edu.teaching.grade.recorded      成绩录入
 *   - edu.identity.user.role_changed   学生角色变更
 *   - edu.notification.sent            通知发送
 *
 * 消费动作:
 *   1. 失效相关 Redis 缓存 (student:grades:* / student:exams:* 等)
 *   2. 调用 push-gateway POST /push/user/:userId 推送给学生
 *
 * 幂等性: Redis SETNX event_id 去重 (workline §5.4)
 */
import {
  Injectable,
  OnModuleDestroy,
  OnModuleInit,
  Inject,
} from "@nestjs/common";
import { Kafka, type Consumer, type EachMessagePayload } from "kafkajs";
import promClient from "prom-client";
import { REDIS_CLIENT, CacheService } from "../../shared/cache/cache.module.js";
import type { Redis } from "ioredis";
import { env } from "../../config/env.js";
import { logger } from "../../shared/observability/logger.js";
import { metricsRegistry } from "../../shared/observability/metrics.js";
import { PushGatewayService } from "../push/push-gateway.service.js";

/**
 * 订阅的 topic 列表 (G16 命名规范).
 */
const SUBSCRIBED_TOPICS = [
  "edu.teaching.homework.assigned",
  "edu.teaching.homework.graded",
  "edu.teaching.exam.published",
  "edu.teaching.exam.updated",
  "edu.teaching.grade.recorded",
  "edu.identity.user.role_changed",
  "edu.notification.sent",
] as const;

/**
 * 事件消息结构 (Kafka JSON payload).
 */
interface StudentBffEvent {
  event_id: string;
  event_type: string;
  student_id?: string;
  user_id?: string;
  payload: unknown;
  timestamp: string;
}

@Injectable()
export class EventSubscriberService implements OnModuleInit, OnModuleDestroy {
  private consumer: Consumer | null = null;
  private readonly cacheService: CacheService;
  private running = false;

  constructor(
    @Inject(REDIS_CLIENT) private readonly redis: Redis,
    private readonly pushGateway: PushGatewayService,
  ) {
    this.cacheService = new CacheService(redis);
  }

  async onModuleInit(): Promise<void> {
    // P5 才订阅, P3/P4 跳过 (B7 裁决)
    // 但 P3 已落地代码, 通过环境变量控制是否启动
    if (env.NODE_ENV === "test") {
      logger.info("EventSubscriber skipped in test environment");
      return;
    }

    try {
      const kafka = new Kafka({
        clientId: env.KAFKA_CLIENT_ID,
        brokers: env.KAFKA_BROKERS.split(",").map((b) => b.trim()),
      });

      this.consumer = kafka.consumer({ groupId: env.KAFKA_CONSUMER_GROUP });
      await this.consumer.connect();

      for (const topic of SUBSCRIBED_TOPICS) {
        await this.consumer.subscribe({ topic, fromBeginning: false });
      }

      this.running = true;
      await this.consumer.run({
        eachMessage: async (payload) => this.handleMessage(payload),
      });

      logger.info(
        { topics: SUBSCRIBED_TOPICS, group: env.KAFKA_CONSUMER_GROUP },
        "Kafka EventSubscriber started",
      );
    } catch (err) {
      // president §2.4 软失败: Kafka 失败不阻塞启动
      logger.error(
        { err },
        "Kafka EventSubscriber failed to start (soft failure, service continues)",
      );
    }
  }

  async onModuleDestroy(): Promise<void> {
    this.running = false;
    if (this.consumer) {
      try {
        await this.consumer.disconnect();
        logger.info("Kafka EventSubscriber disconnected");
      } catch (err) {
        logger.warn({ err }, "Error disconnecting Kafka consumer");
      }
    }
  }

  /**
   * 处理单条 Kafka 消息.
   */
  private async handleMessage(payload: EachMessagePayload): Promise<void> {
    const { topic, partition, message } = payload;
    const eventStr = message.value?.toString("utf-8");
    if (!eventStr) {
      logger.warn(
        { topic, partition, offset: message.offset },
        "Empty Kafka message",
      );
      return;
    }

    try {
      const event = JSON.parse(eventStr) as StudentBffEvent;
      logger.debug(
        { topic, eventId: event.event_id, eventType: event.event_type },
        "Kafka event received",
      );

      // 幂等性: Redis SETNX event_id 去重
      const dedupeKey = `student:event:dedupe:${event.event_id}`;
      const set = await this.redis.set(dedupeKey, "1", "EX", 86400, "NX");
      if (set !== "OK") {
        logger.debug(
          { eventId: event.event_id },
          "Kafka event already processed (deduped)",
        );
        return;
      }

      // 指标记录
      (
        metricsRegistry.getSingleMetric("student_bff_event_consumed_total") as
          promClient.Counter<string> | undefined
      )?.inc({ topic, event_type: event.event_type });

      // 失效相关缓存 + 推送给学生
      const studentId = event.student_id ?? event.user_id;
      if (studentId) {
        await this.invalidateCache(topic, studentId);
        await this.pushGateway.pushToStudent(
          studentId,
          topic,
          event.event_type,
          event.payload,
          event.timestamp,
        );
      }
    } catch (err) {
      logger.error(
        { err, topic, partition, offset: message.offset },
        "Failed to process Kafka event",
      );
    }
  }

  /**
   * 按 topic 失效相关缓存.
   */
  private async invalidateCache(
    topic: string,
    studentId: string,
  ): Promise<void> {
    try {
      if (topic.startsWith("edu.teaching.homework")) {
        await this.cacheService.invalidate("homework", studentId);
        await this.cacheService.invalidate("dashboard", studentId);
      } else if (topic.startsWith("edu.teaching.exam")) {
        await this.cacheService.invalidateByPrefix(`exams:${studentId}`);
        await this.cacheService.invalidate("dashboard", studentId);
      } else if (topic.startsWith("edu.teaching.grade")) {
        await this.cacheService.invalidateByPrefix(`grades:${studentId}`);
        await this.cacheService.invalidate("dashboard", studentId);
      } else if (topic === "edu.identity.user.role_changed") {
        await this.cacheService.invalidate("viewports", studentId);
        await this.cacheService.invalidate("dashboard", studentId);
      } else if (topic === "edu.notification.sent") {
        await this.cacheService.invalidateByPrefix(
          `notifications:${studentId}`,
        );
      }
    } catch (err) {
      logger.warn({ err, topic, studentId }, "Cache invalidation failed");
    }
  }
}
