import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { NotificationAudience, NotificationType } from '@edr/types';
import { DataSource, EntityManager, IsNull, Repository } from 'typeorm';

import { BookingHandover, HandoverMileType } from './entities/booking-handover.entity';
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
import { NotificationsService } from '../notifications/notifications.service';
import { sendCompanyChannels } from '../notifications/notify-company.util';

/**
 * Import handover records. A booking has one handover per truck (single truck ⇒
 * one, effectively per-booking; multiple trucks ⇒ one each). Timing by mile type:
 *  - SELF_HAUL: generated when the customer truck arrives, signed before it leaves.
 *  - EDR_LAST_MILE: generated when the EDR truck exits the warehouse (with its
 *    exit paper), signed by the customer in the portal per truck; once every
 *    handover is signed the delivery auto-completes (inventory / cargo /
 *    booking → delivered).
 */
@Injectable()
export class HandoverService {
  private readonly logger = new Logger(HandoverService.name);

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

  /** Tell the customer a handover is ready and needs their signature. */
  private async notifySignNeeded(
    bookingId: string,
    reference: string,
    opts: { mileType?: HandoverMileType; truckPlate?: string | null } = {},
  ): Promise<void> {
    try {
      const [b]: Array<{ companyId: string | null; reference: string }> = await this.dataSource.query(
        `SELECT company_id AS "companyId", reference FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`,
        [bookingId],
      );
      if (!b?.companyId) return;
      const truck = opts.truckPlate ? ` (truck ${opts.truckPlate})` : '';
      const body =
        opts.mileType === 'EDR_LAST_MILE'
          ? `Your goods for booking ${b.reference} are on their way${truck}. Please review and sign handover ${reference} from the portal to confirm receipt of the delivery.`
          : `Your import handover ${reference} for booking ${b.reference} is ready. Please review and sign it from the portal before the truck leaves.`;
      await this.inbox.notify({
        recipients: { companyId: b.companyId },
        audience: NotificationAudience.PORTAL,
        type: NotificationType.DOCUMENT_ACTION,
        title: 'Handover — signature needed',
        body,
        link: `/bookings/${bookingId}`,
        data: { bookingId, reference },
      });
      await sendCompanyChannels(this.dataSource, this.notifications, b.companyId, body);
    } catch (err) {
      this.logger.warn(`Failed to notify handover sign for ${bookingId}: ${(err as Error).message}`);
    }
  }

  list(bookingId: string): Promise<BookingHandover[]> {
    return this.dataSource.getRepository(BookingHandover).find({
      where: { bookingId },
      order: { generatedAt: 'ASC' },
    });
  }

  /**
   * Ask the customer to sign the booking's handover. Ensures a handover exists
   * (creates a booking-level self-haul one if none yet), then fires the
   * sign-needed notification (in-app + SMS + email). Idempotent to re-send.
   */
  async requestSignature(
    bookingId: string,
  ): Promise<{ notified: boolean; reference: string | null; alreadySigned: boolean }> {
    const repo = this.dataSource.getRepository(BookingHandover);
    const existing = await repo.find({ where: { bookingId }, order: { generatedAt: 'ASC' } });

    if (existing.length === 0) {
      // No handover yet (truck not arrived): create a booking-level one so the
      // customer has something to sign. ensureForArrivedTruck notifies on create.
      const created = await this.ensureForArrivedTruck(bookingId, {});
      return { notified: true, reference: created.reference, alreadySigned: false };
    }

    const unsigned = existing.find((h) => !h.signedAt);
    if (!unsigned) {
      return { notified: false, reference: existing[0].reference, alreadySigned: true };
    }
    await this.notifySignNeeded(bookingId, unsigned.reference);
    return { notified: true, reference: unsigned.reference, alreadySigned: false };
  }

  /**
   * Self-haul: ensure a handover exists for a customer truck that just arrived.
   * Idempotent — one per (booking, truck). Runs inside the caller's transaction
   * when a manager is supplied.
   */
  async ensureForArrivedTruck(
    bookingId: string,
    opts: { truckAssignmentId?: string | null; truckPlate?: string | null },
    manager?: EntityManager,
  ): Promise<BookingHandover> {
    const m = manager ?? this.dataSource.manager;
    const repo = m.getRepository(BookingHandover);
    const existing = await repo.findOne({
      where: {
        bookingId,
        truckAssignmentId: opts.truckAssignmentId ?? IsNull(),
      },
    });
    if (existing) return existing;

    const reference = await this.generateReference(bookingId, m);
    const saved = await repo.save(
      repo.create({
        bookingId,
        truckAssignmentId: opts.truckAssignmentId ?? null,
        truckPlate: opts.truckPlate ?? null,
        mileType: 'SELF_HAUL',
        reference,
        generatedAt: new Date(),
      }),
    );
    this.logger.log(`Handover ${reference} generated on arrival for booking ${bookingId}`);
    void this.notifySignNeeded(bookingId, reference);
    return saved;
  }

  /** Find an existing EDR handover by assignment, else by plate, else booking-level. */
  private async findEdrHandover(
    repo: Repository<BookingHandover>,
    bookingId: string,
    opts: { truckPlate?: string | null; edrAssignmentId?: string | null },
  ): Promise<BookingHandover | null> {
    if (opts.edrAssignmentId) {
      const byAssignment = await repo.findOne({
        where: { bookingId, edrAssignmentId: opts.edrAssignmentId },
      });
      if (byAssignment) return byAssignment;
    }
    if (opts.truckPlate) {
      return repo.findOne({
        where: { bookingId, mileType: 'EDR_LAST_MILE', truckPlate: opts.truckPlate },
      });
    }
    return repo.findOne({
      where: {
        bookingId,
        mileType: 'EDR_LAST_MILE',
        truckPlate: IsNull(),
        edrAssignmentId: IsNull(),
      },
    });
  }

  /**
   * EDR last-mile: generate the handover when the EDR truck exits the warehouse
   * (alongside its exit paper) and ask the customer to sign it from the portal.
   * One per truck (multiple trucks ⇒ one each) or booking-level when the truck
   * cannot be resolved. Idempotent by (booking, assignment) / (booking, plate).
   */
  async ensureForDepartedEdrTruck(
    bookingId: string,
    opts: { truckPlate?: string | null; edrAssignmentId?: string | null },
    manager?: EntityManager,
  ): Promise<BookingHandover> {
    const m = manager ?? this.dataSource.manager;
    const repo = m.getRepository(BookingHandover);
    const existing = await this.findEdrHandover(repo, bookingId, opts);
    if (existing) return existing;

    const reference = await this.generateReference(bookingId, m);
    const saved = await repo.save(
      repo.create({
        bookingId,
        edrAssignmentId: opts.edrAssignmentId ?? null,
        truckPlate: opts.truckPlate ?? null,
        mileType: 'EDR_LAST_MILE',
        reference,
        generatedAt: new Date(),
      }),
    );
    this.logger.log(
      `EDR handover ${reference} generated on truck exit for booking ${bookingId}` +
        (opts.truckPlate ? ` (truck ${opts.truckPlate})` : ''),
    );
    void this.notifySignNeeded(bookingId, reference, {
      mileType: 'EDR_LAST_MILE',
      truckPlate: opts.truckPlate,
    });
    return saved;
  }

  /**
   * EDR last-mile: ensure a handover exists at delivery and stamp delivered_at.
   * Normally the handover was already generated on truck exit — this only fills
   * the delivery timestamp; a handover is created here only for legacy flows
   * where the exit was recorded before this feature existed.
   */
  async ensureAtDelivery(
    bookingId: string,
    opts: { truckPlate?: string | null; edrAssignmentId?: string | null },
    manager?: EntityManager,
  ): Promise<BookingHandover> {
    const m = manager ?? this.dataSource.manager;
    const repo = m.getRepository(BookingHandover);
    const existing = await this.findEdrHandover(repo, bookingId, opts);
    if (existing) {
      if (!existing.deliveredAt) {
        existing.deliveredAt = new Date();
        await repo.save(existing);
      }
      return existing;
    }

    const reference = await this.generateReference(bookingId, m);
    const saved = await repo.save(
      repo.create({
        bookingId,
        edrAssignmentId: opts.edrAssignmentId ?? null,
        truckPlate: opts.truckPlate ?? null,
        mileType: 'EDR_LAST_MILE',
        reference,
        generatedAt: new Date(),
        deliveredAt: new Date(),
      }),
    );
    void this.notifySignNeeded(bookingId, reference, {
      mileType: 'EDR_LAST_MILE',
      truckPlate: opts.truckPlate,
    });
    return saved;
  }

  /** Re-send the sign notification for every unsigned handover on the booking. */
  async notifyUnsignedForBooking(bookingId: string): Promise<void> {
    const unsigned = await this.dataSource.getRepository(BookingHandover).find({
      where: { bookingId, signedAt: IsNull() },
      order: { generatedAt: 'ASC' },
    });
    for (const h of unsigned) {
      await this.notifySignNeeded(bookingId, h.reference, {
        mileType: h.mileType,
        truckPlate: h.truckPlate,
      });
    }
  }

  /**
   * Reminder loop: until a self-haul handover is signed, re-send the sign
   * notification (in-app + SMS + email) every 5 minutes. One reminder per
   * booking per tick, newest unsigned handover's reference. Stops the moment
   * signForBooking() stamps signed_at.
   *
   * NB: runs in every API instance — keep a single instance in dev or the
   * customer is reminded once per instance per tick.
   */
  @Cron(CronExpression.EVERY_5_MINUTES, { name: 'handover-sign-reminder' })
  async remindUnsignedHandovers(): Promise<void> {
    try {
      const rows: Array<{ bookingId: string; reference: string }> = await this.dataSource.query(
        `SELECT DISTINCT ON (booking_id)
                booking_id AS "bookingId", reference
           FROM freight.booking_handovers
          WHERE signed_at IS NULL
            AND deleted_at IS NULL
            AND mile_type = 'SELF_HAUL'
          ORDER BY booking_id, generated_at DESC`,
      );
      if (!rows.length) return;
      this.logger.log(`Handover sign reminder: ${rows.length} booking(s) still unsigned`);
      for (const row of rows) {
        await this.notifySignNeeded(row.bookingId, row.reference);
      }
    } catch (err) {
      this.logger.warn(`Handover sign reminder tick failed: ${(err as Error).message}`);
    }
  }

  /**
   * Sign one handover (EDR last-mile: the customer signs per truck). Returns the
   * fresh handover; idempotent — an already-signed handover is returned as-is.
   */
  async sign(
    handoverId: string,
    userId?: string | null,
    signerName?: string | null,
    signatureImageUrl?: string | null,
  ): Promise<BookingHandover> {
    const repo = this.dataSource.getRepository(BookingHandover);
    const handover = await repo.findOne({ where: { id: handoverId } });
    if (!handover) {
      throw new NotFoundException(`Handover ${handoverId} not found`);
    }
    if (handover.signedAt) return handover;
    handover.signedAt = new Date();
    handover.signedByUserId = userId ?? null;
    handover.signerName = signerName?.trim() || null;
    handover.signatureImageUrl = signatureImageUrl ?? null;
    return repo.save(handover);
  }

  /** Sign all unsigned handovers on a booking (self-haul: before the truck leaves). */
  async signForBooking(
    bookingId: string,
    userId?: string | null,
    signerName?: string | null,
    signatureImageUrl?: string | null,
  ): Promise<void> {
    await this.dataSource
      .getRepository(BookingHandover)
      .update(
        { bookingId, signedAt: IsNull() },
        {
          signedAt: new Date(),
          signedByUserId: userId ?? null,
          signerName: signerName?.trim() || null,
          signatureImageUrl: signatureImageUrl ?? null,
        },
      );
  }

  /** True when every handover on the booking is signed (and at least one exists). */
  async isFullySigned(bookingId: string): Promise<boolean> {
    const repo = this.dataSource.getRepository(BookingHandover);
    const [total, unsigned] = await Promise.all([
      repo.count({ where: { bookingId } }),
      repo.count({ where: { bookingId, signedAt: IsNull() } }),
    ]);
    return total > 0 && unsigned === 0;
  }

  private async generateReference(bookingId: string, manager: EntityManager): Promise<string> {
    const [booking] = await manager.query(
      `SELECT reference FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`,
      [bookingId],
    );
    const ref = String(booking?.reference ?? bookingId).replace(/^BK-?/i, '');
    const count = await manager.getRepository(BookingHandover).count({ where: { bookingId } });
    return `HND-${ref}-${String(count + 1).padStart(2, '0')}`;
  }
}
