import { BadRequestException, Injectable } from '@nestjs/common';
import {
  ContractDocPhase,
  type ClearanceFinalInvoiceSummary,
  type ClearanceSecondDuty,
  type ClearanceT1State,
  type ClearanceTrainState,
} from '@edr/types';

import { DropdownSettingsService } from '../dropdown-settings/dropdown-settings.service';
import { FileUploadSettingsService } from '../file-upload-settings/file-upload-settings.service';
import { FilesService } from '../files/files.service';
import { BookingsRepository } from '../bookings/bookings.repository';
import { BookingsService } from '../bookings/bookings.service';
import { BookingLifecycleNotifierService } from '../bookings/booking-lifecycle-notifier.service';
import {
  ClearanceMilestone,
  type RiskAssignmentRecord,
} from './entities/clearance-milestone.entity';
import { Booking } from '../bookings/entities/booking.entity';
import { clearanceCodesForBooking } from '../bookings/clearance.util';
import { assertDoCollectionDates } from './contract-clearance.util';
import { ClearanceWorkflowService } from './clearance-workflow.service';
import { ClearanceMilestoneService } from './clearance-milestone.service';
import { GlOperationsService } from './gl-operations.service';
import { AdviseContractDutyDto } from './dto/phased-clearance.dto';
import { buildWorkflowFiles, belongsOnDjClearanceQueue, belongsOnEtClearanceQueue, DJ_BOOKING_QUEUE_STATUSES, persistDeclarationUploads, persistTransitPermitUploads, PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES } from './phased-clearance.util';

const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';

export interface BookingClearanceView {
  bookingId: string;
  status: string;
  includesCustoms: boolean;
  inputCode: string | null;
  outputCode: string | null;
  documents: Array<{
    fileKey: string;
    label: string;
    required: boolean;
    uploadedBy: 'customer' | 'gl';
    settingCode: string;
    file: { id: string; name: string; url: string } | null;
    reviewStatus: 'PENDING' | 'APPROVED' | 'QUERIED' | null;
    note: string | null;
  }>;
  allApproved: boolean;
  phase?: string | null;
  milestones?: Array<{
    id: string;
    milestoneCode: string;
    milestoneLabel: string;
    status: string;
    ownerRegion?: string | null;
    metadata?: Record<string, unknown> | null;
    sortOrder: number;
  }>;
  nextAction?: {
    actor: string;
    action: string;
    milestoneCode?: string | null;
    blockedReason?: string | null;
  } | null;
  dutyRequired?: boolean | null;
  roHold?: boolean;
  roHoldReason?: string | null;
  vesselDepartureDate?: string | null;
  /** Import DO dates recorded by GL Djibouti on upload. */
  vesselArrivalDate?: string | null;
  doCollectedDate?: string | null;
  roAmendmentRequestedAt?: string | null;
  operationReady?: boolean;
  preClearanceFinalized?: boolean;
  dutyAdvice?: {
    amount: number;
    currency: string;
    declarationSerial?: string | null;
    noticeFile?: { id: string; name: string; url: string } | null;
  } | null;
  workflowFiles?: ReturnType<typeof buildWorkflowFiles>;
  /** Import post-allocation T1 transit document state (null until wagon allocation). */
  t1?: ClearanceT1State | null;
  /** Train link state for the booking (both directions). */
  train?: ClearanceTrainState | null;
  gatepassGranted?: boolean;
  gatepassAt?: string | null;
  t1Closed?: boolean;
  t1ClosedAt?: string | null;
  offloaded?: boolean;
  /** GL Djibouti post-offload final invoice (export). */
  finalInvoice?: ClearanceFinalInvoiceSummary | null;
  /** Customs risk level assigned by GL ET (import; visible to the customer). */
  riskLevel?: string | null;
  riskAssignedAt?: string | null;
  /** Every risk decision, oldest first; the last entry is the current level. */
  riskHistory?: RiskAssignmentRecord[];
  /** Post-arrival additional duty/tax round (import). */
  secondDuty?: ClearanceSecondDuty | null;
  importReleaseGranted?: boolean;
}

@Injectable()
export class BookingClearanceService {
  constructor(
    private readonly bookingsRepository: BookingsRepository,
    private readonly bookingsService: BookingsService,
    private readonly filesService: FilesService,
    private readonly fileUploadSettingsService: FileUploadSettingsService,
    private readonly workflowService: ClearanceWorkflowService,
    private readonly milestoneService: ClearanceMilestoneService,
    private readonly dropdownSettingsService: DropdownSettingsService,
    private readonly glOperationsService: GlOperationsService,
    private readonly notifier: BookingLifecycleNotifierService,
  ) {}

  private async assertPhasedGeneralCustoms(booking: Booking): Promise<void> {
    if (!booking.customsClearingEnabled) {
      throw new BadRequestException('Phased clearance applies only to customs bookings.');
    }
    if (booking.contractKind !== 'GENERAL') {
      throw new BadRequestException('Per-booking phased clearance applies to general contracts.');
    }
    if (!booking.contractId) {
      throw new BadRequestException('Booking is not linked to a contract.');
    }
  }

  private async loadBooking(bookingId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    await this.assertPhasedGeneralCustoms(booking);
    return booking;
  }

  async getClearanceView(bookingId: string): Promise<BookingClearanceView> {
    const booking = await this.loadBooking(bookingId);
    const { inputCode, outputCode, includesCustoms } = clearanceCodesForBooking(booking);

    const files = await this.filesService.findByResource(bookingId, 'bookings');
    const fileByCode = new Map(files.map((f) => [f.code, f]));
    const reviews = await this.bookingsRepository.findDocumentReviews(bookingId);
    const reviewByKey = new Map(reviews.map((r) => [`${r.settingCode}:${r.fileKey}`, r]));

    const documents: BookingClearanceView['documents'] = [];

    const pushSetting = async (code: string | null, uploadedBy: 'customer' | 'gl') => {
      if (!code) return;
      let setting;
      try {
        setting = await this.fileUploadSettingsService.getByCode(code);
      } catch {
        return;
      }
      for (const field of setting.fields ?? []) {
        const file = fileByCode.get(field.fileKey) ?? null;
        const review = reviewByKey.get(`${code}:${field.fileKey}`) ?? null;
        documents.push({
          fileKey: field.fileKey,
          label: field.fileLabel,
          required: field.isRequired,
          uploadedBy,
          settingCode: code,
          file: file ? { id: file.id, name: file.name, url: file.url } : null,
          reviewStatus: review?.status ?? null,
          note: review?.note ?? null,
        });
      }
    };

    await pushSetting(inputCode, 'customer');
    await pushSetting(outputCode, 'gl');

    for (const f of files) {
      if (!f.code?.startsWith('custom_')) continue;
      const review = reviewByKey.get(`custom:${f.code}`) ?? null;
      documents.push({
        fileKey: f.code,
        label: f.name,
        required: false,
        uploadedBy: 'customer',
        settingCode: 'custom',
        file: { id: f.id, name: f.name, url: f.url },
        reviewStatus: review?.status ?? null,
        note: review?.note ?? null,
      });
    }

    const allApproved = await this.isClearanceFullyApproved(booking);
    let milestones = await this.workflowService.listMilestonesForBooking(bookingId);

    // Self-heal: a booking that has settled its freight payment must have
    // FREIGHT_PAYMENT_SETTLED completed. The batch settle path writes it, but an
    // export FCFS booking (linked to its train at booking time) paid via the
    // prepaid invoice can leave the milestone PENDING — the clearance "Payment &
    // wagon allocation" step then never ticks. Backfill it here so already-stuck
    // rows recover without a migration; idempotent (no-op once COMPLETED).
    const paymentSettled = milestones.find(
      (m) => m.milestoneCode === 'FREIGHT_PAYMENT_SETTLED',
    );
    if (
      paymentSettled &&
      paymentSettled.status === 'PENDING' &&
      (booking.paymentStatus === 'PAID' || booking.status === 'PAID')
    ) {
      await this.workflowService.completeMilestoneForBooking(
        bookingId,
        'FREIGHT_PAYMENT_SETTLED',
      );
      milestones = await this.workflowService.listMilestonesForBooking(bookingId);
    }
    const phase = this.workflowService.resolvePhaseForBooking(booking, milestones);
    const nextAction = this.workflowService.computeNextActionForBooking(booking, milestones);
    const boundary = await this.workflowService.isBoundaryCompleteForBooking(
      bookingId,
      booking.tradeDirection ?? 'IMPORT',
    );
    const dutyAdvice = this.buildDutyAdvice(files, milestones);
    const workflowFiles = buildWorkflowFiles(
      files,
      booking.tradeDirection ?? 'IMPORT',
    );

    let t1: ClearanceT1State | null = null;
    if ((booking.tradeDirection ?? 'IMPORT') === 'IMPORT') {
      try {
        t1 = await this.glOperationsService.t1State(bookingId);
      } catch {
        t1 = null;
      }
    }

    let train: ClearanceTrainState | null = null;
    try {
      train = await this.glOperationsService.trainState(bookingId);
    } catch {
      train = null;
    }
    const finalInvoice = await this.glOperationsService.finalInvoiceSummary(bookingId);
    const bookingMilestone = (code: string) =>
      milestones.find((m) => m.milestoneCode === code);
    const gatepass = await this.glOperationsService.gatepassForBooking(bookingId);
    const t1ClosedMilestone = bookingMilestone('T1_CLOSED');
    const riskMilestone = bookingMilestone('RISK_ASSIGNED');
    const secondDuty = this.glOperationsService.secondDutyState(milestones, files);

    return {
      bookingId,
      status: booking.status,
      includesCustoms,
      inputCode,
      outputCode,
      documents,
      allApproved,
      phase,
      milestones: milestones.map((m) => ({
        id: m.id,
        milestoneCode: m.milestoneCode,
        milestoneLabel: m.milestoneLabel,
        status: m.status,
        ownerRegion: m.ownerRegion,
        metadata: (m.metadata ?? null) as Record<string, unknown> | null,
        sortOrder: m.sortOrder,
      })),
      nextAction,
      dutyRequired: booking.dutyRequired ?? null,
      roHold: Boolean(booking.roHoldReason),
      roHoldReason: booking.roHoldReason ?? null,
      vesselDepartureDate: booking.vesselDepartureDate ?? null,
      vesselArrivalDate: booking.vesselArrivalDate ?? null,
      doCollectedDate: booking.doCollectedDate ?? null,
      roAmendmentRequestedAt: booking.roAmendmentRequestedAt
        ? booking.roAmendmentRequestedAt.toISOString()
        : null,
      operationReady: boundary,
      preClearanceFinalized: Boolean(booking.preClearanceFinalizedAt),
      dutyAdvice,
      workflowFiles,
      t1,
      train,
      gatepassGranted: gatepass.granted,
      gatepassAt: gatepass.grantedAt,
      t1Closed: t1ClosedMilestone?.status === 'COMPLETED',
      t1ClosedAt:
        t1ClosedMilestone?.status === 'COMPLETED' && t1ClosedMilestone.triggeredAt
          ? t1ClosedMilestone.triggeredAt.toISOString()
          : null,
      offloaded: bookingMilestone('OFFLOADED')?.status === 'COMPLETED',
      finalInvoice,
      riskLevel:
        riskMilestone?.status === 'COMPLETED'
          ? ((riskMilestone.metadata?.riskLevel as string | undefined) ?? null)
          : null,
      riskAssignedAt:
        riskMilestone?.status === 'COMPLETED' && riskMilestone.triggeredAt
          ? riskMilestone.triggeredAt.toISOString()
          : null,
      // Every risk decision, oldest first. `riskLevel`/`riskAssignedAt` above are
      // the current one; this is the trail behind it.
      riskHistory:
        riskMilestone?.status === 'COMPLETED'
          ? (riskMilestone.metadata?.riskHistory ?? [])
          : [],
      secondDuty,
      importReleaseGranted:
        bookingMilestone('IMPORT_RELEASE_GRANTED')?.status === 'COMPLETED',
    };
  }

  private buildDutyAdvice(
    files: Array<{ code?: string | null; id: string; name: string; url: string }>,
    milestones: ClearanceMilestone[],
  ): BookingClearanceView['dutyAdvice'] {
    const advised = milestones.find(
      (m) => m.milestoneCode === 'DUTY_TAXES_ADVISED' && m.status === 'COMPLETED',
    );
    if (!advised?.metadata) return null;
    const amount = advised.metadata.dutyAmount;
    const currency = advised.metadata.dutyCurrency;
    if (typeof amount !== 'number' || typeof currency !== 'string') return null;
    const notice = files.find((f) => f.code === 'duty_tax_notice');
    return {
      amount,
      currency,
      declarationSerial:
        typeof advised.metadata.declarationSerial === 'string'
          ? advised.metadata.declarationSerial
          : null,
      noticeFile: notice
        ? { id: notice.id, name: notice.name, url: notice.url }
        : null,
    };
  }

  private async isClearanceFullyApproved(booking: Booking): Promise<boolean> {
    const { inputCode } = clearanceCodesForBooking(booking);
    if (!inputCode) return true;
    let setting;
    try {
      setting = await this.fileUploadSettingsService.getByCode(inputCode);
    } catch {
      return false;
    }
    const required = (setting.fields ?? []).filter((f) => f.isRequired);
    if (required.length === 0) return true;
    const reviews = await this.bookingsRepository.findDocumentReviews(booking.id);
    return required.every((field) =>
      reviews.some(
        (r) =>
          r.settingCode === inputCode &&
          r.fileKey === field.fileKey &&
          r.status === 'APPROVED',
      ),
    );
  }

  isPhasedGeneralCustomsBooking(booking: Booking): boolean {
    return (
      Boolean(booking.customsClearingEnabled) &&
      booking.contractKind === 'GENERAL' &&
      Boolean(booking.contractId)
    );
  }

  async uploadDeclaration(
    bookingId: string,
    files: Express.Multer.File[],
    userId?: string,
  ): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    const tradeDirection = booking.tradeDirection ?? 'IMPORT';
    const allApproved = await this.isClearanceFullyApproved(booking);
    if (!allApproved) {
      throw new BadRequestException(
        'All required customer documents must be approved before uploading a declaration.',
      );
    }
    const milestones = await this.workflowService.listMilestonesForBooking(bookingId);
    const docsApproved = milestones.find((m) => m.milestoneCode === 'DOCUMENTS_APPROVED');
    if (docsApproved?.status !== 'COMPLETED' && docsApproved?.status !== 'SKIPPED') {
      await this.workflowService.onAllDocsApprovedForBooking(bookingId);
    }
    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      tradeDirection,
      'UNDER_CUSTOMS_CLEARANCE',
    );

    if (files.length === 0) {
      throw new BadRequestException('No declaration documents uploaded');
    }

    await persistDeclarationUploads(this.filesService, bookingId, 'bookings', files);

    await this.workflowService.onDeclarationUploadedForBooking(bookingId, userId);
    await this.bookingsRepository.update(bookingId, {
      clearanceCurrentPhase:
        tradeDirection === 'EXPORT'
          ? ContractDocPhase.GlEtPostClearance
          : ContractDocPhase.CustomerDuty,
    } as never);

    // Export: the declaration is the last GL ET pre-operation action — release
    // immediately so the customer can proceed without a separate confirm click.
    if (tradeDirection === 'EXPORT') {
      await this.workflowService.onExportReleasedForBooking(bookingId, userId);
    }

    return this.bookingsService.findById(bookingId);
  }

  async adviseDuty(
    bookingId: string,
    dto: AdviseContractDutyDto,
    userId?: string,
    attachment?: Express.Multer.File,
  ): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'IMPORT') {
      throw new BadRequestException('Duty advice applies only to import bookings.');
    }
    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      'IMPORT',
      'DUTY_TAXES_ADVISED',
    );

    await this.bookingsRepository.update(bookingId, {
      dutyRequired: dto.dutyRequired,
      clearanceCurrentPhase: dto.dutyRequired
        ? ContractDocPhase.CustomerDuty
        : ContractDocPhase.GlEtPostClearance,
    } as never);

    if (!dto.dutyRequired) {
      await this.workflowService.onDutySkippedForBooking(bookingId);
    } else {
      if (dto.amount == null || dto.amount < 0) {
        throw new BadRequestException('Duty amount is required when duty applies.');
      }
      if (!attachment) {
        throw new BadRequestException('Duty notice attachment is required when duty applies.');
      }
      await this.filesService.upsertByCode({
        resourceId: bookingId,
        resource: 'bookings',
        code: 'duty_tax_notice',
        file: attachment,
      });
      await this.milestoneService.adviseDuty(
        bookingId,
        {
          amount: dto.amount,
          currency: dto.currency ?? 'ETB',
          declarationSerial: dto.declarationSerial,
        },
        userId,
      );
      this.notifier.dutyAdvised(booking, dto.amount, dto.currency ?? 'ETB');
    }

    return this.bookingsService.findById(bookingId);
  }

  async uploadDutySlip(bookingId: string, file: Express.Multer.File): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'IMPORT') {
      throw new BadRequestException('Duty slip upload applies only to import bookings.');
    }
    if (!booking.dutyRequired) {
      throw new BadRequestException('Duty/tax is not required for this clearance.');
    }
    if (!file) throw new BadRequestException('No payment slip uploaded');

    await this.filesService.upsertByCode({
      resourceId: bookingId,
      resource: 'bookings',
      code: 'duty_tax_receipt',
      file,
    });

    await this.workflowService.completeMilestoneForBooking(bookingId, 'DUTY_TAX_PAID');
    await this.bookingsRepository.update(bookingId, {
      clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
    } as never);

    this.notifier.dutySlipUploadedToStaff(booking, 'first');
    return this.bookingsService.findById(bookingId);
  }

  async uploadTransitPermit(
    bookingId: string,
    files: Express.Multer.File[],
    userId?: string,
  ): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'IMPORT') {
      throw new BadRequestException('Transit permit applies only to import bookings.');
    }
    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      'IMPORT',
      'TRANSIT_PERMIT_UPLOADED',
    );
    if (files.length === 0) {
      throw new BadRequestException('No transit permit documents uploaded');
    }

    await persistTransitPermitUploads(this.filesService, bookingId, 'bookings', files);

    await this.workflowService.completeMilestoneForBooking(
      bookingId,
      'TRANSIT_PERMIT_UPLOADED',
      userId,
    );
    await this.bookingsRepository.update(bookingId, {
      clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
    } as never);

    return this.bookingsService.findById(bookingId);
  }

  async finalizePreClearance(bookingId: string): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'IMPORT') {
      throw new BadRequestException('Pre-clearance finalize applies only to import bookings.');
    }

    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      'IMPORT',
      'TRANSIT_PERMIT_UPLOADED',
    );

    if (booking.preClearanceFinalizedAt) {
      return this.bookingsService.findById(bookingId);
    }

    await this.bookingsRepository.update(bookingId, {
      preClearanceFinalizedAt: new Date(),
      clearanceCurrentPhase: ContractDocPhase.GlDjCollection,
    } as never);

    // GL Djibouti may have uploaded the DO early (un-gated) — count it now.
    const files = await this.filesService.findByResource(bookingId, 'bookings');
    if (files.some((f) => f.code === 'delivery_order')) {
      await this.workflowService.completeMilestoneForBooking(bookingId, 'DO_COLLECTED');
      await this.workflowService.markReadyForOperation(bookingId);
    }

    return this.bookingsService.findById(bookingId);
  }

  async uploadDeliveryOrder(
    bookingId: string,
    file: Express.Multer.File,
    userId?: string,
    dates?: { vesselArrivalDate?: string; doCollectedDate?: string },
  ): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'IMPORT') {
      throw new BadRequestException('Delivery Order applies only to import bookings.');
    }

    if (!file) throw new BadRequestException('No Delivery Order uploaded');

    const { vesselArrivalDate, doCollectedDate } = assertDoCollectionDates(dates);

    // DO upload is deliberately un-gated: GL Djibouti may attach it at any point,
    // any file type. The DO_COLLECTED milestone (and operation readiness) still
    // waits for GL Ethiopia to finalize pre-clearance so the workflow order holds.
    await this.filesService.upsertByCode({
      resourceId: bookingId,
      resource: 'bookings',
      code: 'delivery_order',
      file,
    });

    await this.bookingsRepository.update(bookingId, {
      vesselArrivalDate,
      doCollectedDate,
    } as never);

    if (booking.preClearanceFinalizedAt) {
      await this.workflowService.completeMilestoneForBooking(bookingId, 'DO_COLLECTED', userId);
      await this.workflowService.markReadyForOperation(bookingId);
    }

    return this.bookingsService.findById(bookingId);
  }

  private async resolveRoMinDays(): Promise<number> {
    try {
      const setting = await this.dropdownSettingsService.getByCode(RO_VESSEL_MIN_DAYS_CODE);
      const first = setting.children?.[0];
      const n = Number(first?.value);
      return Number.isFinite(n) && n > 0 ? n : 2;
    } catch {
      return 2;
    }
  }

  private daysUntil(dateStr: string): number {
    const target = new Date(dateStr);
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    target.setHours(0, 0, 0, 0);
    return Math.floor((target.getTime() - today.getTime()) / (24 * 60 * 60 * 1000));
  }

  async uploadReleaseOrder(
    bookingId: string,
    file: Express.Multer.File,
    vesselDepartureDate: string,
    userId?: string,
  ): Promise<{ booking: Booking; hold: boolean; holdReason?: string }> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'EXPORT') {
      throw new BadRequestException('Release Order applies only to export bookings.');
    }
    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      'EXPORT',
      'RELEASE_ORDER_SECURED',
    );

    if (!file) throw new BadRequestException('No Release Order uploaded');
    if (!vesselDepartureDate?.trim()) {
      throw new BadRequestException('Vessel departure date is required');
    }

    const minDays = await this.resolveRoMinDays();
    const leadDays = this.daysUntil(vesselDepartureDate);

    await this.filesService.upsertByCode({
      resourceId: bookingId,
      resource: 'bookings',
      code: 'release_order',
      file,
    });

    await this.bookingsRepository.update(bookingId, {
      vesselDepartureDate,
      roAmendmentRequestedAt: null,
    } as never);

    if (leadDays < minDays) {
      const reason = `Vessel departs in ${leadDays} day(s) — minimum lead time is ${minDays} day(s). Request a port amendment or upload a new RO with a later date.`;
      await this.bookingsRepository.update(bookingId, {
        roHoldReason: reason,
        clearanceCurrentPhase: ContractDocPhase.GlDjCollection,
      } as never);
      return {
        booking: await this.bookingsService.findById(bookingId),
        hold: true,
        holdReason: reason,
      };
    }

    await this.bookingsRepository.update(bookingId, {
      roHoldReason: null,
      clearanceCurrentPhase: ContractDocPhase.GlEtOutput,
    } as never);
    await this.workflowService.completeMilestoneForBooking(
      bookingId,
      'RELEASE_ORDER_SECURED',
      userId,
    );

    return { booking: await this.bookingsService.findById(bookingId), hold: false };
  }

  async requestRoAmendment(
    bookingId: string,
    note?: string,
    userId?: string,
  ): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'EXPORT') {
      throw new BadRequestException('RO amendment applies only to export bookings.');
    }

    const reason =
      note?.trim() ||
      'Port amendment requested — vessel departure window is too short. A new Release Order will be required.';

    await this.bookingsRepository.update(bookingId, {
      roAmendmentRequestedAt: new Date(),
      roHoldReason: reason,
      clearanceCurrentPhase: ContractDocPhase.GlDjCollection,
    } as never);

    if (userId) {
      await this.bookingsRepository.createReviewNote(
        bookingId,
        reason,
        'CHANGES_REQUESTED',
        userId,
      );
    }

    return this.bookingsService.findById(bookingId);
  }

  async confirmExportRelease(bookingId: string, userId?: string): Promise<Booking> {
    const booking = await this.loadBooking(bookingId);
    if (booking.tradeDirection !== 'EXPORT') {
      throw new BadRequestException('Export release applies only to export bookings.');
    }
    await this.workflowService.assertPriorCompleteForBooking(
      bookingId,
      'EXPORT',
      'EXPORT_RELEASED',
    );
    await this.workflowService.onExportReleasedForBooking(bookingId, userId);
    return this.bookingsService.findById(bookingId);
  }

  async etQueue(): Promise<Booking[]> {
    const candidates = await this.bookingsRepository.findByStatuses([
      ...PHASED_CUSTOMS_BOOKING_QUEUE_STATUSES,
    ]);
    const filtered: Booking[] = [];
    for (const b of candidates) {
      if (!this.isPhasedGeneralCustomsBooking(b)) continue;
      const milestones = await this.workflowService.listMilestonesForBooking(b.id);
      if (belongsOnEtClearanceQueue(milestones)) filtered.push(b);
    }
    return filtered;
  }

  async djQueue(): Promise<Booking[]> {
    const candidates = await this.bookingsRepository.findByStatuses([
      ...DJ_BOOKING_QUEUE_STATUSES,
    ]);
    const filtered: Booking[] = [];
    for (const b of candidates) {
      if (!this.isPhasedGeneralCustomsBooking(b)) continue;
      const milestones = await this.workflowService.listMilestonesForBooking(b.id);
      if (
        belongsOnDjClearanceQueue(b.tradeDirection, null, milestones, {
          roHoldReason: b.roHoldReason,
          preClearanceFinalizedAt: b.preClearanceFinalizedAt,
        })
      ) {
        filtered.push(b);
      }
    }
    return filtered;
  }
}
