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

import { Booking } from './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';

/**
 * Customer + staff notifications for the booking lifecycle: review, clearance
 * and operation flow. Every customer event fans out over SMS + email (direct)
 * and a persisted in-app notification deep-linking to the booking detail page;
 * staff events land in the backoffice inbox. All sends are fire-and-forget and
 * never throw — a notification failure must not break a booking transition.
 *
 * NOTE: the batch/payment-window notifications (pay-now, allocated, expired,
 * displaced) are handled separately by {@link BookingNotifierService} in
 * train-scheduling.
 */
@Injectable()
export class BookingLifecycleNotifierService {
  private readonly logger = new Logger(BookingLifecycleNotifierService.name);

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

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

  /** Send SMS + email to the booking's company contact; log-only on failure. */
  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.BOOKING_STATUS,
      title,
      body,
      link: `/bookings/${b.id}`,
      data: { bookingId: b.id, reference: b.reference },
      ...overrides,
    });
  }

  /** Persist + push an in-app item to every backoffice staff user. */
  private inAppStaff(
    b: Booking,
    title: string,
    body: string,
    overrides: Partial<NotifyInput> = {},
  ): void {
    void this.inbox.notify({
      recipients: { allBackoffice: true },
      audience: NotificationAudience.BACKOFFICE,
      type: NotificationType.REQUEST_SUBMITTED,
      title,
      body,
      link: `/dashboard/booking-requests/${b.id}`,
      data: { bookingId: b.id, reference: b.reference },
      ...overrides,
    });
  }

  // ── Customer-facing lifecycle events ───────────────────────────────────────

  /** Line staff accepted intake → booking is under approval. */
  accepted(b: Booking): void {
    const msg =
      `Your booking ${b.reference} has been accepted and is now under approval. ` +
      `We will notify you once it is approved.`;
    void this.notifyContact(b, msg, 'ACCEPTED');
    this.inApp(b, 'Booking accepted', msg);
  }

  /** All approval steps complete → contract generated, ready for customer to sign. */
  approved(b: Booking): void {
    const msg =
      `Your booking ${b.reference} has been approved. ` +
      `Please review and sign your contract from the portal.`;
    void this.notifyContact(b, msg, 'APPROVED');
    this.inApp(b, 'Booking approved', msg);
  }

  /** Staff rejected the booking (intake or approval step). */
  rejected(b: Booking, reason: string): void {
    const msg =
      `Your booking ${b.reference} was rejected. Reason: ${reason}. ` +
      `Please contact us for details.`;
    void this.notifyContact(b, msg, 'REJECTED');
    this.inApp(b, 'Booking rejected', msg);
  }

  /** Staff requested changes before approval. */
  changesRequested(b: Booking, note: string): void {
    const msg =
      `Changes were requested on your booking ${b.reference}: ${note}. ` +
      `Please update and resubmit from the portal.`;
    void this.notifyContact(b, msg, 'CHANGES REQUESTED');
    this.inApp(b, 'Booking changes requested', msg);
  }

  /** A clearance document was queried and needs the customer to re-upload. */
  documentQueried(b: Booking, fileKey: string, note: string): void {
    const msg =
      `A clearance document on booking ${b.reference} needs attention: "${fileKey}". ` +
      `${note}. Please re-upload from the portal.`;
    void this.notifyContact(b, msg, 'DOCUMENT QUERIED');
    this.inApp(b, 'Document queried', msg, {
      type: NotificationType.DOCUMENT_ACTION,
    });
  }

  /** Document approval finalized → customer can proceed to request operation. */
  clearanceReady(b: Booking): void {
    const msg =
      `Document approval for booking ${b.reference} is finalized. ` +
      `You can now proceed to request operation from the portal.`;
    void this.notifyContact(b, msg, 'DOCUMENT APPROVAL FINALIZED');
    this.inApp(b, 'Document approval finalized', msg, {
      type: NotificationType.CLEARANCE_DECISION,
    });
  }

  /** Intercity documents approved → booking waits in the ride-along pool. */
  intercityDocumentsApproved(b: Booking): void {
    const msg =
      `Documents for intercity booking ${b.reference} are approved. ` +
      `Operations will assign your shipment to a passing train; payment opens once it is accepted.`;
    void this.notifyContact(b, msg, 'DOCUMENTS APPROVED');
    this.inApp(b, 'Documents approved', msg, {
      type: NotificationType.CLEARANCE_DECISION,
    });
  }

  /**
   * Operations returned the operation request for changes.
   *
   * A customs (Path B) booking was created BY GL Ethiopia on the customer's
   * behalf — the customer cannot edit or resubmit it, so telling them to "update
   * from the portal" is a dead end. Those go to the GL who created it, linking
   * the contract clearance page they work from. Everything else (customer-made
   * bookings) keeps the portal message.
   */
  operationChangesRequested(b: Booking, note: string): void {
    if (b.createdByRole === 'GL_ET' && b.createdByUserId) {
      const msg =
        `Operations returned booking ${b.reference} for changes: ${note}. ` +
        `Address it on the contract clearance page and resubmit to Operations.`;
      this.logger.log(`OPERATION CHANGES REQUESTED (to GL) — ${this.ref(b)}`);
      void this.inbox.notify({
        recipients: { userIds: [b.createdByUserId] },
        audience: NotificationAudience.BACKOFFICE,
        type: NotificationType.BOOKING_STATUS,
        title: `Booking ${b.reference} needs changes`,
        body: msg,
        link: b.contractId
          ? `/dashboard/contracts/clearance/${b.contractId}`
          : `/dashboard/bookings/${b.id}/clearance`,
        data: { bookingId: b.id, reference: b.reference, note },
      });
      return;
    }

    const msg =
      `Your operation request for booking ${b.reference} needs changes: ${note}. ` +
      `Please update and resubmit from the portal.`;
    void this.notifyContact(b, msg, 'OPERATION CHANGES REQUESTED');
    this.inApp(b, 'Operation request needs changes', msg);
  }

  /** Operation accepted → invoice ready; await payment / booking window. */
  operationAccepted(b: Booking): void {
    const msg =
      `Your operation request for booking ${b.reference} has been accepted. ` +
      `An invoice has been prepared — watch for the payment window to secure your slot.`;
    void this.notifyContact(b, msg, 'OPERATION ACCEPTED');
    this.inApp(b, 'Operation request accepted', msg);
  }

  /**
   * GL Ethiopia created this booking on the customer's behalf. On a customs
   * (Path B) contract the customer never books themselves, so without this they
   * would have no signal that their shipment now exists and is priced.
   */
  createdByGlForCustomer(b: Booking): void {
    const total = Number(b.totalAmount ?? 0);
    const priced =
      total > 0
        ? ` The total is ${total.toLocaleString()} ${b.paymentCurrency}.`
        : '';
    const msg =
      `Global Logistics has created shipment ${b.reference} under your contract.${priced} ` +
      `You can review it in the portal.`;
    void this.notifyContact(b, msg, 'CREATED BY GL');
    this.inApp(b, 'Shipment created for you', msg);
  }

  /** Shipment started → in transit. */
  inTransit(b: Booking): void {
    const msg = `Your shipment for booking ${b.reference} is now in transit.`;
    void this.notifyContact(b, msg, 'IN TRANSIT');
    this.inApp(b, 'Shipment in transit', msg);
  }

  /** Shipment delivered → completed. */
  completed(b: Booking): void {
    const msg = `Your shipment for booking ${b.reference} has been delivered. Thank you.`;
    void this.notifyContact(b, msg, 'COMPLETED');
    this.inApp(b, 'Shipment delivered', msg);
  }

  /** Booking cancelled. */
  cancelled(b: Booking, reason: string): void {
    const msg = `Your booking ${b.reference} has been cancelled. Reason: ${reason}.`;
    void this.notifyContact(b, msg, 'CANCELLED');
    this.inApp(b, 'Booking cancelled', msg);
  }

  // ── Clearance milestones needing customer action ──────────────────────────

  /** GL advised duty & tax — the customer must pay and upload the slip. */
  dutyAdvised(b: Booking, amount: number, currency: string): void {
    const msg =
      `Duty & tax of ${amount} ${currency} has been advised for booking ${b.reference}. ` +
      `Please pay and upload the payment slip from the portal.`;
    void this.notifyContact(b, msg, 'DUTY ADVISED');
    this.inApp(b, 'Duty & tax advised', msg, {
      type: NotificationType.INVOICE_ISSUED,
    });
  }

  /** GL advised the post-arrival additional duty round (import). */
  secondDutyAdvised(b: Booking, amount: number, currency: string): void {
    const msg =
      `Additional duty & tax of ${amount} ${currency} has been advised for booking ${b.reference}. ` +
      `Please pay and upload the payment slip from the portal.`;
    void this.notifyContact(b, msg, 'SECOND DUTY ADVISED');
    this.inApp(b, 'Additional duty & tax advised', msg, {
      type: NotificationType.INVOICE_ISSUED,
    });
  }

  /** GL raised the final (post-offload) invoice — customer pays + uploads slip. */
  finalInvoiceCreated(b: Booking, amount: number, currency: string): void {
    const msg =
      `A final invoice of ${amount} ${currency} has been issued for booking ${b.reference}. ` +
      `Please pay and upload the payment slip from the portal.`;
    void this.notifyContact(b, msg, 'FINAL INVOICE');
    this.inApp(b, 'Final invoice issued', msg, {
      type: NotificationType.INVOICE_ISSUED,
    });
  }

  /** GL confirmed the final-invoice payment slip. */
  finalInvoicePaid(b: Booking): void {
    const msg = `Your final invoice payment for booking ${b.reference} has been confirmed. Thank you.`;
    void this.notifyContact(b, msg, 'FINAL INVOICE PAID');
    this.inApp(b, 'Final invoice paid', msg, {
      type: NotificationType.INVOICE_ISSUED,
    });
  }

  // ── Staff-facing (backoffice inbox) ────────────────────────────────────────

  /**
   * A booking was created under a contract. Contract drawdowns never pass
   * through submit, so this is the only point at which staff learn the booking
   * exists — {@link submittedToStaff} covers the direct-booking flow instead.
   */
  createdToStaff(b: Booking): void {
    this.inAppStaff(
      b,
      'New booking created',
      `Booking ${this.ref(b)} was created under a contract and has entered the pipeline.`,
    );
  }

  /** Customer submitted a booking for review. */
  submittedToStaff(b: Booking): void {
    this.inAppStaff(
      b,
      'New booking submitted',
      `Booking ${this.ref(b)} was submitted and is awaiting intake review.`,
    );
  }

  /** Customer signed the booking contract. */
  customerSignedToStaff(b: Booking): void {
    this.inAppStaff(
      b,
      'Customer signed booking contract',
      `The contract for booking ${this.ref(b)} was signed by the customer.`,
    );
  }

  /** Customer requested operation (picked a shipment day). */
  operationRequestedToStaff(b: Booking): void {
    this.inAppStaff(
      b,
      'Operation requested',
      `Booking ${this.ref(b)} requested operation — review capacity, documents and route.`,
    );
  }

  /** Customer uploaded clearance documents — review is next. */
  clearanceDocsUploadedToStaff(b: Booking): void {
    this.inAppStaff(
      b,
      'Clearance documents uploaded',
      `Customer uploaded clearance documents for booking ${this.ref(b)} — review them in the clearance queue.`,
      {
        type: NotificationType.CLEARANCE_REVIEW,
        link: `/dashboard/bookings/${b.id}/clearance`,
      },
    );
  }

  /** Customer uploaded a duty/tax payment slip — GL verifies it. */
  dutySlipUploadedToStaff(b: Booking, round: 'first' | 'second' | 'final'): void {
    const label =
      round === 'final'
        ? 'final invoice'
        : round === 'second'
          ? 'additional duty & tax'
          : 'duty & tax';
    this.inAppStaff(
      b,
      'Payment slip uploaded',
      `Customer uploaded the ${label} payment slip for booking ${this.ref(b)}.`,
      {
        type: NotificationType.PAYMENT_RECEIVED,
        link: `/dashboard/bookings/${b.id}/clearance`,
      },
    );
  }
}
