import { createId } from "@paralleldrive/cuid2";
import { eq } from "drizzle-orm";
import { getDb } from "../../config/database.js";
import { getRedis } from "./redis.client.js";
import { logger } from "../observability/logger.js";
import { processedEvents } from "../outbox/outbox.schema.js";

/**
 * 幂等去重守卫。
 *
 * 三层防线（仲裁依据 02-architecture-design.md §3.3.1 + §5）：
 * 1. Redis SETNX（首选，高性能）：key=`msg:processed:{eventId}` TTL 7 天
 * 2. DB 唯一索引（Redis 不可用时降级）：msg_idempotency 表 event_id UNIQUE
 * 3. 业务 event_id UNIQUE INDEX（msg_notifications.event_id，最终防线）
 *
 * 调用方在处理 Kafka 消息或 HTTP send 时，先调 checkAndMark(eventId)：
 * - 返回 true → 首次处理，继续业务逻辑
 * - 返回 false → 已处理过，跳过（幂等）
 */
const REDIS_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 天

export interface IdempotencyResult {
  /** true=首次处理可继续，false=已处理过应跳过 */
  isFirst: boolean;
  /** 去重使用的 key */
  key: string;
}

/**
 * 检查并标记事件为已处理。
 *
 * 优先走 Redis SETNX；Redis 不可用时降级到 DB 唯一索引插入。
 * 两者均不可用（极端情况）返回 true 放行，由业务层 event_id UNIQUE 兜底。
 */
export async function checkAndMark(
  eventId: string,
  topic?: string,
): Promise<IdempotencyResult> {
  const key = `msg:processed:${eventId}`;

  // 1. Redis 优先
  const redis = getRedis();
  if (redis) {
    try {
      const result = await redis.set(key, "1", "EX", REDIS_TTL_SECONDS, "NX");
      if (result === "OK") {
        return { isFirst: true, key };
      }
      return { isFirst: false, key };
    } catch (err) {
      logger.warn({ err, eventId }, "Redis SETNX failed, falling back to DB");
    }
  }

  // 2. DB 降级
  try {
    const db = getDb();
    await db.insert(processedEvents).values({
      eventId,
      topic: topic ?? "unknown",
    });
    return { isFirst: true, key };
  } catch (err) {
    // 唯一索引冲突 = 已处理
    logger.debug({ eventId, err }, "Idempotency DB hit (already processed)");
    return { isFirst: false, key };
  }
}

/**
 * 仅检查不标记（用于查询是否已处理，不产生副作用）。
 */
export async function isProcessed(eventId: string): Promise<boolean> {
  const redis = getRedis();
  if (redis) {
    try {
      const exists = await redis.exists(`msg:processed:${eventId}`);
      return exists === 1;
    } catch {
      // fall through to DB
    }
  }
  const db = getDb();
  const [row] = await db
    .select({ eventId: processedEvents.eventId })
    .from(processedEvents)
    .where(eq(processedEvents.eventId, eventId))
    .limit(1);
  return row !== undefined;
}

/**
 * 生成幂等键（HTTP send 无外部 eventId 时使用）。
 * 格式：cuid2，保证全局唯一。
 */
export function generateIdempotencyKey(): string {
  return createId();
}
