/**
 * Notification Service
 *
 * Manages in-app notifications stored in the portal database.
 * Notifications are created in response to Salesforce CDC events
 * and displayed alongside email notifications.
 */

import {
  Injectable,
  Inject,
  BadRequestException,
  InternalServerErrorException,
} from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { PrismaService } from "@bff/infra/database/prisma.service.js";
import type { Notification as PrismaNotification } from "@prisma/client";
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
import {
  NOTIFICATION_SOURCE,
  NOTIFICATION_TEMPLATES,
  type NotificationTypeValue,
  type NotificationSourceValue,
  type Notification,
  type NotificationListResponse,
} from "@customer-portal/domain/notifications";

// Notification expiry in days
const NOTIFICATION_EXPIRY_DAYS = 30;

// Dedupe window (in hours) per notification type.
// Defaults to 1 hour when not specified.
const NOTIFICATION_DEDUPE_WINDOW_HOURS: Partial<Record<NotificationTypeValue, number>> = {
  // These are often evaluated opportunistically (e.g., on dashboard load),
  // so keep the dedupe window larger to avoid spam.
  INVOICE_DUE: 24,
  PAYMENT_METHOD_EXPIRING: 24,
  SYSTEM_ANNOUNCEMENT: 24,
};

export interface CreateNotificationParams {
  userId: string;
  type: NotificationTypeValue;
  title?: string | undefined;
  message?: string | undefined;
  actionUrl?: string | undefined;
  actionLabel?: string | undefined;
  source?: NotificationSourceValue | undefined;
  sourceId?: string | undefined;
}

@Injectable()
export class NotificationService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  /**
   * Create a notification for a user
   */
  async createNotification(params: CreateNotificationParams): Promise<Notification> {
    const template = NOTIFICATION_TEMPLATES[params.type];
    if (!template) {
      throw new BadRequestException(`Unknown notification type: ${params.type}`);
    }

    // Calculate expiry date (30 days from now)
    const expiresAt = new Date();
    expiresAt.setDate(expiresAt.getDate() + NOTIFICATION_EXPIRY_DAYS);

    try {
      // Check for duplicate notification (same type + sourceId within a short window)
      if (params.sourceId) {
        const dedupeHours = NOTIFICATION_DEDUPE_WINDOW_HOURS[params.type] ?? 1;
        const since = new Date(Date.now() - dedupeHours * 60 * 60 * 1000);

        const existingNotification = await this.prisma.notification.findFirst({
          where: {
            userId: params.userId,
            type: params.type,
            sourceId: params.sourceId,
            createdAt: { gte: since },
          },
        });

        if (existingNotification) {
          this.logger.debug("Duplicate notification detected, skipping", {
            userId: params.userId,
            type: params.type,
            sourceId: params.sourceId,
          });
          return this.mapToNotification(existingNotification);
        }
      }

      const notification = await this.prisma.notification.create({
        data: {
          userId: params.userId,
          type: params.type,
          title: params.title ?? template.title,
          message: params.message ?? template.message,
          actionUrl: params.actionUrl ?? template.actionUrl ?? null,
          actionLabel: params.actionLabel ?? template.actionLabel ?? null,
          source: params.source ?? NOTIFICATION_SOURCE.SALESFORCE,
          sourceId: params.sourceId ?? null,
          expiresAt,
        },
      });

      this.logger.log("Notification created", {
        notificationId: notification.id,
        userId: params.userId,
        type: params.type,
      });

      return this.mapToNotification(notification);
    } catch (error) {
      this.logger.error("Failed to create notification", {
        error: extractErrorMessage(error),
        userId: params.userId,
        type: params.type,
      });
      throw new InternalServerErrorException("Failed to create notification");
    }
  }

  /**
   * Get notifications for a user
   */
  async getNotifications(
    userId: string,
    options?: {
      limit?: number;
      offset?: number;
      includeRead?: boolean;
      includeDismissed?: boolean;
    }
  ): Promise<NotificationListResponse> {
    const limit = options?.limit ?? 20;
    const offset = options?.offset ?? 0;
    const now = new Date();

    const where = {
      userId,
      expiresAt: { gt: now },
      ...(options?.includeDismissed ? {} : { dismissed: false }),
      // By default we include read notifications. If includeRead=false, filter them out.
      ...(options?.includeRead === false ? { read: false } : {}),
    };

    try {
      const [notifications, total, unreadCount] = await Promise.all([
        this.prisma.notification.findMany({
          where,
          orderBy: { createdAt: "desc" },
          take: limit,
          skip: offset,
        }),
        this.prisma.notification.count({ where }),
        this.prisma.notification.count({
          where: {
            userId,
            read: false,
            dismissed: false,
            expiresAt: { gt: now },
          },
        }),
      ]);

      return {
        notifications: notifications.map((n: PrismaNotification) => this.mapToNotification(n)),
        unreadCount,
        total,
      };
    } catch (error) {
      this.logger.error("Failed to get notifications", {
        error: extractErrorMessage(error),
        userId,
      });
      throw new InternalServerErrorException("Failed to get notifications");
    }
  }

  /**
   * Get unread notification count for a user
   */
  async getUnreadCount(userId: string): Promise<number> {
    const now = new Date();

    try {
      return await this.prisma.notification.count({
        where: {
          userId,
          read: false,
          dismissed: false,
          expiresAt: { gt: now },
        },
      });
    } catch (error) {
      this.logger.error("Failed to get unread count", {
        error: extractErrorMessage(error),
        userId,
      });
      return 0;
    }
  }

  /**
   * Mark a notification as read
   */
  async markAsRead(notificationId: string, userId: string): Promise<void> {
    try {
      await this.prisma.notification.updateMany({
        where: { id: notificationId, userId },
        data: { read: true, readAt: new Date() },
      });

      this.logger.debug("Notification marked as read", {
        notificationId,
        userId,
      });
    } catch (error) {
      this.logger.error("Failed to mark notification as read", {
        error: extractErrorMessage(error),
        notificationId,
        userId,
      });
      throw new InternalServerErrorException("Failed to update notification");
    }
  }

  /**
   * Mark all notifications as read for a user
   */
  async markAllAsRead(userId: string): Promise<void> {
    try {
      const result = await this.prisma.notification.updateMany({
        where: { userId, read: false },
        data: { read: true, readAt: new Date() },
      });

      this.logger.debug("All notifications marked as read", {
        userId,
        count: result.count,
      });
    } catch (error) {
      this.logger.error("Failed to mark all notifications as read", {
        error: extractErrorMessage(error),
        userId,
      });
      throw new InternalServerErrorException("Failed to update notifications");
    }
  }

  /**
   * Dismiss a notification (hide from UI)
   */
  async dismiss(notificationId: string, userId: string): Promise<void> {
    try {
      await this.prisma.notification.updateMany({
        where: { id: notificationId, userId },
        data: { dismissed: true, read: true, readAt: new Date() },
      });

      this.logger.debug("Notification dismissed", {
        notificationId,
        userId,
      });
    } catch (error) {
      this.logger.error("Failed to dismiss notification", {
        error: extractErrorMessage(error),
        notificationId,
        userId,
      });
      throw new InternalServerErrorException("Failed to dismiss notification");
    }
  }

  /**
   * Clean up expired notifications (called by scheduled job)
   */
  async cleanupExpired(): Promise<number> {
    try {
      const result = await this.prisma.notification.deleteMany({
        where: {
          expiresAt: { lt: new Date() },
        },
      });

      if (result.count > 0) {
        this.logger.log("Cleaned up expired notifications", {
          count: result.count,
        });
      }

      return result.count;
    } catch (error) {
      this.logger.error("Failed to cleanup expired notifications", {
        error: extractErrorMessage(error),
      });
      return 0;
    }
  }

  /**
   * Map Prisma model to domain type
   */
  private mapToNotification(record: {
    id: string;
    userId: string;
    type: string;
    title: string;
    message: string | null;
    actionUrl: string | null;
    actionLabel: string | null;
    source: string;
    sourceId: string | null;
    read: boolean;
    readAt: Date | null;
    dismissed: boolean;
    createdAt: Date;
    expiresAt: Date;
  }): Notification {
    return {
      id: record.id,
      userId: record.userId,
      type: record.type as NotificationTypeValue,
      title: record.title,
      message: record.message,
      actionUrl: record.actionUrl,
      actionLabel: record.actionLabel,
      source: record.source as NotificationSourceValue,
      sourceId: record.sourceId,
      read: record.read,
      readAt: record.readAt?.toISOString() ?? null,
      dismissed: record.dismissed,
      createdAt: record.createdAt.toISOString(),
      expiresAt: record.expiresAt.toISOString(),
    };
  }
}
