import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PrismaService } from '../../common/prisma.service';
import { SendNotificationDto, NotificationCategoryEnum } from './notifications.dto';
import { EmailAdapter, SmsAdapter, PushAdapter, NotificationChannel } from './notification.adapters';

export type NotificationChannelType = 'EMAIL' | 'SMS' | 'PUSH' | 'IN_APP';

@Injectable()
export class NotificationsService {
  private readonly logger = new Logger(NotificationsService.name);
  private readonly channels: Map<NotificationChannelType, NotificationChannel>;

  constructor(
    private prisma: PrismaService,
    private emailAdapter: EmailAdapter,
    private smsAdapter: SmsAdapter,
    private pushAdapter: PushAdapter,
  ) {
    this.channels = new Map<NotificationChannelType, NotificationChannel>([
      ['EMAIL', this.emailAdapter as NotificationChannel],
      ['SMS', this.smsAdapter as NotificationChannel],
      ['PUSH', this.pushAdapter as NotificationChannel],
    ]);
  }

  /**
   * Send notification using template key and context
   * @param templateKey - Template code from NotificationTemplate table
   * @param recipient - User/Passenger ID or email/phone
   * @param context - Variables to interpolate in template
   * @param channels - Optional array of channels to use (defaults to user preferences)
   */
  async send(
    templateKey: string,
    recipient: string,
    context: Record<string, unknown>,
    channels?: NotificationChannelType[],
  ): Promise<{ sent: boolean; channels: string[] }> {
    const template = await this.prisma.notificationTemplate.findUnique({
      where: { code: templateKey },
    });

    if (!template || !template.active) {
      this.logger.warn(`Template ${templateKey} not found or inactive`);
      return { sent: false, channels: [] };
    }

    const { subject, body } = this.interpolate(template, context);
    const targetChannels = channels || await this.getUserPreferredChannels(recipient);
    const sentChannels: string[] = [];

    // Always create in-app notification
    if (targetChannels.includes('IN_APP')) {
      await this.createInAppNotification(recipient, subject, body, context);
      sentChannels.push('IN_APP');
    }

    // Send via other channels
    for (const channelType of targetChannels) {
      if (channelType === 'IN_APP') continue;

      const adapter = this.channels.get(channelType);
      if (!adapter) {
        this.logger.warn(`No adapter for channel: ${channelType}`);
        continue;
      }

      const recipientAddress = await this.getRecipientAddress(recipient, channelType);
      if (!recipientAddress) {
        this.logger.warn(`No ${channelType} address for recipient: ${recipient}`);
        continue;
      }

      const success = await adapter.send(recipientAddress, subject, body, context);
      if (success) {
        sentChannels.push(channelType);
      }
    }

    return { sent: sentChannels.length > 0, channels: sentChannels };
  }

  /**
   * Legacy method for backward compatibility
   */
  async sendDirect(dto: SendNotificationDto) {
    const notification = await this.prisma.notification.create({
      data: {
        passengerId: dto.passengerId,
        title: dto.title,
        body: dto.body,
        category: dto.category as any,
        deepLink: dto.deepLink,
        metadata: dto.metadata,
      },
    });

    const passenger = await this.prisma.passenger.findUnique({
      where: { id: dto.passengerId },
      include: { user: true },
    });

    if (passenger?.user) {
      await this.emailAdapter.send(
        passenger.user.email,
        this.sanitize(dto.title),
        this.sanitize(dto.body),
      );
    }

    return notification;
  }

  private async createInAppNotification(
    recipient: string,
    title: string,
    body: string,
    context: Record<string, unknown>,
  ): Promise<void> {
    // Try to find passenger by ID or email
    let passengerId = recipient;

    if (!recipient.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
      const user = await this.prisma.user.findFirst({
        where: {
          OR: [{ email: recipient }, { phone: recipient }],
        },
        include: { passenger: true },
      });
      if (user?.passenger) {
        passengerId = user.passenger.id;
      } else {
        this.logger.warn(`Could not find passenger for recipient: ${recipient}`);
        return;
      }
    }

    await this.prisma.notification.create({
      data: {
        passengerId,
        title,
        body,
        category: (context.category as any) || 'SYSTEM',
        deepLink: context.deepLink as string,
        metadata: context as any,
      },
    });
  }

  private interpolate(
    template: { subject?: string | null; bodyTemplate: string },
    context: Record<string, unknown>,
  ): { subject: string; body: string } {
    const subject = template.subject || 'Notification';
    let body = template.bodyTemplate;

    // Simple template interpolation: {{variable}}
    for (const [key, value] of Object.entries(context)) {
      const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
      body = body.replace(regex, String(value));
    }

    return { subject, body };
  }

  private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
    const user = await this.prisma.user.findFirst({
      where: {
        OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
      },
      include: { preferences: true },
    });

    if (!user?.preferences) {
      return ['IN_APP', 'EMAIL'];
    }

    const channels: NotificationChannelType[] = ['IN_APP'];
    if (user.preferences.emailEnabled) channels.push('EMAIL');
    if (user.preferences.smsEnabled) channels.push('SMS');
    if (user.preferences.pushEnabled) channels.push('PUSH');

    return channels;
  }

  private async getRecipientAddress(
    recipient: string,
    channel: NotificationChannelType,
  ): Promise<string | null> {
    const user = await this.prisma.user.findFirst({
      where: {
        OR: [{ id: recipient }, { email: recipient }, { phone: recipient }],
      },
    });

    if (!user) return null;

    switch (channel) {
      case 'EMAIL':
        return user.email;
      case 'SMS':
        return user.phone;
      case 'PUSH':
        // Would need to fetch device push token
        return user.id;
      default:
        return null;
    }
  }

  private sanitize(value: string): string {
    return value
      .replace(/[\r\n]/g, ' ')
      .replace(/[<>&"']/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', "'": '&#x27;' }[c] ?? c));
  }

  getForPassenger(passengerId: string) {
    return this.prisma.notification.findMany({
      where: { passengerId },
      orderBy: { createdAt: 'desc' },
      take: 50,
    });
  }

  markRead(id: string) {
    return this.prisma.notification.update({ where: { id }, data: { read: true } });
  }

  async markAllRead(passengerId: string) {
    await this.prisma.notification.updateMany({
      where: { passengerId, read: false },
      data: { read: true },
    });
    return { updated: true };
  }

  @OnEvent('booking.created')
  async onBookingCreated(payload: any) {
    await this.send(
      'booking.created',
      payload.booking.passengerId,
      {
        bookingRef: payload.booking.bookingRef,
        category: 'BOOKING',
        deepLink: `edr://bookings/${payload.booking.bookingRef}`,
      },
    );
  }

  @OnEvent('payment.succeeded')
  async onPaymentSucceeded(payload: any) {
    await this.send(
      'payment.succeeded',
      payload.booking.passengerId,
      {
        bookingRef: payload.booking.bookingRef,
        category: 'PAYMENT',
        deepLink: `edr://tickets/${payload.booking.bookingRef}`,
      },
    );
  }
}