import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { PrismaService } from '../../common/prisma.service';
import { PushAdapter, NotificationChannel } from './notification.adapters';
import { EmailClientService } from './email-client.service';
import { SmsClientService } from './sms-client.service';

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 emailClient: EmailClientService,
    private smsClient: SmsClientService,
    private pushAdapter: PushAdapter,
  ) {
    this.channels = new Map<NotificationChannelType, NotificationChannel>([
      ['EMAIL', { send: (to, subject, body) => this.emailClient.sendEmail({ to, subject, text: body }).then((r) => r.queued) }],
      ['SMS', { send: (to, _subject, body) => this.smsClient.sendSms({ to, message: body }).then((r) => r.queued) }],
      ['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<{ queued: 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 { queued: false, channels: [] };
    }

    const { subject, body } = this.interpolate(template, context);

    // Channel resolution: explicit argument wins; otherwise honor the template's declared
    // channel(s); otherwise fall back to the recipient's preferences.
    let targetChannels: NotificationChannelType[];
    if (channels) {
      targetChannels = channels;
    } else if (template.channel) {
      targetChannels = this.parseTemplateChannels(template.channel);
    } else {
      targetChannels = await this.getUserPreferredChannels(recipient);
    }

    // Channels successfully handed off (in-app persisted / email+SMS enqueued to RabbitMQ).
    // NOTE: enqueue is fire-and-forget — this is NOT a delivery confirmation.
    const queuedChannels: string[] = [];

    if (targetChannels.includes('IN_APP')) {
      await this.createInAppNotification(recipient, subject, body, context);
      queuedChannels.push('IN_APP');
    }

    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 queued = await adapter.send(recipientAddress, subject, body, context);
      if (queued) {
        queuedChannels.push(channelType);
      }
    }

    return { queued: queuedChannels.length > 0, channels: queuedChannels };
  }

  /**
   * Parses a template's `channel` column (e.g. "EMAIL" or "EMAIL,SMS") into valid channel
   * types, always including IN_APP so an in-app record is created.
   */
  private parseTemplateChannels(channel: string): NotificationChannelType[] {
    const valid: NotificationChannelType[] = ['EMAIL', 'SMS', 'PUSH', 'IN_APP'];
    const parsed = channel
      .split(',')
      .map((c) => c.trim().toUpperCase())
      .filter((c): c is NotificationChannelType => valid.includes(c as NotificationChannelType));
    return Array.from(new Set<NotificationChannelType>(['IN_APP', ...parsed]));
  }

  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 } {
    return {
      subject: this.applyVars(template.subject || 'Notification', context),
      body: this.applyVars(template.bodyTemplate, context),
    };
  }

  /** Replaces {{variable}} placeholders in a string with values from the context. */
  private applyVars(text: string, context: Record<string, unknown>): string {
    let out = text;
    for (const [key, value] of Object.entries(context)) {
      const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
      out = out.replace(regex, String(value));
    }
    return out;
  }

  private async getUserPreferredChannels(recipient: string): Promise<NotificationChannelType[]> {
    const user = await this.prisma.user.findFirst({
      where: {
        OR: [
          { id: recipient },
          { email: recipient },
          { phone: recipient },
          { passenger: { id: 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 },
          { passenger: { id: 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;
    }
  }

  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) {
    const booking = payload.booking;
    await this.send(
      'booking.created',
      booking.passengerId,
      {
        bookingRef: booking.bookingRef,
        amount: this.formatAmount(booking),
        currency: booking.displayCurrency ?? 'ETB',
        category: 'BOOKING',
        deepLink: `edr://bookings/${booking.bookingRef}`,
      },
      // For now, always notify the travelling passenger on every channel.
      ['IN_APP', 'EMAIL', 'SMS'],
    );
  }

  /**
   * Payment succeeded → one combined "payment successful, here is your ticket" notification.
   * Email carries the full ticket (HTML + QR); SMS is a short pointer to view it. The shallow
   * event payload is re-fetched with the relations needed to render the ticket.
   */
  @OnEvent('payment.succeeded')
  async onPaymentSucceeded(payload: any) {
    const passengerId = payload.booking.passengerId;
    const bookingId = payload.booking.id;

    const booking = await this.prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
        seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } },
      },
    });
    const ticket = await this.prisma.ticket.findUnique({ where: { bookingId } });

    const ref = booking?.bookingRef ?? payload.booking.bookingRef;
    const amount = this.formatAmount(booking ?? payload.booking);
    const currency = (booking ?? payload.booking).displayCurrency ?? 'ETB';
    const ticketUrl = `${process.env.PORTAL_URL ?? 'http://localhost:5174'}/booking/confirmation?ref=${ref}`;

    // IN_APP — always created.
    await this.createInAppNotification(
      passengerId,
      'Payment successful',
      `Your payment of ${amount} ${currency} for booking ${ref} was successful. Your ticket is ready.`,
      { category: 'PAYMENT', deepLink: `edr://tickets/${ref}` },
    );

    // Ticket not ready (generation failed/raced) — fall back to a payment-only confirmation.
    if (!ticket || !booking) {
      this.logger.warn(`payment.succeeded: ticket not ready for booking ${ref}; sending payment-only confirmation`);
      const text = `EDR: Payment of ${amount} ${currency} received for booking ${ref}. Your ticket is being prepared.`;
      await this.deliverEmail(passengerId, `Payment received — ${ref}`, text);
      await this.deliverSms(passengerId, text);
      return;
    }

    // SMS — short pointer (no HTML/QR over SMS).
    await this.deliverSms(
      passengerId,
      `EDR: Booking ${ref} confirmed, ${amount} ${currency} paid. Show ref ${ref} at the gate or view your ticket: ${ticketUrl}`,
    );

    // EMAIL — rich HTML ticket with plain-text fallback.
    await this.deliverEmail(
      passengerId,
      `Your EDR ticket — ${ref}`,
      this.buildTicketEmailText(booking, amount, currency, ticketUrl),
      this.buildTicketEmailHtml(booking, ticket, amount, currency, ticketUrl),
    );
  }

  private async deliverEmail(recipient: string, subject: string, text: string, html?: string): Promise<void> {
    const to = await this.getRecipientAddress(recipient, 'EMAIL');
    if (!to) {
      this.logger.warn(`No EMAIL address for recipient: ${recipient}`);
      return;
    }
    await this.emailClient.sendEmail({ to, subject, text, html });
  }

  private async deliverSms(recipient: string, message: string): Promise<void> {
    const to = await this.getRecipientAddress(recipient, 'SMS');
    if (!to) {
      this.logger.warn(`No SMS address for recipient: ${recipient}`);
      return;
    }
    await this.smsClient.sendSms({ to, message });
  }

  private buildTicketEmailText(booking: any, amount: string, currency: string, url: string): string {
    const s = booking.schedule ?? {};
    const dep = s.departureAt ? new Date(s.departureAt).toLocaleString('en-GB') : 'TBD';
    const passengers = (booking.seats ?? []).map((bs: any) => bs.passengerName).filter(Boolean).join(', ');
    return [
      `Booking ${booking.bookingRef} confirmed.`,
      `${s.originStation?.name ?? ''} -> ${s.destinationStation?.name ?? ''}`,
      `Train: ${s.train?.name ?? s.train?.number ?? ''}`,
      `Departs: ${dep}`,
      passengers ? `Passengers: ${passengers}` : '',
      `Total paid: ${amount} ${currency}`,
      `View your ticket: ${url}`,
    ].filter(Boolean).join('\n');
  }

  private buildTicketEmailHtml(booking: any, ticket: any, amount: string, currency: string, url: string): string {
    const s = booking.schedule ?? {};
    const fmt = (d: any) =>
      d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD';
    const seatRows = (booking.seats ?? [])
      .map((bs: any) => {
        const coach = bs.seat?.coach?.number ?? '-';
        const seatNo = bs.seat?.seatNumber ?? '-';
        const cls = bs.seat?.coach?.coachType?.name ?? '-';
        return `<tr>
          <td style="padding:8px;border-bottom:1px solid #eee;">${bs.passengerName ?? ''}</td>
          <td style="padding:8px;border-bottom:1px solid #eee;">${coach}</td>
          <td style="padding:8px;border-bottom:1px solid #eee;">${seatNo}</td>
          <td style="padding:8px;border-bottom:1px solid #eee;">${cls}</td>
        </tr>`;
      })
      .join('');

    return `<!DOCTYPE html>
<html>
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"></head>
  <body style="margin:0;font-family:Arial,Helvetica,sans-serif;color:#333;background:#f4f4f4;">
    <div style="max-width:600px;margin:0 auto;background:#fff;">
      <div style="background:#0066cc;color:#fff;padding:24px;text-align:center;">
        <h2 style="margin:0;">Ethio-Djibouti Railway</h2>
        <p style="margin:8px 0 0;">Payment successful — your ticket is ready</p>
      </div>
      <div style="padding:24px;">
        <p>Booking reference: <strong>${booking.bookingRef}</strong></p>
        <table style="width:100%;border-collapse:collapse;margin:16px 0;">
          <tr>
            <td style="padding:8px 0;color:#666;">From</td>
            <td style="padding:8px 0;text-align:right;"><strong>${s.originStation?.name ?? ''}</strong> (${s.originStation?.code ?? ''})</td>
          </tr>
          <tr>
            <td style="padding:8px 0;color:#666;">To</td>
            <td style="padding:8px 0;text-align:right;"><strong>${s.destinationStation?.name ?? ''}</strong> (${s.destinationStation?.code ?? ''})</td>
          </tr>
          <tr>
            <td style="padding:8px 0;color:#666;">Train</td>
            <td style="padding:8px 0;text-align:right;">${s.train?.name ?? s.train?.number ?? ''}</td>
          </tr>
          <tr>
            <td style="padding:8px 0;color:#666;">Departs</td>
            <td style="padding:8px 0;text-align:right;">${fmt(s.departureAt)}</td>
          </tr>
          <tr>
            <td style="padding:8px 0;color:#666;">Arrives</td>
            <td style="padding:8px 0;text-align:right;">${fmt(s.arrivalAt)}</td>
          </tr>
        </table>

        <h3 style="margin:16px 0 8px;">Passengers</h3>
        <table style="width:100%;border-collapse:collapse;">
          <tr style="text-align:left;color:#666;">
            <th style="padding:8px;border-bottom:2px solid #eee;">Name</th>
            <th style="padding:8px;border-bottom:2px solid #eee;">Coach</th>
            <th style="padding:8px;border-bottom:2px solid #eee;">Seat</th>
            <th style="padding:8px;border-bottom:2px solid #eee;">Class</th>
          </tr>
          ${seatRows}
        </table>

        <div style="text-align:center;margin:24px 0;">
          <p style="color:#666;margin:0 0 8px;">Show this QR code at the gate</p>
          <img src="${ticket.qrPayload}" alt="Ticket QR code" width="180" height="180" style="border:1px solid #eee;padding:8px;background:#fff;" />
        </div>

        <table style="width:100%;border-collapse:collapse;border-top:2px solid #eee;margin-top:16px;">
          <tr>
            <td style="padding:12px 0;font-size:16px;"><strong>Total paid</strong></td>
            <td style="padding:12px 0;font-size:16px;text-align:right;"><strong>${amount} ${currency}</strong></td>
          </tr>
        </table>

        <div style="text-align:center;margin:24px 0;">
          <a href="${url}" style="background:#0066cc;color:#fff;text-decoration:none;padding:12px 28px;border-radius:4px;display:inline-block;">View ticket</a>
        </div>
      </div>
      <div style="text-align:center;padding:20px;color:#999;font-size:12px;">
        <p style="margin:0;">© Ethio-Djibouti Railway. All rights reserved.</p>
      </div>
    </div>
  </body>
</html>`;
  }

  @OnEvent('payment.failed')
  async onPaymentFailed(payload: any) {
    const booking = payload.booking;
    await this.send(
      'payment.failed',
      booking.passengerId,
      {
        bookingRef: booking.bookingRef,
        category: 'PAYMENT',
        deepLink: `edr://bookings/${booking.bookingRef}`,
      },
      ['IN_APP', 'EMAIL', 'SMS'],
    );
  }

  @OnEvent('booking.cancelled')
  async onBookingCancelled(payload: any) {
    const booking = payload.booking;
    await this.send(
      'booking.cancelled',
      booking.passengerId,
      {
        bookingRef: booking.bookingRef,
        // refundAmount is computed in ETB minor units in BookingsService.cancel().
        refundAmount: ((payload.refundAmount ?? 0) / 100).toFixed(2),
        currency: 'ETB',
        category: 'BOOKING',
        deepLink: `edr://bookings/${booking.bookingRef}`,
      },
      ['IN_APP', 'EMAIL', 'SMS'],
    );
  }

  /**
   * Formats a booking's payable amount from minor units into a major-unit string.
   * Money is stored as integer minor units (e.g. 59600 santim) to avoid floating-point
   * drift; we divide by 100 only here, at the display edge. e.g. 59600 -> "596.00".
   */
  private formatAmount(booking: any): string {
    const minor = booking.displayTotalMinor ?? booking.totalMinor ?? 0;
    return (minor / 100).toFixed(2);
  }
}