import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';

export interface ReceivedUnitRow {
  id: string;
  containerNumber: string;
  receivedToPort: boolean;
  receivedAt: string | null;
  grnNumber: string | null;
}

/**
 * Per-container receive + GRN tracking on booking_container_units.
 *
 * Containers arrive individually (on separate self-haul trucks), so each unit is
 * flipped `received_to_port` when its truck arrives (auto). Staff then confirm a
 * Goods Received Note over the received-but-un-GRN'd containers: one GRN covers a
 * batch, so if the whole booking arrives together every unit shares a single GRN
 * (per-booking GRN); if trucks arrive separately each batch gets its own GRN.
 */
@Injectable()
export class ContainerReceiptService {
  constructor(private readonly dataSource: DataSource) {}

  /**
   * Auto-mark the containers loaded on an arrived truck as received into the
   * port. Idempotent — only flips units not already received. Runs inside the
   * caller's transaction when a manager is supplied.
   */
  async markReceivedForAssignment(
    bookingId: string,
    assignmentId: string,
    manager?: EntityManager,
  ): Promise<void> {
    const m = manager ?? this.dataSource.manager;
    await m.query(
      `UPDATE freight.booking_container_units bcu
          SET received_to_port = true,
              received_at = COALESCE(bcu.received_at, NOW()),
              updated_at = NOW()
         FROM freight.booking_container bc,
              freight.customer_truck_containers ctc
        WHERE bc.id = bcu.booking_container_id
          AND bc.booking_id = $1
          AND ctc.assignment_id = $2
          AND ctc.deleted_at IS NULL
          AND ctc.container_number = bcu.container_number
          AND bcu.deleted_at IS NULL
          AND bcu.received_to_port = false`,
      [bookingId, assignmentId],
    );
  }

  /** Received-into-port containers that have not yet been assigned a GRN. */
  async listReceivedPendingGrn(bookingId: string): Promise<ReceivedUnitRow[]> {
    return this.dataSource.query(
      `SELECT bcu.id,
              bcu.container_number AS "containerNumber",
              bcu.received_to_port AS "receivedToPort",
              bcu.received_at AS "receivedAt",
              bcu.grn_number AS "grnNumber"
         FROM freight.booking_container_units bcu
         JOIN freight.booking_container bc
           ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
        WHERE bc.booking_id = $1
          AND bcu.deleted_at IS NULL
          AND bcu.received_to_port = true
          AND bcu.grn_number IS NULL
        ORDER BY bcu.received_at`,
      [bookingId],
    );
  }

  /**
   * Confirm a GRN over the currently received-but-un-GRN'd containers (optionally
   * a subset by container number). Assigns one GRN number to the whole batch and
   * returns it with the covered containers. If the batch covers every container
   * on the booking it is effectively a per-booking GRN.
   */
  async generateGrn(
    bookingId: string,
    containerNumbers?: string[],
  ): Promise<{ grnNumber: string; containerNumbers: string[]; perBooking: boolean }> {
    const [booking] = await this.dataSource.query(
      `SELECT reference FROM freight.bookings WHERE id = $1 AND deleted_at IS NULL`,
      [bookingId],
    );
    if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);

    return this.dataSource.transaction(async (manager) => {
      const wanted = containerNumbers?.map((n) => n.trim().toUpperCase());
      const pending: ReceivedUnitRow[] = await manager.query(
        `SELECT bcu.id, bcu.container_number AS "containerNumber"
           FROM freight.booking_container_units bcu
           JOIN freight.booking_container bc
             ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
          WHERE bc.booking_id = $1
            AND bcu.deleted_at IS NULL
            AND bcu.received_to_port = true
            AND bcu.grn_number IS NULL
            ${wanted ? 'AND bcu.container_number = ANY($2::varchar[])' : ''}`,
        wanted ? [bookingId, wanted] : [bookingId],
      );
      if (!pending.length) {
        throw new BadRequestException('No received containers are awaiting a GRN');
      }

      // Batch sequence = number of GRNs already issued for this booking + 1.
      const [{ batches }]: Array<{ batches: string }> = await manager.query(
        `SELECT COUNT(DISTINCT bcu.grn_number) AS batches
           FROM freight.booking_container_units bcu
           JOIN freight.booking_container bc
             ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
          WHERE bc.booking_id = $1 AND bcu.grn_number IS NOT NULL AND bcu.deleted_at IS NULL`,
        [bookingId],
      );
      const seq = Number(batches) + 1;
      const grnNumber = `GRN-${String(booking.reference).replace(/^BK-?/i, '')}-${String(seq).padStart(2, '0')}`;

      const ids = pending.map((p) => p.id);
      await manager.query(
        `UPDATE freight.booking_container_units
            SET grn_number = $1, updated_at = NOW()
          WHERE id = ANY($2::uuid[])`,
        [grnNumber, ids],
      );

      // Per-booking when no container on the booking is left un-GRN'd.
      const [{ remaining }]: Array<{ remaining: string }> = await manager.query(
        `SELECT COUNT(*) AS remaining
           FROM freight.booking_container_units bcu
           JOIN freight.booking_container bc
             ON bc.id = bcu.booking_container_id AND bc.deleted_at IS NULL
          WHERE bc.booking_id = $1 AND bcu.deleted_at IS NULL AND bcu.grn_number IS NULL`,
        [bookingId],
      );

      return {
        grnNumber,
        containerNumbers: pending.map((p) => p.containerNumber),
        perBooking: Number(remaining) === 0 && seq === 1,
      };
    });
  }
}
