import { Injectable } from "@nestjs/common";
import { createId } from "@paralleldrive/cuid2";
import { getDb } from "../config/database.js";
import { notificationDeliveries } from "../notifications/notifications.schema.js";
import type { NotificationChannel } from "../notifications/notifications.schema.js";
import { logger } from "../shared/observability/logger.js";
import { inAppChannel } from "./in-app.channel.js";
import { emailChannel } from "./email.channel.js";
import { smsChannel } from "./sms.channel.js";
import { pushChannel } from "./push.channel.js";
import type {
  ChannelSendContext,
  ChannelSendResult,
  NotificationChannelStrategy,
} from "./channel.types.js";

/**
 * ChannelDispatcher —— 多渠道分发编排器。
 *
 * 仲裁依据 02-architecture-design.md §2.4：
 * 1. in_app 渠道总是发送（保证站内信可见）
 * 2. 其他渠道按 NotificationPreference.channels 配置启用/禁用
 * 3. 所有渠道并行发送、软失败，结果记录到 msg_notification_deliveries
 *
 * 渠道注册表：新增渠道只需实现 NotificationChannelStrategy 并注册到此。
 */
const CHANNEL_REGISTRY: Record<
  NotificationChannel,
  NotificationChannelStrategy
> = {
  in_app: inAppChannel,
  email: emailChannel,
  sms: smsChannel,
  push: pushChannel,
  wechat: {
    // wechat 渠道预留（未实现），直接返回未配置
    name: "wechat" as const,
    async send(ctx: ChannelSendContext): Promise<ChannelSendResult> {
      logger.debug(
        { userId: ctx.userId, notificationId: ctx.notificationId },
        "WeChat channel not implemented (future channel)",
      );
      return {
        channel: "wechat",
        sent: false,
        error: "WeChat channel not implemented",
      };
    },
  } as NotificationChannelStrategy,
};

/** 默认渠道（无偏好配置时） */
const DEFAULT_CHANNELS: NotificationChannel[] = ["in_app"];

@Injectable()
export class ChannelDispatcherService {
  /**
   * 根据用户偏好 fan-out 到启用的渠道。
   *
   * @param ctx 发送上下文
   * @param enabledChannels 用户偏好的启用渠道列表（可为 null=用默认）
   * @returns 每个渠道的发送结果
   */
  async dispatch(
    ctx: ChannelSendContext,
    enabledChannels: NotificationChannel[] | null,
  ): Promise<ChannelSendResult[]> {
    // 确定要发送的渠道列表：in_app 总是包含
    const channels = this.resolveChannels(enabledChannels);

    // 并行发送所有渠道
    const results = await Promise.allSettled(
      channels.map((ch) => this.sendToChannel(ctx, ch)),
    );

    // 收集结果，rejected 的转为 failed 结果
    // 用 channels.map 遍历保证 channel 一定存在（results 与 channels 长度一致）
    const sendResults: ChannelSendResult[] = channels.map((channel, i) => {
      const r = results[i];
      if (!r) {
        return { channel, sent: false, error: "No result" };
      }
      if (r.status === "fulfilled") {
        return r.value;
      }
      const error =
        r.reason instanceof Error ? r.reason.message : String(r.reason);
      return { channel, sent: false, error };
    });

    // 异步记录投递结果（不阻断返回）
    void this.recordDeliveries(ctx.notificationId, sendResults);

    return sendResults;
  }

  /**
   * 解析要发送的渠道列表。
   * in_app 总是包含；其他渠道按偏好配置。
   */
  private resolveChannels(
    enabledChannels: NotificationChannel[] | null,
  ): NotificationChannel[] {
    if (!enabledChannels || enabledChannels.length === 0) {
      return DEFAULT_CHANNELS;
    }
    // 确保 in_app 总在列表中
    const set = new Set<NotificationChannel>(enabledChannels);
    set.add("in_app");
    return Array.from(set);
  }

  private async sendToChannel(
    ctx: ChannelSendContext,
    channel: NotificationChannel,
  ): Promise<ChannelSendResult> {
    const strategy = CHANNEL_REGISTRY[channel];
    if (!strategy) {
      return { channel, sent: false, error: `Unknown channel: ${channel}` };
    }
    return strategy.send(ctx);
  }

  /**
   * 记录投递结果到 msg_notification_deliveries 表。
   * 软失败：记录失败不阻断主流程。
   */
  private async recordDeliveries(
    notificationId: string,
    results: ChannelSendResult[],
  ): Promise<void> {
    try {
      const db = getDb();
      const now = new Date();
      const rows = results.map((r) => ({
        id: createId(),
        notificationId,
        channel: r.channel,
        status: r.sent ? ("sent" as const) : ("failed" as const),
        attemptCount: 1,
        lastError: r.error ?? null,
        deliveredAt: r.sent ? now : null,
      }));
      if (rows.length > 0) {
        await db.insert(notificationDeliveries).values(rows);
      }
    } catch (err) {
      logger.warn(
        { err, notificationId },
        "Failed to record deliveries (non-fatal)",
      );
    }
  }
}
