import {
  Body,
  Controller,
  Delete,
  Get,
  HttpCode,
  Param,
  ParseUUIDPipe,
  Patch,
  Post,
  Put,
  Query,
  Res,
  UnauthorizedException,
  UploadedFiles,
  UploadedFile,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { JwtGuard } from '@tria-plc/api-common/modules/auth/services/jwt.guard';
import { AnyFilesInterceptor, FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import {
  ApiBearerAuth,
  ApiBody,
  ApiConsumes,
  ApiOkResponse,
  ApiOperation,
  ApiTags,
} from '@nestjs/swagger';

import { actorLabel } from '../warehouses/current-actor.util';
import { BookingStaff } from '../../common/booking-guards';
import { ContractDocumentHistoryService } from './contract-document-history.service';
import {
  FREIGHT_PERMS,
  bothFreightTypes,
  forFreightType,
} from '../../seed/freight-permissions.registry';
import {
  assertFreightPermission,
  hasFreightPermission,
} from '../../common/freight-permission.util';
import {
  type AuthUserPayload,
  resolveAuthUserId,
} from '../../common/resolve-auth-user-id';

import { ContractsService } from './contracts.service';
import { ContractPricingService } from './contract-pricing.service';
import { ContractTransitionService } from './contract-transition.service';
import { ContractClearanceService } from './contract-clearance.service';
import { BookingClearanceService } from './booking-clearance.service';
import { ContractBookingService } from './contract-booking.service';
import { ClearanceMilestoneService } from './clearance-milestone.service';
import { GlOperationsService } from './gl-operations.service';
import { BookingRequestService } from './booking-request.service';
import { SignaturesService } from '../signatures/signatures.service';
import { BookingsService } from '../bookings/bookings.service';
import { CreateContractDto } from './dto/create-contract.dto';
import { UpdateContractDto } from './dto/update-contract.dto';
import { FilterContractDto } from './dto/filter-contract.dto';
import { ContractListSummaryDto } from './dto/contract-list-summary.dto';
import { AcceptContractDto } from './dto/accept-contract.dto';
import { UpdateContractDocumentDto } from './dto/contract-document.dto';
import {
  RejectContractDto,
  RejectStepDto,
  RequestChangesDto,
} from './dto/approve-step.dto';
import { SignContractDto } from './dto/sign-contract.dto';
import { ReviewClearanceDocumentDto } from './dto/review-clearance-document.dto';
import { RenewContractDto } from './dto/renew-contract.dto';
import { CreateBookingUnderContractDto } from './dto/create-booking-under-contract.dto';
import {
  CreateBookingRequestDto,
  ReviewBookingRequestDto,
} from './dto/create-booking-request.dto';
import {
  AdviseDutyDto,
  AssignRiskDto,
  AssignStationDto,
  CompleteMilestoneDto,
  ReportIncidentDto,
} from './dto/gl-operations.dto';
import {
  AdviseContractDutyDto,
  RoAmendmentDto,
} from './dto/phased-clearance.dto';

@ApiTags('contracts')
@Controller('contracts')
@ApiBearerAuth()
export class ContractsController {
  constructor(
    private readonly documentHistory: ContractDocumentHistoryService,
    private readonly contractsService: ContractsService,
    private readonly pricingService: ContractPricingService,
    private readonly transitionService: ContractTransitionService,
    private readonly clearanceService: ContractClearanceService,
    private readonly contractBookingService: ContractBookingService,
    private readonly milestoneService: ClearanceMilestoneService,
    private readonly glOperationsService: GlOperationsService,
    private readonly bookingRequestService: BookingRequestService,
    private readonly signaturesService: SignaturesService,
    private readonly bookingClearanceService: BookingClearanceService,
    private readonly bookingsService: BookingsService,
  ) {}

  // ── Shipment / booking requests (GENERAL + customs, Path B) ───────────────
  // STATIC routes declared before any `:id`-param route so Nest matches them
  // (mirrors the clearance/queue ordering below).

  @Get('booking-requests/queue')
  @BookingStaff(FREIGHT_PERMS.contracts.createBooking)
  @ApiOperation({ summary: 'GL queue: shipment requests across contracts (all statuses, newest first)' })
  bookingRequestQueue() {
    return this.bookingRequestService.queue();
  }

  @Get('booking-requests/:reqId')
  @ApiOperation({ summary: 'A single shipment request' })
  getBookingRequest(@Param('reqId', ParseUUIDPipe) reqId: string) {
    return this.bookingRequestService.findOne(reqId);
  }

  @Post('booking-requests/:reqId/accept')
  @BookingStaff(FREIGHT_PERMS.contracts.createBooking)
  @ApiOperation({ summary: 'GL marks a shipment request accepted + links the created booking' })
  acceptBookingRequest(
    @Param('reqId', ParseUUIDPipe) reqId: string,
    @Body() body: { bookingId: string },
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.bookingRequestService.accept(
      reqId,
      body.bookingId,
      resolveAuthUserId(user),
    );
  }

  @Post('booking-requests/:reqId/reject')
  @BookingStaff(FREIGHT_PERMS.contracts.createBooking)
  @ApiOperation({ summary: 'GL rejects a shipment request' })
  rejectBookingRequest(
    @Param('reqId', ParseUUIDPipe) reqId: string,
    @Body() dto: ReviewBookingRequestDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.bookingRequestService.reject(reqId, dto.note, resolveAuthUserId(user));
  }

  @Post('booking-requests/:reqId/cancel')
  @ApiOperation({ summary: 'Customer cancels their own pending shipment request' })
  cancelBookingRequest(
    @Param('reqId', ParseUUIDPipe) reqId: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.bookingRequestService.cancel(reqId, resolveAuthUserId(user));
  }

  @Post(':id/booking-requests')
  @ApiOperation({ summary: 'Customer submits a shipment request on a GENERAL customs contract' })
  submitBookingRequest(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: CreateBookingRequestDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.bookingRequestService.submit(id, dto, resolveAuthUserId(user));
  }

  @Get(':id/booking-requests')
  @ApiOperation({ summary: 'List the shipment requests on a contract' })
  listBookingRequests(@Param('id', ParseUUIDPipe) id: string) {
    return this.bookingRequestService.listForContract(id);
  }

  @Post()
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Create a new contract (DRAFT) with routes + cargo scope' })
  @ApiBody({ type: CreateContractDto })
  async create(
    @Body() dto: CreateContractDto,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: TCurrentUser,
  ) {
    if (dto.isGovernment) {
      assertFreightPermission(
        user,
        forFreightType(FREIGHT_PERMS.contracts.staffAccept, dto.freightType),
      );
    }
    return this.contractsService.create(dto, files ?? [], user?.id);
  }

  @Get()
  @ApiOperation({ summary: 'List contracts (paginated)' })
  async findAll(
    @Query() filter: FilterContractDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Staff see every contract; customers are force-scoped to their own company.
    if (
      hasFreightPermission(user, FREIGHT_PERMS.bookings.view) ||
      hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      return this.contractsService.findAll(filter);
    }
    const userId = user?.id;
    if (!userId) throw new UnauthorizedException('Authentication required');
    const companyId = await this.contractsService.resolveCustomerCompanyId(userId);
    if (!companyId) {
      const page = filter.page ?? 1;
      const pageSize = filter.pageSize ?? 20;
      return {
        items: [],
        total: 0,
        meta: {
          page,
          pageSize,
          total: 0,
          totalPages: 0,
          hasNextPage: false,
          hasPreviousPage: false,
        },
      };
    }
    return this.contractsService.findAll(filter, companyId);
  }

  @Get('my')
  @ApiOperation({ summary: "List the current customer's contracts" })
  async findMy(
    @CurrentUser() user: AuthUserPayload,
    @Query() filter: FilterContractDto,
  ) {
    const userId = resolveAuthUserId(user);
    const companyId = await this.contractsService.resolveCustomerCompanyId(userId);
    if (!companyId) {
      const page = filter.page ?? 1;
      const pageSize = filter.pageSize ?? 20;
      return {
        items: [],
        total: 0,
        meta: {
          page,
          pageSize,
          total: 0,
          totalPages: 0,
          hasNextPage: false,
          hasPreviousPage: false,
        },
      };
    }
    return this.contractsService.findAll(filter, companyId);
  }

  @Get('list-summary')
  @BookingStaff([FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.contracts.view])
  @ApiOperation({ summary: 'Contract list metrics and status counts (backoffice)' })
  @ApiOkResponse({ type: ContractListSummaryDto })
  findListSummary(@Query() filter: FilterContractDto) {
    return this.contractsService.getListSummary(filter);
  }

  @Get('clearance/queue')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview)
  @ApiOperation({ summary: 'GL ET queue: contracts awaiting pre-booking document review' })
  clearanceQueue(@Query() filter: FilterContractDto) {
    return this.clearanceService.queue(filter);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Get contract by ID (routes, cargo scope, unit rates)' })
  async findOne(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const contract = await this.contractsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.reviewDocuments) &&
      !hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      await this.contractsService.assertCustomerCanAccessContract(user?.id, contract);
    }
    return contract;
  }

  @Patch(':id')
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary: 'Update contract',
    description: 'Allowed when status is DRAFT or CHANGES_REQUESTED.',
  })
  @ApiBody({ type: UpdateContractDto })
  update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: UpdateContractDto,
    @UploadedFiles() files: Express.Multer.File[],
    // Recorded on the edit's audit revision — who changed the contract.
    @CurrentUser() user?: TCurrentUser,
  ) {
    return this.contractsService.update(id, dto, files ?? [], user?.id);
  }

  @Delete(':id')
  @HttpCode(204)
  @ApiOperation({ summary: 'Soft-delete DRAFT contract' })
  remove(@Param('id', ParseUUIDPipe) id: string) {
    return this.contractsService.remove(id);
  }

  @Post(':id/documents')
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Upload intake documents for a contract (DRAFT only)' })
  uploadDocuments(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.contractsService.uploadDocuments(id, files ?? []);
  }

  @Post(':id/generate-price')
  @ApiOperation({ summary: 'Generate unit-rate breakdown (no totals at contract phase)' })
  generatePrice(@Param('id', ParseUUIDPipe) id: string) {
    return this.pricingService.generatePrice(id);
  }

  @Post(':id/submit')
  @ApiOperation({ summary: 'Customer submit contract (freezes contract_rate_snapshots)' })
  submit(@Param('id', ParseUUIDPipe) id: string) {
    return this.transitionService.submit(id);
  }

  @Post(':id/confirm-submit')
  @ApiOperation({ summary: 'Confirm submit after a price change' })
  confirmSubmit(@Param('id', ParseUUIDPipe) id: string) {
    return this.transitionService.confirmSubmit(id);
  }

  @Post(':id/staff/accept')
  // One-of guard; the service then requires the arm matching the contract's freight type.
  @BookingStaff(bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept))
  @ApiOperation({ summary: 'Staff accept → set validity window + start approval chain' })
  staffAccept(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: AcceptContractDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.transitionService.staffAccept(
      id,
      resolveAuthUserId(user),
      dto.validityDays,
      dto.documentSnapshot,
      user,
      { validFrom: dto.validFrom, validUntil: dto.validUntil },
    );
  }

  // Readable by anyone who may view the contract: the draft carries
  // `editableByMe`, and the approval chain's approvers (identified by position
  // type, not by staff_accept) must be able to fetch it to learn it is their
  // turn. Gating this on staff_accept hid the edit dialog from every approver.
  @Get(':id/document/draft')
  @BookingStaff([
    FREIGHT_PERMS.contracts.view,
    ...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept),
  ])
  @ApiOperation({
    summary:
      'Editable contract-document draft (this contract\'s snapshot, or the live template) for the accept/edit dialog',
  })
  getContractDocumentDraft(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Editability depends on WHO is asking — only the approver whose turn it is
    // may edit — so the caller is part of the draft lookup.
    return this.transitionService.getContractDocumentDraft(id, user);
  }

  @Get(':id/document/revisions')
  @BookingStaff(FREIGHT_PERMS.contracts.view)
  @ApiOperation({
    summary: 'Audit trail of edits to this contract\'s document (newest first)',
  })
  getContractDocumentRevisions(@Param('id', ParseUUIDPipe) id: string) {
    return this.documentHistory.list(id);
  }

  // Coarse gate only. WHO may actually edit is turn-based, not a static
  // permission, so `updateContractDocument` -> `assertDocumentEditable` is the
  // real boundary: it admits only the approver whose step is currently pending
  // (edit rights hand off down the chain on each approval).
  @Put(':id/document/articles')
  @BookingStaff([
    FREIGHT_PERMS.contracts.view,
    ...bothFreightTypes(FREIGHT_PERMS.contracts.staffAccept),
  ])
  @ApiOperation({
    summary:
      'Edit this contract\'s document articles only (per-contract; never touches the six shared templates)',
  })
  updateContractDocument(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: UpdateContractDocumentDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.transitionService.updateContractDocument(
      id,
      dto,
      user,
      resolveAuthUserId(user),
    );
  }

  @Post(':id/staff/request-changes')
  @BookingStaff(bothFreightTypes(FREIGHT_PERMS.contracts.requestChanges))
  @ApiOperation({ summary: 'Staff return contract for customer updates' })
  requestChanges(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: RequestChangesDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.transitionService.requestChanges(
      id,
      dto.note,
      resolveAuthUserId(user),
      user,
    );
  }

  @Post(':id/staff/reject')
  @BookingStaff(bothFreightTypes(FREIGHT_PERMS.contracts.reject))
  @ApiOperation({ summary: 'Staff reject contract' })
  reject(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: RejectContractDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.transitionService.reject(
      id,
      dto.reason,
      resolveAuthUserId(user),
      user,
    );
  }

  @Post(':id/approval-steps/:stepId/approve')
  @BookingStaff(FREIGHT_PERMS.contracts.view)
  @ApiOperation({ summary: 'Approve one approval step in sequence' })
  approveStep(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('stepId', ParseUUIDPipe) stepId: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Whether this caller may approve depends on the step's own required role
    // (an IAM position type), so the service resolves the step and authorizes
    // against it — the client never declares its own role.
    return this.transitionService.approveStep(
      id,
      stepId,
      resolveAuthUserId(user),
      user,
    );
  }

  @Post(':id/approval-steps/:stepId/reject')
  @BookingStaff([
    FREIGHT_PERMS.contracts.approveLineStaff,
    FREIGHT_PERMS.contracts.approveDirector,
    FREIGHT_PERMS.contracts.approveCeo,
  ])
  @ApiOperation({
    summary:
      'Reject one approval step — to the customer (terminal → REJECTED) or, via returnToStepId, back to an earlier approver (chain re-runs from there)',
  })
  rejectStep(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('stepId', ParseUUIDPipe) stepId: string,
    @Body() dto: RejectStepDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.transitionService.rejectStep(
      id,
      stepId,
      resolveAuthUserId(user),
      dto.reason,
      dto.returnToStepId,
    );
  }

  @Post(':id/contract/generate')
  @BookingStaff(FREIGHT_PERMS.contracts.generateContract)
  @ApiOperation({ summary: 'Generate contract document → CONTRACT_READY' })
  generateContract(@Param('id', ParseUUIDPipe) id: string) {
    return this.transitionService.generateContract(id);
  }

  @Get(':id/contract/view')
  @ApiOperation({ summary: 'Contract PDF view-model + rendered HTML for signing' })
  async getContractView(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const contract = await this.contractsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      await this.contractsService.assertCustomerCanAccessContract(user?.id, contract);
    }
    const { view, html, signatures } =
      await this.transitionService.getContractDocumentView(id);
    // The signer's reusable saved signature (if any) so the sign UI can offer
    // "Approve & sign" with the stored image instead of forcing a fresh draw.
    const signerId = resolveAuthUserId(user);
    const savedSignature = signerId
      ? await this.signaturesService.getForUser(signerId)
      : null;
    return {
      contractId: view.bookingId,
      reference: view.reference,
      status: view.status,
      // Drives the per-freight-type sign permission on the client.
      freightType: contract.freightType,
      templateKey: view.templateKey,
      title: view.template.title,
      html,
      view,
      canSignCustomer: view.canSignCustomer,
      canSignStaff: view.canSignStaff,
      hasContractDocument: view.hasContractDocument,
      signatures,
      savedSignature,
    };
  }

  @Get(':id/contract/document')
  @ApiOperation({ summary: 'Download contract PDF' })
  async downloadContractDocument(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
    @Res() res: Response,
  ): Promise<void> {
    const contract = await this.contractsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      await this.contractsService.assertCustomerCanAccessContract(user?.id, contract);
    }
    const { stream, record } = await this.transitionService.streamContractPdf(id);
    res.setHeader('Content-Type', record.mimeType ?? 'application/pdf');
    res.setHeader(
      'Content-Disposition',
      `attachment; filename="${record.name}"`,
    );
    stream.pipe(res);
  }

  @Post(':id/contract/send-signing-otp')
  @UseGuards(JwtGuard)
  @ApiOperation({
    summary:
      "Send the sudo-mode signing OTP to the contract company's registered phone (server picks the number)",
  })
  sendSigningOtp(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.transitionService.sendSigningOtp(id, { signerUserId: user?.id });
  }

  @Post(':id/contract/sign')
  @UseGuards(JwtGuard)
  @ApiOperation({ summary: 'Apply digital signature (customer or staff/director/ceo)' })
  async signContract(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: SignContractDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    if (dto.role !== 'CUSTOMER') {
      // Each staff signing role maps to the permission that step already
      // requires; the STAFF counter-signature is split per freight type, so a
      // bulk signer cannot counter-sign a container contract (and vice versa).
      const contract = await this.contractsService.findById(id);
      const signRolePermission: Record<string, string> = {
        STAFF: forFreightType(FREIGHT_PERMS.contracts.signStaff, contract.freightType),
        DIRECTOR: FREIGHT_PERMS.contracts.approveDirector,
        CEO: FREIGHT_PERMS.contracts.approveCeo,
      };
      assertFreightPermission(user, signRolePermission[dto.role]);
    }
    return this.transitionService.sign(id, dto, {
      signerUserId: user?.id,
    });
  }

  @Post(':id/renew')
  @ApiOperation({ summary: 'Create a renewal draft linked via renewalOfId' })
  async renew(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() _dto: RenewContractDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    // H12(c): a customer may only renew a contract their company owns. Staff
    // with bookings.view/contracts.view bypass, mirroring getContractView/downloadContractDocument.
    const contract = await this.contractsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      await this.contractsService.assertCustomerCanAccessContract(user?.id, contract);
    }
    return this.transitionService.renew(id, resolveAuthUserId(user));
  }

  // ── Pre-booking clearance (Path B, doc §15.2.1) ────────────────────────────

  @Get(':id/clearance')
  @ApiOperation({ summary: 'Pre-booking clearance document grid on the contract' })
  getClearance(@Param('id', ParseUUIDPipe) id: string) {
    return this.clearanceService.getClearanceView(id);
  }

  @Post(':id/clearance/documents')
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer uploads clearance documents (fieldname = document key)' })
  async uploadClearanceDocuments(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    // H12(c): only the owning company's customer may upload clearance docs.
    // Staff with bookings.view/contracts.view bypass, mirroring the other contract handlers.
    const contract = await this.contractsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.contracts.view)
    ) {
      await this.contractsService.assertCustomerCanAccessContract(user?.id, contract);
    }
    return this.clearanceService.uploadDocuments(id, files ?? []);
  }

  @Post(':id/clearance/review')
  @BookingStaff([
    FREIGHT_PERMS.contracts.clearanceReview,
    FREIGHT_PERMS.contracts.clearanceEtActions,
  ])
  @ApiOperation({ summary: 'GL ET reviews a clearance document (Approve | Query)' })
  reviewClearanceDocument(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: ReviewClearanceDocumentDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.reviewDocument(
      id,
      dto.fileKey,
      dto.status,
      resolveAuthUserId(user),
      dto.note,
    );
  }

  @Post(':id/clearance/output-documents')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL uploads customs output documents (IM4/EX3/…) pre-booking' })
  uploadOutputDocuments(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.clearanceService.uploadOutputDocuments(id, files ?? []);
  }

  @Post(':id/clearance/finalize')
  @BookingStaff(FREIGHT_PERMS.contracts.finalizeClearance)
  @ApiOperation({ summary: 'GL ET finalizes clearance → CLEARANCE_READY_FOR_BOOKING (legacy)' })
  finalizeClearance(@Param('id', ParseUUIDPipe) id: string) {
    return this.clearanceService.finalize(id);
  }

  @Post(':id/clearance/declaration')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET uploads customs declaration documents (multi-file)' })
  uploadDeclaration(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.uploadDeclaration(id, files ?? [], resolveAuthUserId(user));
  }

  @Post(':id/clearance/duty')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDutyAdvise)
  @UseInterceptors(FileInterceptor('attachment'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET sets duty/tax requirement and advises amount with notice attachment' })
  adviseContractDuty(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('dutyRequired') dutyRequiredRaw: string,
    @Body('amount') amountRaw: string | undefined,
    @Body('currency') currency: string | undefined,
    @Body('declarationSerial') declarationSerial: string | undefined,
    @UploadedFile() attachment: Express.Multer.File | undefined,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const dutyRequired = dutyRequiredRaw === 'true' || dutyRequiredRaw === '1';
    const dto: AdviseContractDutyDto = {
      dutyRequired,
      amount:
        amountRaw != null && amountRaw !== '' ? Number(amountRaw) : undefined,
      currency: currency ?? 'ETB',
      declarationSerial,
    };
    return this.clearanceService.adviseDuty(
      id,
      dto,
      resolveAuthUserId(user),
      attachment,
    );
  }

  @Post(':id/clearance/finalize-pre-clearance')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({ summary: 'GL ET finalizes import pre-clearance — unlocks Djibouti DO upload' })
  finalizePreClearance(@Param('id', ParseUUIDPipe) id: string) {
    return this.clearanceService.finalizePreClearance(id);
  }

  @Post(':id/clearance/transit-assignee/request')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({
    summary:
      'GL ET asks GL Djibouti to name the transit officer — required before the customs declaration',
  })
  requestTransitAssignee(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('note') note: string | undefined,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.requestTransitAssignee(
      id,
      note,
      resolveAuthUserId(user),
    );
  }

  @Post(':id/clearance/transit-assignee/assign')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @ApiOperation({
    summary:
      'GL Djibouti names the transit officer (free text) — unblocks the customs declaration; calling again reassigns',
  })
  assignTransitAssignee(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('assignee') assignee: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.assignTransitAssignee(
      id,
      assignee,
      resolveAuthUserId(user),
    );
  }

  @Get(':id/clearance/documents/:fileKey/versions')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview)
  @ApiOperation({
    summary:
      'Version history of one clearance document — the customer original plus every staff replacement',
  })
  documentVersions(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('fileKey') fileKey: string,
  ) {
    return this.clearanceService.documentVersions(id, fileKey);
  }

  @Post(':id/clearance/documents/:fileKey/replace')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary:
      'GL replaces a clearance document in place (reason required) — the previous version is kept in the file history and the new one needs approving',
  })
  replaceClearanceDocument(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('fileKey') fileKey: string,
    @UploadedFile() file: Express.Multer.File,
    @Body('reason') reason: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.replaceDocument(
      id,
      fileKey,
      file,
      resolveAuthUserId(user),
      reason,
    );
  }

  @Post(':id/clearance/duty/dispute')
  @ApiOperation({
    summary:
      'Customer disputes the advised duty/tax with a reason — reopens the step so GL Ethiopia can re-advise (repeatable)',
  })
  disputeContractDuty(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('note') note: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.disputeDuty(id, note, resolveAuthUserId(user));
  }

  @Post(':id/clearance/duty-slip')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer uploads duty/tax payment slip on contract' })
  uploadContractDutySlip(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    return this.clearanceService.uploadDutySlip(id, file);
  }

  @Post(':id/clearance/transit-permit')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET uploads import transit permit documents (multi-file)' })
  uploadTransitPermit(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.uploadTransitPermit(id, files ?? [], resolveAuthUserId(user));
  }

  @Post(':id/clearance/delivery-order')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary:
      'GL DJ uploads Delivery Order (import) with vessel arrival + DO collected dates',
  })
  uploadDeliveryOrder(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
    @Body('vesselArrivalDate') vesselArrivalDate: string | undefined,
    @Body('doCollectedDate') doCollectedDate: string | undefined,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.uploadDeliveryOrder(id, file, resolveAuthUserId(user), {
      vesselArrivalDate,
      doCollectedDate,
    });
  }

  @Post(':id/clearance/release-order')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL DJ uploads Release Order + vessel departure date (export)' })
  uploadReleaseOrder(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
    @Body('vesselDepartureDate') vesselDepartureDate: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.uploadReleaseOrder(
      id,
      file,
      vesselDepartureDate,
      resolveAuthUserId(user),
    );
  }

  @Post(':id/clearance/ro-amendment')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @ApiOperation({ summary: 'GL DJ requests port amendment when RO vessel window is too short' })
  requestRoAmendment(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: RoAmendmentDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.requestRoAmendment(id, dto.note, resolveAuthUserId(user));
  }

  @Post(':id/clearance/export-release')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({ summary: 'GL ET confirms export release after declaration' })
  confirmExportRelease(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.confirmExportRelease(id, resolveAuthUserId(user));
  }

  @Post(':id/clearance/finalize-export-clearance')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({
    summary: 'GL ET finalizes export clearance after post-booking transit permit upload',
  })
  finalizeExportClearance(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.finalizeExportClearance(id, resolveAuthUserId(user));
  }

  @Get('clearance/et-queue')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({ summary: 'GL Ethiopia phased clearance list (persistent after booking)' })
  etClearanceQueue(@Query() filter: FilterContractDto) {
    return this.clearanceService.etQueue(filter);
  }

  @Get('clearance/dj-queue')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @ApiOperation({ summary: 'GL Djibouti phased clearance list (persistent after booking)' })
  djClearanceQueue(@Query() filter: FilterContractDto) {
    return this.clearanceService.djQueue(filter);
  }

  // ── Path A self-clearance — Operations reviews the customer's own docs ───────

  @Get('clearance/ops-queue')
  @BookingStaff(FREIGHT_PERMS.contracts.opsClearanceReview)
  @ApiOperation({
    summary: 'Operations queue: self-clearance (non-customs) contracts awaiting review',
  })
  opsClearanceQueue(@Query() filter: FilterContractDto) {
    return this.clearanceService.opsQueue(filter);
  }

  @Post(':id/clearance/ops-review')
  @BookingStaff(FREIGHT_PERMS.contracts.opsClearanceReview)
  @ApiOperation({
    summary: 'Operations reviews a customer self-clearance document (Approve | Query)',
  })
  opsReviewClearanceDocument(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: ReviewClearanceDocumentDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.clearanceService.opsReviewDocument(
      id,
      dto.fileKey,
      dto.status,
      resolveAuthUserId(user),
      dto.note,
    );
  }

  @Post(':id/clearance/ops-finalize')
  @BookingStaff(FREIGHT_PERMS.contracts.opsClearanceReview)
  @ApiOperation({
    summary: 'Operations finalizes self-clearance → customer may create the booking',
  })
  opsFinalizeClearance(@Param('id', ParseUUIDPipe) id: string) {
    return this.clearanceService.opsFinalize(id);
  }

  @Get('clearance/history')
  @BookingStaff(FREIGHT_PERMS.contracts.finalizeClearance)
  @ApiOperation({ summary: 'GL ET clearance history: contracts that completed Path B clearance' })
  clearanceHistory(@Query() filter: FilterContractDto) {
    return this.clearanceService.history(filter);
  }

  @Get('clearance/ops-history')
  @BookingStaff(FREIGHT_PERMS.contracts.opsClearanceReview)
  @ApiOperation({ summary: 'Operations clearance history: contracts that completed Path A self-clearance' })
  opsClearanceHistory(@Query() filter: FilterContractDto) {
    return this.clearanceService.opsHistory(filter);
  }

  // ── Booking under contract (Path A customer / Path B GL ET) ────────────────

  @Post(':id/bookings')
  @ApiOperation({
    summary:
      'Create a shipment booking under a contract — Path A (customer) or Path B (GL Ethiopia).',
  })
  createBooking(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: CreateBookingUnderContractDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    // The service decides the execution path from the contract:
    //   Path A (customs disabled) → customer/staff create; status checks apply.
    //   Path B (customs enabled)  → GL Ethiopia only, once clearance is ready.
    return this.contractBookingService.createUnderContract(
      id,
      dto,
      { id: user?.id ?? user?.sub },
      user,
    );
  }

  @Post(':id/bookings/initiate')
  @ApiOperation({
    summary:
      'Initiate a bare booking instance under a GENERAL non-customs contract — no cargo, no date; enters per-booking clearance (AWAITING_DOCUMENTS).',
  })
  initiateBooking(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: CreateBookingUnderContractDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.contractBookingService.initiateUnderContract(
      id,
      { contractRouteId: dto?.contractRouteId },
      { id: user?.id ?? user?.sub },
      user,
    );
  }

  @Post(':id/bookings/:bookingId/complete')
  @ApiOperation({
    summary:
      'Complete an initiated booking after Operations finalized its clearance — cargo + binding day, window and departure checks, pricing and invoicing.',
  })
  completeBooking(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body() dto: CreateBookingUnderContractDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    // Customs (Path B) instances may only be completed by GL Ethiopia — the
    // service checks the actor's contracts:create_booking permission.
    return this.contractBookingService.completeUnderContract(
      id,
      bookingId,
      dto,
      user,
    );
  }

  @Post(':id/validate-shipment')
  @ApiOperation({
    summary:
      'Pre-create validation + authoritative price preview: full booking price breakdown (rail, first/last mile, surcharges), overweight lines and 20ft weight-pairing errors for a shipment payload (no booking created).',
  })
  validateShipment(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: CreateBookingUnderContractDto,
  ) {
    return this.contractBookingService.validateShipment(id, dto);
  }

  @Get(':id/capacity')
  @ApiOperation({
    summary:
      'Remaining bookable quantity per cargo line (GENERAL draw-down cap, or the outstanding remainder of a split ONE_TIME contract)',
  })
  async capacity(@Param('id', ParseUUIDPipe) id: string) {
    const contract = await this.contractsService.findById(id);
    return this.contractBookingService.capacityView(contract);
  }

  // ── Clearance milestones (doc §11.3, §12.2) ────────────────────────────────

  @Get(':id/milestones')
  @ApiOperation({ summary: 'Pre-booking clearance milestones for a contract cycle' })
  listContractMilestones(@Param('id', ParseUUIDPipe) id: string) {
    return this.milestoneService.listForContract(id);
  }

  @Get('bookings/:bookingId/milestones')
  @ApiOperation({ summary: 'Post-booking clearance milestones for a shipment booking' })
  listBookingMilestones(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
    return this.milestoneService.listForBooking(bookingId);
  }

  @Post('bookings/:bookingId/milestones/:code/complete')
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: 'GL / Ops / Terminal marks a post-booking milestone complete' })
  completeBookingMilestone(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Param('code') code: string,
    @Body() body: { note?: string },
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.milestoneService.completeForBooking(
      bookingId,
      code,
      user?.id ?? user?.sub,
      body?.note,
    );
  }

  @Post(':id/milestones/:code/complete')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceReview)
  @ApiOperation({ summary: 'GL marks a pre-booking (contract) milestone complete' })
  completeContractMilestone(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('code') code: string,
    @Body() body: CompleteMilestoneDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.milestoneService.completeForContract(
      id,
      code,
      resolveAuthUserId(user),
      body?.note,
    );
  }

  // ── GL operational actions on a booking (doc §11–§13) ──────────────────────

  @Post('bookings/:bookingId/risk')
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: 'GL ET assigns a customs risk level (GREEN/YELLOW/RED)' })
  assignRisk(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body() dto: AssignRiskDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    return this.milestoneService.assignRisk(
      bookingId,
      dto.riskLevel,
      resolveAuthUserId(user),
      dto.note,
      // Risk history is read by people, so resolve the name now — the id alone
      // would render as a UUID in the trail.
      actorLabel(user),
    );
  }

  @Post('bookings/:bookingId/duty')
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: 'GL ET advises duty & tax amount + declaration serial' })
  adviseDuty(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body() dto: AdviseDutyDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.milestoneService.adviseDuty(
      bookingId,
      { amount: dto.amount, currency: dto.currency, declarationSerial: dto.declarationSerial },
      resolveAuthUserId(user),
      dto.note,
    );
  }

  @Post('bookings/:bookingId/station-assign')
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: 'GL station manager routes the shipment + binds staff' })
  assignStation(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body() dto: AssignStationDto,
  ) {
    return this.glOperationsService.assignStation(bookingId, {
      stationYardId: dto.stationYardId,
      staffId: dto.staffId,
    });
  }

  @Post('bookings/:bookingId/transport-document')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET uploads export transit permit documents (multi-file)' })
  uploadTransportDocument(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.glOperationsService.uploadTransportDocument(bookingId, files ?? []);
  }

  @Post('bookings/:bookingId/t1-documents')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary:
      'GL Djibouti uploads T1 transit documents (multi-file) after wagon allocation; locked once the train departs',
  })
  uploadT1Documents(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.glOperationsService.uploadT1Documents(bookingId, files ?? []);
  }

  @Post('bookings/:bookingId/t1-close')
  @BookingStaff([
    FREIGHT_PERMS.contracts.clearanceEtActions,
    FREIGHT_PERMS.contracts.clearanceDjActions,
  ])
  @ApiOperation({
    summary:
      'Close (accept) the T1 set — GL ET after arrival (import) / GL DJ after gate pass (export)',
  })
  closeT1(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.glOperationsService.closeT1(bookingId, resolveAuthUserId(user));
  }

  @Post('bookings/:bookingId/final-invoice')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary: 'GL DJ raises the post-offload final invoice (amount + invoice document)',
  })
  createFinalInvoice(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body('amount') amountRaw: string,
    @Body('currency') currency: string | undefined,
    @Body('description') description: string | undefined,
    @UploadedFile() file: Express.Multer.File,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.glOperationsService.createFinalInvoice(
      bookingId,
      {
        amount: Number(amountRaw),
        currency: currency?.trim() || 'ETB',
        description,
      },
      file,
      resolveAuthUserId(user),
    );
  }

  @Post('bookings/:bookingId/final-invoice-slip')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer attaches the payment slip for the final invoice' })
  uploadFinalInvoiceSlip(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    return this.glOperationsService.uploadFinalInvoiceSlip(bookingId, file);
  }

  @Post('bookings/:bookingId/final-invoice/confirm')
  @BookingStaff([
    FREIGHT_PERMS.contracts.clearanceDjActions,
    FREIGHT_PERMS.contracts.clearanceEtActions,
  ])
  @ApiOperation({ summary: 'GL (ET or DJ) confirms the payment slip — settles the final invoice' })
  confirmFinalInvoicePaid(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.glOperationsService.confirmFinalInvoicePaid(
      bookingId,
      resolveAuthUserId(user),
    );
  }

  @Post('bookings/:bookingId/second-duty')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(FileInterceptor('attachment'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary: 'GL ET advises (or skips) the post-arrival additional duty/tax round (import)',
  })
  adviseSecondDuty(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body('dutyRequired') dutyRequiredRaw: string,
    @Body('amount') amountRaw: string | undefined,
    @Body('currency') currency: string | undefined,
    @Body('declarationSerial') declarationSerial: string | undefined,
    @UploadedFile() attachment: Express.Multer.File | undefined,
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.glOperationsService.adviseSecondDuty(
      bookingId,
      {
        dutyRequired: dutyRequiredRaw === 'true' || dutyRequiredRaw === '1',
        amount:
          amountRaw != null && amountRaw !== '' ? Number(amountRaw) : undefined,
        currency: currency ?? 'ETB',
        declarationSerial,
      },
      attachment,
      resolveAuthUserId(user),
    );
  }

  @Post('bookings/:bookingId/second-duty-slip')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer attaches the additional duty/tax payment slip' })
  uploadSecondDutySlip(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    return this.glOperationsService.uploadSecondDutySlip(bookingId, file);
  }

  @Post('bookings/:bookingId/documents')
  @BookingStaff(FREIGHT_PERMS.bookings.uploadClearanceOutput)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary: 'GL uploads post-booking operational documents (DO/RO/T1/…)',
  })
  uploadGlDocuments(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.glOperationsService.uploadDocuments(bookingId, files ?? []);
  }

  @Post('bookings/:bookingId/duty-slip')
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer uploads the duty/tax payment slip' })
  async uploadDutySlip(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    const file = (files ?? [])[0];
    const booking = await this.bookingsService.findById(bookingId);
    if (this.bookingClearanceService.isPhasedGeneralCustomsBooking(booking)) {
      return this.bookingClearanceService.uploadDutySlip(bookingId, file);
    }
    return this.glOperationsService.uploadDutySlip(bookingId, file);
  }

  @Get('bookings/:bookingId/incidents')
  @ApiOperation({ summary: 'List cargo exception/damage reports for a shipment' })
  listIncidents(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
    return this.glOperationsService.listIncidents(bookingId);
  }

  @Post('bookings/:bookingId/incidents')
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL DJ logs a cargo exception with photo evidence' })
  reportIncident(
    @Param('bookingId', ParseUUIDPipe) bookingId: string,
    @Body() dto: ReportIncidentDto,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: AuthUserPayload,
  ) {
    return this.glOperationsService.reportIncident(
      bookingId,
      {
        incidentType: dto.incidentType,
        description: dto.description,
        files: files ?? [],
      },
      resolveAuthUserId(user),
    );
  }
}
