import { Injectable, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import {
  NotificationAudience,
  NotificationPriority,
  NotificationType,
  NotifyInput,
} from '@edr/types';

import { Booking } from '../bookings/entities/booking.entity';
import { NotificationsService } from '../notifications/notifications.service';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
import { resolveCompanyNotifyPhone } from '../notifications/resolve-company-phone.util';
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
import { BATCH_TIMEZONE } from './booking-batch.constants';

@Injectable()
export class BookingNotifierService {
  private readonly logger = new Logger(BookingNotifierService.name);

  constructor(
    private readonly notifications: NotificationsService,
    private readonly inbox: NotificationInboxService,
    private readonly trainSchedules: TrainSchedulesRepository,
    @InjectDataSource()
    private readonly dataSource: DataSource,
  ) {}

  /**
   * Human-readable description of a train schedule for customer messages:
   * reference (or train number) + route + departure date. Never leaks a UUID —
   * falls back to a generic phrase when the schedule can't be loaded.
   */
  private async scheduleLabel(scheduleId?: string | null): Promise<string> {
    const fallback = 'your selected train';
    if (!scheduleId) return fallback;
    try {
      const s = await this.trainSchedules.findByIdWithStations(scheduleId);
      if (!s) return fallback;
      const ref = s.reference ?? s.trainNumber ?? null;
      const route =
        s.originStation?.label && s.destinationStation?.label
          ? ` (${s.originStation.label} → ${s.destinationStation.label})`
          : '';
      const departure = s.scheduledDepartureDate
        ? `, departing ${new Date(s.scheduledDepartureDate).toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE })}`
        : '';
      return ref ? `train ${ref}${route}${departure}` : `${fallback}${route}${departure}`;
    } catch (err) {
      this.logger.warn(
        `scheduleLabel(${scheduleId}) failed: ${(err as Error).message}`,
      );
      return fallback;
    }
  }

  private ref(b: Booking): string {
    return `${b.reference}${b.isGovernment ? ' (gov)' : ''}`;
  }

  private async notifyContact(
    b: Booking,
    message: string,
    logLabel: string,
  ): Promise<void> {
    this.logger.log(`${logLabel} — ${this.ref(b)}`);
    const phone = b.companyId
      ? await resolveCompanyNotifyPhone(this.dataSource, b.companyId)
      : null;
    const email = b.company?.email ?? b.company?.generalManagerEmail ?? null;

    if (phone) {
      try {
        await this.notifications.directSend('sms', phone, message);
      } catch (err) {
        this.logger.warn(`SMS failed for ${this.ref(b)}: ${(err as Error).message}`);
      }
    }
    if (email) {
      try {
        await this.notifications.directSend('email', email, message);
      } catch (err) {
        this.logger.warn(`Email failed for ${this.ref(b)}: ${(err as Error).message}`);
      }
    }
    if (!phone && !email) {
      this.logger.warn(`No contact on file for ${this.ref(b)} — notification not sent`);
    }
  }

  /** Persist + push an in-app item to all portal users of the booking's company. */
  private inApp(
    b: Booking,
    title: string,
    body: string,
    overrides: Partial<NotifyInput> = {},
  ): void {
    if (!b.companyId) return; // government/unlinked bookings have no portal users
    void this.inbox.notify({
      recipients: { companyId: b.companyId },
      audience: NotificationAudience.PORTAL,
      type: NotificationType.SCHEDULE_UPDATE,
      title,
      body,
      link: `/bookings/${b.id}`,
      data: { bookingId: b.id, reference: b.reference },
      ...overrides,
    });
  }

  /** Train carrying the booking departed — dispatched origin → destination. */
  dispatched(b: Booking, origin: string | null, destination: string | null): void {
    const msg =
      `Your booking ${b.reference ?? b.id} has been dispatched` +
      `${origin || destination ? ` from ${origin ?? '?'} to ${destination ?? '?'}` : ''}.`;
    void this.notifyContact(b, msg, 'DISPATCHED');
    this.inApp(b, 'Shipment dispatched', msg);
  }

  /** Train carrying the booking arrived at destination. */
  arrived(b: Booking, origin: string | null, destination: string | null): void {
    const msg =
      `Your booking ${b.reference ?? b.id} has arrived` +
      `${destination ? ` at ${destination}` : ''}${origin ? ` (from ${origin})` : ''}.`;
    void this.notifyContact(b, msg, 'ARRIVED');
    this.inApp(b, 'Shipment arrived', msg);
  }

  async payNow(b: Booking, deadline: Date): Promise<void> {
    const payMinutes = Math.max(1, Math.round((deadline.getTime() - Date.now()) / 60_000));
    const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' });
    const msg = `Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to secure train slot ${b.reference ?? b.id}. Deadline: ${eat} EAT.`;
    await this.notifyContact(b, msg, 'PAY NOW');
    this.inApp(b, 'Payment window open', msg, {
      type: NotificationType.INVOICE_ISSUED,
    });
  }

  /**
   * Partial-capacity offer: only `offeredWagons` of the booking's `totalWagons` fit
   * this train. Paying accepts the split; letting the deadline pass keeps the
   * booking whole and expires it for this train.
   */
  async payNowPartial(
    b: Booking,
    deadline: Date,
    offeredWagons: number,
    totalWagons: number,
  ): Promise<void> {
    const payMinutes = Math.max(1, Math.round((deadline.getTime() - Date.now()) / 60_000));
    const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' });
    const leftover = totalWagons - offeredWagons;
    // With auto-placement on, the leftover is booked FOR the customer on another
    // train (its own invoice) — telling them to rebook it themselves would be
    // wrong. Without it, the leftover returns to the contract to rebook.
    const leftoverCopy =
      process.env.FREIGHT_AUTO_REMAINDER === 'true'
        ? `The remaining ${leftover} will be booked for you on another train, with its own invoice. `
        : `The remaining ${leftover} return${leftover === 1 ? 's' : ''} to your contract — book them yourself in a later window. `;
    const msg =
      `Only ${offeredWagons} of ${totalWagons} wagons fit the train for booking ${b.reference ?? b.id}. ` +
      `Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to accept and ship ${offeredWagons} wagon${offeredWagons === 1 ? '' : 's'} now. ` +
      leftoverCopy +
      `If you do not pay, the booking stays whole and you can rebook in the next window. Deadline: ${eat} EAT.`;
    await this.notifyContact(b, msg, 'PAY NOW (PARTIAL)');
    // HIGH: a split is a change to what the customer ordered AND a live payment
    // deadline — it must reach email/SMS, not just the portal inbox.
    this.inApp(b, 'Partial allocation offer', msg, {
      type: NotificationType.INVOICE_ISSUED,
      priority: NotificationPriority.HIGH,
    });
  }

  /**
   * The wagons that did not fit the train the customer just paid for have been
   * auto-booked as their own booking (`remainder`) — they ride another train and
   * are billed separately. Sent instead of leaving the customer to rebook.
   */
  remainderPlaced(remainder: Booking, parentReference: string): void {
    const msg =
      `The wagons left over from booking ${parentReference} have been booked as ` +
      `${remainder.reference ?? remainder.id} on another train. ` +
      `It carries its own invoice — pay it to secure that slot.`;
    void this.notifyContact(remainder, msg, 'REMAINDER BOOKED');
    this.inApp(remainder, 'Leftover wagons booked', msg, {
      type: NotificationType.INVOICE_ISSUED,
      priority: NotificationPriority.HIGH,
    });
  }

  secured(b: Booking, reason: 'paid' | 'gov', scheduleId?: string | null): void {
    void (async () => {
      const label = await this.scheduleLabel(scheduleId ?? b.trainScheduleId);
      const msg = `Booking ${b.reference ?? b.id} allocated on ${label}${
        reason === 'gov' ? ' (government)' : ''
      }.`;
      void this.notifyContact(b, msg, 'ALLOCATED');
      this.inApp(b, 'Wagon allocated', msg);
    })();
  }

  expired(b: Booking): void {
    const msg = `Payment window expired for booking ${b.reference ?? b.id}. Reschedule or cancel — no re-approval needed.`;
    void this.notifyContact(b, msg, 'EXPIRED');
    this.inApp(b, 'Payment window expired', msg);
  }

  /**
   * Every train on the booking's chosen day filled up (or no further train runs)
   * before the waiting list reached this booking — it expired unplaced. HIGH so
   * the customer hears about it by email/SMS and rebooks another day.
   */
  expiredNoCapacity(b: Booking): void {
    const msg =
      `Booking ${b.reference ?? b.id} could not be placed: every train for your selected day ` +
      `is full and no other train is scheduled that day. The booking has expired — ` +
      `please rebook for another day. No re-approval is needed.`;
    void this.notifyContact(b, msg, 'EXPIRED (NO CAPACITY)');
    this.inApp(b, 'No capacity — booking expired', msg, {
      priority: NotificationPriority.HIGH,
    });
  }

  scheduleFull(b: Booking): void {
    this.logger.warn(
      `SCHEDULE FULL — ${this.ref(b)} could not be placed; change schedule, pick another day, or cancel.`,
    );
  }

  /**
   * Staff-facing warning when a pooled booking fits no train on its chosen day.
   * It stays pending and is retried next batch; staff can add capacity or pin it
   * to a train manually. Mirrors {@link scheduleFull} — no customer notification.
   */
  unplaced(b: Booking, day: string): void {
    this.logger.warn(
      `UNPLACED — ${this.ref(b)} could not be placed on any train for ${day}; add capacity or assign it manually.`,
    );
  }

  displaced(b: Booking): void {
    const msg = `Booking ${b.reference ?? b.id} was displaced by a government booking. Move to another schedule or cancel.`;
    void this.notifyContact(b, msg, 'DISPLACED');
    this.inApp(b, 'Booking displaced', msg);
  }

  /**
   * Staff rescheduled the train carrying this booking to a new departure date.
   * The booking stays on the train — only the date moved.
   */
  rescheduled(b: Booking, newDeparture: Date): void {
    const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE });
    const msg = `Booking ${b.reference ?? b.id} has been rescheduled. New departure date: ${when}.`;
    void this.notifyContact(b, msg, 'RESCHEDULED');
    this.inApp(b, 'Booking rescheduled', msg);
  }

  /**
   * Booking was removed from its train during a staff reschedule (not a government
   * pre-empt). It returns to eligible — the customer must rebook or reschedule.
   */
  removedFromTrain(b: Booking): void {
    const msg =
      `Booking ${b.reference ?? b.id} has been removed from its train during rescheduling. ` +
      `Please rebook or select a new schedule from the portal.`;
    void this.notifyContact(b, msg, 'REMOVED FROM TRAIN');
    this.inApp(b, 'Removed from train', msg);
  }

  /**
   * The train carrying this booking was moved for maintenance to a new departure
   * date. The booking stays on the train — only the date moved.
   */
  maintenanceMoved(b: Booking, newDeparture: Date): void {
    const when = newDeparture.toLocaleDateString('en-GB', { timeZone: BATCH_TIMEZONE });
    const msg =
      `The train for booking ${b.reference ?? b.id} was rescheduled for maintenance. ` +
      `New departure date: ${when}.`;
    void this.notifyContact(b, msg, 'MAINTENANCE RESCHEDULE');
    this.inApp(b, 'Train maintenance reschedule', msg);
  }
}
