import { Injectable } from "@nestjs/common";
import { createId } from "@paralleldrive/cuid2";
import { and, eq } from "drizzle-orm";
import { getDb } from "../config/database.js";
import {
  notifications,
  notificationPreferences,
  type Notification,
  type NotificationChannel,
  type NotificationStatus,
} from "./notifications.schema.js";
import * as repo from "./notifications.repository.js";
import type {
  SendNotificationDto,
  SendNotificationBatchDto,
} from "./notifications.dto.js";
import { ChannelDispatcherService } from "../channels/channel-dispatcher.service.js";
import type { ChannelSendContext } from "../channels/channel.types.js";
import { safeIndex, safeSearch, safeDelete } from "../config/elasticsearch.js";
import { publish as outboxPublish } from "../shared/outbox/outbox.service.js";
import { logger } from "../shared/observability/logger.js";

export interface SendResult {
  id: string;
  status: string;
  channels: string[];
}

export interface BatchSendResult {
  ids: string[];
  failed: { userId: string; error: string }[];
}

export interface PaginatedResult {
  items: Notification[];
  total: number;
  page: number;
  pageSize: number;
}

export interface SearchResult {
  items: Record<string, unknown>[];
  total: number;
  page: number;
  pageSize: number;
}

/**
 * NotificationService —— 通知发送编排层。
 *
 * 职责：
 * 1. 幂等检查（eventId 去重）
 * 2. 查询用户偏好
 * 3. 写入 MySQL（通知主表）
 * 4. 写入 ES 索引（降级安全）
 * 5. 调用 ChannelDispatcher fan-out 到多渠道
 * 6. 写入 Outbox（发布 notification.sent 事件）
 *
 * 仲裁依据：
 * - G10：getDb() 函数式
 * - G11：cuid2 ID
 * - 02-architecture-design.md §2.4：in_app 总是发送，其他渠道按偏好
 */
@Injectable()
export class NotificationsService {
  constructor(private readonly channelDispatcher: ChannelDispatcherService) {}

  async send(dto: SendNotificationDto): Promise<SendResult> {
    // 幂等：eventId 去重
    if (dto.eventId) {
      const existing = await repo.findByEventId(dto.eventId);
      if (existing) {
        logger.info(
          { eventId: dto.eventId, id: existing.id },
          "Notification already exists (idempotent skip)",
        );
        return {
          id: existing.id,
          status: existing.status,
          channels: [existing.channel],
        };
      }
    }

    const id = createId();
    const channel = (dto.channel ?? "in_app") as NotificationChannel;

    // 写入 MySQL
    await repo.insertNotification({
      id,
      userId: dto.userId,
      type: dto.type as Notification["type"],
      title: dto.title,
      content: dto.content,
      channel,
      isRead: false,
      status: "pending",
      metadata: dto.metadata ?? null,
      relatedEntityType: dto.relatedEntityType ?? null,
      relatedEntityId: dto.relatedEntityId ?? null,
      groupId: dto.groupId ?? null,
      senderId: dto.senderId ?? null,
      templateId: dto.templateId ?? null,
      eventId: dto.eventId ?? null,
    });

    // 写入 ES（降级安全）
    await safeIndex({
      index: "notifications",
      id,
      document: {
        id,
        user_id: dto.userId,
        type: dto.type,
        title: dto.title,
        content: dto.content,
        channel,
        status: "pending",
        group_id: dto.groupId ?? null,
        related_entity_type: dto.relatedEntityType ?? null,
        related_entity_id: dto.relatedEntityId ?? null,
        sender_id: dto.senderId ?? null,
        is_read: false,
        created_at: new Date().toISOString(),
      },
    });

    // 查询用户偏好
    const enabledChannels = await this.getUserChannels(dto.userId, dto.type);

    // 渠道分发
    const ctx: ChannelSendContext = {
      notificationId: id,
      userId: dto.userId,
      title: dto.title,
      content: dto.content,
      type: dto.type,
      metadata: dto.metadata ?? null,
      relatedEntityType: dto.relatedEntityType,
      relatedEntityId: dto.relatedEntityId,
    };
    const results = await this.channelDispatcher.dispatch(ctx, enabledChannels);

    // 更新状态为 sent
    const anySent = results.some((r) => r.sent);
    await this.updateStatus(id, anySent ? "sent" : "failed");

    // 写入 Outbox（发布 notification.sent 事件）
    await outboxPublish(
      "notification.sent",
      {
        notificationId: id,
        userId: dto.userId,
        type: dto.type,
        channel,
        channels: results.map((r) => r.channel),
      },
      {
        aggregateType: "Notification",
        aggregateId: id,
        metadata: { userId: dto.userId },
      },
    );

    return {
      id,
      status: anySent ? "sent" : "failed",
      channels: results.map((r) => r.channel),
    };
  }

  async sendBatch(dto: SendNotificationBatchDto): Promise<BatchSendResult> {
    const groupId = dto.groupId ?? createId();
    const ids: string[] = [];
    const failed: { userId: string; error: string }[] = [];

    for (const item of dto.items) {
      try {
        const result = await this.send({ ...item, groupId });
        ids.push(result.id);
      } catch (err) {
        const error = err instanceof Error ? err.message : String(err);
        failed.push({ userId: item.userId, error });
      }
    }

    return { ids, failed };
  }

  async listByUser(
    userId: string,
    options: {
      onlyUnread?: boolean;
      type?: string;
      page: number;
      pageSize: number;
    },
  ): Promise<PaginatedResult> {
    const { items, total } = await repo.listByUser(userId, options);
    return {
      items,
      total,
      page: options.page,
      pageSize: options.pageSize,
    };
  }

  async getUnreadCount(userId: string): Promise<number> {
    return repo.getUnreadCount(userId);
  }

  async markAsRead(id: string, userId: string): Promise<void> {
    await repo.markAsRead(id, userId);

    // 发布 notification.read 事件
    await outboxPublish(
      "notification.read",
      { notificationId: id, userId },
      {
        aggregateType: "Notification",
        aggregateId: id,
        metadata: { userId },
      },
    );
  }

  async batchMarkAsRead(ids: string[], userId: string): Promise<void> {
    await repo.batchMarkAsRead(ids, userId);

    // 批量发布 read 事件
    for (const id of ids) {
      await outboxPublish(
        "notification.read",
        { notificationId: id, userId },
        {
          aggregateType: "Notification",
          aggregateId: id,
          metadata: { userId },
        },
      );
    }
  }

  async markAllAsRead(userId: string, before?: number): Promise<number> {
    const updated = await repo.markAllAsRead(userId, before);

    // 发布 read 事件
    await outboxPublish(
      "notification.read",
      { userId, before: before ?? Date.now(), bulk: true },
      {
        aggregateType: "Notification",
        aggregateId: userId,
        metadata: { userId, bulk: "true" },
      },
    );

    return updated;
  }

  async search(
    userId: string,
    query: string,
    options: { type?: string; page: number; pageSize: number },
  ): Promise<SearchResult> {
    const from = (options.page - 1) * options.pageSize;
    const must: Record<string, unknown>[] = [
      { term: { user_id: userId } },
      { multi_match: { query, fields: ["title", "content"] } },
    ];
    if (options.type) {
      must.push({ term: { type: options.type } });
    }

    const result = await safeSearch(
      "notifications",
      { bool: { must } },
      from,
      options.pageSize,
    );

    return {
      items: result.hits.map((h) => h._source),
      total: result.total,
      page: options.page,
      pageSize: options.pageSize,
    };
  }

  async recall(groupId: string): Promise<number> {
    const recalled = await repo.recallByGroup(groupId);

    // 同步删除 ES 索引（撤回的通知不再可搜索）
    void this.deleteEsByGroup(groupId);

    // 发布 notification.recalled 事件
    await outboxPublish(
      "notification.recalled",
      { groupId, recalledCount: recalled },
      {
        aggregateType: "Notification",
        aggregateId: groupId,
        metadata: { groupId },
      },
    );

    return recalled;
  }

  async delete(id: string): Promise<void> {
    await repo.deleteById(id);
    await safeDelete("notifications", id);
  }

  // ============================================================
  // 内部方法
  // ============================================================

  private async getUserChannels(
    userId: string,
    type: string,
  ): Promise<NotificationChannel[] | null> {
    const db = getDb();
    const [pref] = await db
      .select()
      .from(notificationPreferences)
      .where(
        and(
          eq(notificationPreferences.userId, userId),
          eq(notificationPreferences.type, type as Notification["type"]),
          eq(notificationPreferences.enabled, true),
        ),
      )
      .limit(1);
    return pref?.channels ?? null;
  }

  private async updateStatus(
    id: string,
    status: NotificationStatus,
  ): Promise<void> {
    const db = getDb();
    await db
      .update(notifications)
      .set({ status })
      .where(eq(notifications.id, id));
  }

  private async deleteEsByGroup(groupId: string): Promise<void> {
    // ES 删除按 group_id 查询后逐条删除（ES 无批量 delete by query 在 safeDelete 封装中）
    // P5 简化：仅记录日志，实际清理走后台任务
    logger.info(
      { groupId },
      "ES cleanup for recalled notifications (deferred)",
    );
  }
}
