import {
  NotificationAudience,
  NotificationChannels,
  NotificationChannelsSent,
  NotificationDto,
  NotificationListResult,
  NotificationPriority,
  NotificationType,
  NotifyInput,
} from "@edr/types";
import { Injectable, Logger } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity";
import { Repository } from "typeorm";

import { EmailClientService } from "../notifications/email-client.service";
import { SmsClientService } from "../notifications/sms-client.service";
import { ListNotificationsQueryDto } from "./dto/list-notifications-query.dto";
import { Notification } from "./entities/notification.entity";
import { NotificationInboxRepository } from "./notification-inbox.repository";
import { NotificationRecipientsService } from "./notification-recipients.service";
import { NotificationsGateway } from "./notifications.gateway";

/**
 * The single entry point subsystems use for in-app notifications. Call
 * {@link notify}; everything else (reads, mark-read) backs the REST controller.
 *
 * `notify` is deliberately fault-tolerant: it never throws into the caller so a
 * notification failure can't roll back or break the business transaction that
 * triggered it. Failures are logged.
 */
@Injectable()
export class NotificationInboxService {
  private readonly logger = new Logger(NotificationInboxService.name);

  constructor(
    private readonly repo: NotificationInboxRepository,
    private readonly recipients: NotificationRecipientsService,
    private readonly gateway: NotificationsGateway,
    private readonly emailClient: EmailClientService,
    private readonly smsClient: SmsClientService,
    @InjectRepository(User)
    private readonly users: Repository<User>,
  ) {}

  /**
   * Fan a logical notification out to every resolved recipient: persist one row
   * each, push it live over WebSocket, and (for HIGH priority) also queue
   * email/SMS via the existing clients.
   */
  async notify(input: NotifyInput): Promise<void> {
    try {
      const userIds = await this.recipients.resolve(input.recipients);
      if (userIds.length === 0) {
        this.logger.debug(
          `notify(${input.type}) resolved 0 recipients — skipped`,
        );
        return;
      }
      const priority = input.priority ?? NotificationPriority.NORMAL;

      for (const userId of userIds) {
        await this.deliverToUser(userId, input, priority);
      }
    } catch (err) {
      this.logger.error(
        `notify failed: ${(err as Error).message}`,
        (err as Error).stack,
      );
    }
  }

  async list(
    userId: string,
    query: ListNotificationsQueryDto,
  ): Promise<NotificationListResult> {
    const [items, count] = await this.repo.findForRecipient(userId, {
      page: query.page,
      limit: query.limit,
      isRead: query.isRead,
    });
    const unreadCount = await this.repo.countUnread(userId);
    return { items: items.map((n) => this.toDto(n)), count, unreadCount };
  }

  async unreadCount(userId: string): Promise<{ unreadCount: number }> {
    return { unreadCount: await this.repo.countUnread(userId) };
  }

  async markRead(
    id: string,
    userId: string,
  ): Promise<{ success: boolean; unreadCount: number }> {
    const success = await this.repo.markRead(id, userId);
    const unreadCount = await this.repo.countUnread(userId);
    this.gateway.emitUnreadCount(userId, unreadCount);
    return { success, unreadCount };
  }

  async markAllRead(
    userId: string,
  ): Promise<{ updated: number; unreadCount: number }> {
    const updated = await this.repo.markAllRead(userId);
    const unreadCount = await this.repo.countUnread(userId);
    this.gateway.emitUnreadCount(userId, unreadCount);
    return { updated, unreadCount };
  }

  /** [dev/verification only] Send a canned notification straight to one user. */
  async sendTestToUser(
    userId: string,
    body: {
      audience?: NotificationAudience;
      type?: NotificationType;
      priority?: NotificationPriority;
      title?: string;
      message?: string;
    },
  ): Promise<NotificationDto> {
    const entity = await this.repo.create({
      recipientUserId: userId,
      audience: body.audience ?? NotificationAudience.BACKOFFICE,
      type: body.type ?? NotificationType.GENERIC,
      title: body.title ?? "Test notification",
      body: body.message ?? "This is a test in-app notification.",
      priority: body.priority ?? NotificationPriority.NORMAL,
      isRead: false,
    });
    const dto = this.toDto(entity);
    this.gateway.emitNew(userId, dto, await this.repo.countUnread(userId));
    return dto;
  }

  private async deliverToUser(
    userId: string,
    input: NotifyInput,
    priority: NotificationPriority,
  ): Promise<void> {
    const entity = await this.repo.create({
      recipientUserId: userId,
      audience: input.audience,
      type: input.type,
      title: input.title,
      body: input.body,
      link: input.link ?? null,
      data: input.data ?? null,
      priority,
      isRead: false,
    });

    const unreadCount = await this.repo.countUnread(userId);
    this.gateway.emitNew(userId, this.toDto(entity), unreadCount);

    const channels = this.resolveChannels(input, priority);
    if (channels.email || channels.sms) {
      const channelsSent = await this.fanOut(userId, input, channels);
      if (channelsSent) {
        await this.repo.update(entity.id, { channelsSent });
      }
    }
  }

  /**
   * Decide which outbound channels to use. An explicit `input.channels`
   * selection wins; otherwise fall back to priority (HIGH ⇒ email + SMS).
   */
  private resolveChannels(
    input: NotifyInput,
    priority: NotificationPriority,
  ): Required<NotificationChannels> {
    if (input.channels) {
      return {
        email: input.channels.email === true,
        sms: input.channels.sms === true,
      };
    }
    const high = priority === NotificationPriority.HIGH;
    return { email: high, sms: high };
  }

  /**
   * Best-effort email/SMS fan-out for the requested channels. Skips a channel
   * the recipient has no address for. Never throws.
   */
  private async fanOut(
    userId: string,
    input: NotifyInput,
    channels: Required<NotificationChannels>,
  ): Promise<NotificationChannelsSent | null> {
    try {
      const user = await this.users.findOne({
        where: { id: userId } as never,
      });
      if (!user) return null;

      const sent: NotificationChannelsSent = {};
      const text = `${input.title}\n\n${input.body}`;

      if (channels.email && user.email) {
        const res = await this.emailClient.sendEmail({
          to: user.email,
          subject: input.title,
          text,
        });
        sent.email = res.queued;
      }
      if (channels.sms && user.phoneNumber) {
        const res = await this.smsClient.sendSms({
          to: user.phoneNumber,
          message: text,
        });
        sent.sms = res.queued;
      }
      return Object.keys(sent).length ? sent : null;
    } catch (err) {
      this.logger.warn(
        `fan-out failed for user ${userId}: ${(err as Error).message}`,
      );
      return null;
    }
  }

  private toDto(n: Notification): NotificationDto {
    return {
      id: n.id,
      recipientUserId: n.recipientUserId,
      audience: n.audience,
      type: n.type,
      title: n.title,
      body: n.body,
      link: n.link ?? null,
      data: n.data ?? null,
      priority: n.priority,
      isRead: n.isRead,
      readAt: n.readAt ? new Date(n.readAt).toISOString() : null,
      createdAt: new Date(n.createdAt).toISOString(),
    };
  }
}
