import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  Param,
  ParseUUIDPipe,
  Patch,
  Post,
  Query,
  Request,
  Res,
  UnauthorizedException,
  UploadedFile,
  UploadedFiles,
  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 { BookingStaff, BookingView } from '../../common/booking-guards';
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { AnyFilesInterceptor, FileInterceptor } from '@nestjs/platform-express';
import {
  ApiBearerAuth,
  ApiBody,
  ApiConsumes,
  ApiOkResponse,
  ApiOperation,
  ApiTags,
} from "@nestjs/swagger";
import type { Response } from "express";

import { BookingContractService } from './booking-contract.service';
import { BookingPricingService } from './booking-pricing.service';
import { BookingTransitionService } from './booking-transition.service';
import { BookingClearanceService } from '../contracts/booking-clearance.service';
import {
  AdviseContractDutyDto,
  RoAmendmentDto,
} from '../contracts/dto/phased-clearance.dto';
import { BookingReferenceDataService } from './booking-reference-data.service';
import { BookingsService } from './bookings.service';
import { BookingReferenceDataDto } from './dto/booking-reference-data.dto';
import { CreateBookingDto } from './dto/create-booking.dto';
import { BookingListSummaryDto } from './dto/booking-list-summary.dto';
import { FilterBookingDto } from './dto/filter-booking.dto';
import { GeneratePriceResponseDto } from './dto/generate-price-response.dto';
import { SubmitBookingResponseDto } from './dto/submit-booking-response.dto';
import {
  AcceptIntakeDto,
  CancelBookingDto,
  RejectBookingDto,
  RequestChangesDto,
  ReviewDocumentDto,
  RequestOperationDto,
  OperationReviewDto,
  StaffRejectDto,
} from './dto/request-changes.dto';
import { ContractViewDto } from './dto/contract-view.dto';
import { CustomerTruckAssignmentDto } from './dto/customer-truck-assignment.dto';
import { AddCustomerTruckDto } from './dto/add-customer-truck.dto';
import { DepartCustomerTruckDto } from './dto/depart-customer-truck.dto';
import { LoadCustomerTruckDto } from './dto/load-customer-truck.dto';
import { CustomerTruckService } from './customer-truck.service';
import { FirstMileService } from '../first-mile/first-mile.service';
import { LastMileService } from '../last-mile/last-mile.service';
import { GenerateGrnDto } from './dto/generate-grn.dto';
import { ContainerReceiptService } from './container-receipt.service';
import { SignContractDto } from './dto/sign-contract.dto';
import { UpdateBookingDto } from './dto/update-booking.dto';
import {
  type AuthUserPayload,
  resolveAuthUserId,
} from "../../common/resolve-auth-user-id";
import {
  assertFreightPermission,
  hasFreightPermission,
} from "../../common/freight-permission.util";

interface MileVehicleSummary {
  plate: string | null;
  code: string | null;
  driverName: string | null;
  containerNumber: string | null;
  distanceKm: number | null;
}

interface MileLegSummary {
  status: string;
  exactKm: number | null;
  remainingPayment: number | null;
  currency: string;
  invoiced: boolean;
  vehicles: MileVehicleSummary[];
}

/** Trim a first/last-mile record down to a customer-safe operational summary. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function summarizeMileLeg(rec?: Record<string, any>): MileLegSummary | null {
  if (!rec) return null;
  const num = (v: unknown) => (v == null ? null : Number(v));
  const assignments: Array<Record<string, any>> = rec.vehicleAssignments ?? []; // eslint-disable-line @typescript-eslint/no-explicit-any
  const currency =
    rec.vehicle?.currency ??
    assignments[0]?.vehicle?.currency ??
    rec.booking?.paymentCurrency ??
    'ETB';
  const vehicles: MileVehicleSummary[] = assignments.map((a) => ({
    plate: a.vehicle?.plateNumber ?? null,
    code: a.vehicle?.code ?? null,
    driverName: a.vehicle?.assignedDriverName ?? null,
    containerNumber: a.containerNumber ?? null,
    distanceKm: num(a.distanceKm),
  }));
  if (!vehicles.length && rec.vehicle) {
    vehicles.push({
      plate: rec.vehicle.plateNumber ?? null,
      code: rec.vehicle.code ?? null,
      driverName: rec.vehicle.assignedDriverName ?? null,
      containerNumber: null,
      distanceKm: num(rec.exactKm),
    });
  }
  return {
    status: rec.status ?? '',
    exactKm: num(rec.exactKm),
    remainingPayment: num(rec.remainingPayment),
    currency,
    invoiced: Boolean(rec.invoice),
    vehicles,
  };
}

@ApiTags("bookings")
@Controller("bookings")
@ApiBearerAuth()
export class BookingsController {
  constructor(
    private readonly bookingsService: BookingsService,
    private readonly bookingReferenceDataService: BookingReferenceDataService,
    private readonly pricingService: BookingPricingService,
    private readonly transitionService: BookingTransitionService,
    private readonly contractService: BookingContractService,
    private readonly bookingClearanceService: BookingClearanceService,
    private readonly customerTruckService: CustomerTruckService,
    private readonly containerReceiptService: ContainerReceiptService,
    private readonly firstMileService: FirstMileService,
    private readonly lastMileService: LastMileService,
  ) {}

  @Post()
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes("multipart/form-data")
  @ApiOperation({ summary: "Create a new freight booking (DRAFT)" })
  @ApiBody({ type: CreateBookingDto })
  async create(
    @Body() dto: CreateBookingDto,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: TCurrentUser,
  ) {
    if (dto.isGovernment) {
      assertFreightPermission(user, FREIGHT_PERMS.bookings.staffAccept);
    }
    const result = await this.bookingsService.create(
      dto,
      files ?? [],
      user?.id,
    );

    // Staff-created commercial bookings skip the draft stage: auto generate-price + submit.
    const isStaff = hasFreightPermission(
      user,
      FREIGHT_PERMS.bookings.staffAccept,
    );
    if (isStaff && !dto.isGovernment) {
      try {
        await this.pricingService.generatePrice(result.booking.id);
        await this.transitionService.submit(result.booking.id);
        const submitted = await this.bookingsService.findById(
          result.booking.id,
        );
        return { booking: submitted, warnings: result.warnings };
      } catch {
        // If auto-pricing/submit fails, fall back to the DRAFT so staff can finish manually.
        return result;
      }
    }
    return result;
  }

  @Patch(":id")
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes("multipart/form-data")
  @ApiOperation({
    summary: "Update booking",
    description: "Allowed when status is DRAFT or CHANGES_REQUESTED.",
  })
  @ApiBody({ type: UpdateBookingDto })
  update(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: UpdateBookingDto,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    return this.bookingsService.update(id, dto, files ?? []);
  }

  @Get()
  @ApiOperation({ summary: "List freight bookings (paginated)" })
  async findAll(
    @Query() filter: FilterBookingDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Staff (backoffice) see every booking. Customers (portal) are always
    // force-scoped to their own company, regardless of any companyId they pass.
    if (hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      return this.bookingsService.findAll(filter);
    }
    // Global Logistics has clearance:view but NOT bookings:view — it is scoped
    // to the customs document-clearance queue only and never sees the general
    // booking-request list.
    if (hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)) {
      return this.bookingsService.findClearanceQueue(filter);
    }
    const userId = user?.id;
    if (!userId) throw new UnauthorizedException("Authentication required");
    const companyId =
      await this.bookingsService.resolveCustomerCompanyId(userId);
    // No linked company yet → no bookings to show (avoids leaking all bookings).
    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,
        },
      };
    }
    // Company-wide by default; the optional filter.companyProfileId (per-page
    // service filter) narrows within the company. The company guard always
    // applies, so a customer can only ever see their own company's bookings.
    return this.bookingsService.findAll(filter, companyId);
  }

  // Powers the customer-detail bookings tab, so `customers:view` reaches it too
  // — otherwise a staffer granted only the customer permission gets a page whose
  // tabs 403 individually.
  @Get("by-company/:companyId/customer-view")
  @BookingStaff([FREIGHT_PERMS.customers.view, FREIGHT_PERMS.bookings.view])
  @ApiOperation({
    summary: "List bookings for a company (customer-view shape, backoffice)",
  })
  findByCompanyCustomerView(
    @Param("companyId", ParseUUIDPipe) companyId: string,
  ) {
    return this.bookingsService.findCustomerBookings(companyId);
  }

  @Get("list-summary")
  @BookingView()
  @ApiOperation({ summary: "Booking list metrics and tab counts (backoffice)" })
  @ApiOkResponse({ type: BookingListSummaryDto })
  findListSummary(@Query() filter: FilterBookingDto) {
    return this.bookingsService.getListSummary(filter);
  }

  @Get("my")
  @ApiOperation({
    summary: "List the current customer's bookings ready for payment",
    description:
      "Bookings owned by the authenticated user's company that are payable " +
      "(FULLY_EXECUTED, SELECTED_FOR_BATCH, AWAITING_PAYMENT) and not yet PAID.",
  })
  findMyPayable(
    @CurrentUser() user: AuthUserPayload,
    @Query() filter: FilterBookingDto,
  ) {
    return this.bookingsService.findMyPayable(resolveAuthUserId(user), filter);
  }

  @Get("queues/:queue")
  @BookingView()
  @ApiOperation({
    summary: "List bookings for a dashboard queue",
    description: "Queues: intake, approval, signatures, marketing, finance",
  })
  findQueue(
    @Param("queue") queue: string,
    @Query() filter: FilterBookingDto,
    @Query("excludeBulk") excludeBulk?: string,
  ) {
    return this.bookingsService.findQueue(queue, filter, {
      excludeBulk: excludeBulk === "true",
    });
  }

  @Get("reference-data")
  @ApiOperation({ summary: "Booking form catalog" })
  @ApiOkResponse({ type: BookingReferenceDataDto })
  getReferenceData(): Promise<BookingReferenceDataDto> {
    return this.bookingReferenceDataService.getReferenceData();
  }

  @Get("by-reference/:reference")
  @ApiOperation({ summary: "Get booking by reference" })
  async findByReference(
    @Param("reference") reference: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findByReference(reference);
    // Staff see any booking; customers only their own company's.
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(
        user?.id,
        booking,
      );
    }
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Get(":id")
  @ApiOperation({ summary: "Get booking by ID" })
  async findOne(
    @Param("id", ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    // Staff see any booking; Global Logistics (clearance:view) may inspect any
    // booking for the clearance gate; customers only their own company's.
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)
    ) {
      await this.bookingsService.assertCustomerCanAccessBooking(
        user?.id,
        booking,
      );
    }
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Get(':id/available-days')
  @ApiOperation({
    summary:
      'Days bookable for THIS booking (cargo-aware wagon-TYPE gate; days only, no capacity counts)',
  })
  async availableDays(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)
    ) {
      await this.bookingsService.assertCustomerCanAccessBooking(
        user?.id,
        booking,
      );
    }
    return this.bookingsService.availableDaysForBooking(id);
  }

  @Get(':id/day-availability')
  @ApiOperation({
    summary:
      'Advisory free-wagon count for a shipment day (planning hint, not enforced). ' +
      'Export: whole-booking fit + largest single-train leftover. ' +
      'Import/domestic: total room across the day for the booking\'s wagon type.',
  })
  async dayAvailability(
    @Param('id', ParseUUIDPipe) id: string,
    @Query('date') date: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)
    ) {
      await this.bookingsService.assertCustomerCanAccessBooking(
        user?.id,
        booking,
      );
    }
    return this.transitionService.dayAvailabilityForBooking(id, date);
  }

  @Get(':id/mile-summary')
  @ApiOperation({
    summary: 'First/last-mile operational summary for a booking (customer-safe)',
  })
  async mileSummary(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Customers may only see their own booking's mile summary.
    const booking = await this.bookingsService.findById(id);
    if (
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.view) &&
      !hasFreightPermission(user, FREIGHT_PERMS.bookings.clearanceView)
    ) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }

    const [first, last] = await Promise.all([
      this.firstMileService.findAll({ bookingId: id, pageSize: 1 }),
      this.lastMileService.findAll({ bookingId: id, pageSize: 1 }),
    ]);
    return {
      firstMile: summarizeMileLeg(first.data[0]),
      lastMile: summarizeMileLeg(last.data[0]),
    };
  }

  @Post(':id/customer-truck-assignment')
  @ApiOperation({ summary: 'Customer assigns external truck and driver for terminal pickup' })
  async assignCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: CustomerTruckAssignmentDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    const assigned = await this.bookingsService.assignCustomerTruck(id, dto);
    return this.transitionService.enrichBookingResponse(assigned);
  }

  @Get(':id/customer-truck-assignment/freight-order')
  @ApiOperation({
    summary:
      'Download freight order copies. The 2 gate copies always print; ?copies=1,2,8 adds waybill-style copies (catalog indexes 1-8).',
  })
  async customerTruckFreightOrder(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
    @Res() res: Response,
    @Query('copies') copies?: string,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    const extraCopyIndexes = (copies ?? '')
      .split(',')
      .map((n) => Number(n.trim()))
      .filter((n) => Number.isInteger(n) && n >= 1 && n <= 8);
    const { filename, buffer } =
      await this.bookingsService.customerTruckFreightOrderCopies(id, extraCopyIndexes);
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.send(buffer);
  }

  @Get(':id/customer-trucks')
  @ApiOperation({ summary: 'List customer self-haul trucks (multi-truck) for a booking' })
  async listCustomerTrucks(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.listTrucks(id);
  }

  @Post(':id/customer-trucks')
  @ApiOperation({ summary: 'Add a customer self-haul truck carrying 1–2 of the booking containers' })
  async addCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: AddCustomerTruckDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.addTruck(id, dto);
  }

  @Post(':id/customer-trucks/bulk')
  @ApiOperation({ summary: 'Bulk add customer trucks from array payload (Excel parsed)' })
  async bulkAddCustomerTrucks(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() payload: { trucks: AddCustomerTruckDto[] },
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.addBulkTrucks(id, payload.trucks);
  }

  @Patch(':id/customer-trucks/:assignmentId')
  @ApiOperation({ summary: 'Edit a not-yet-arrived customer truck (plate/driver/type + containers)' })
  async updateCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('assignmentId', ParseUUIDPipe) assignmentId: string,
    @Body() dto: AddCustomerTruckDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.updateTruck(id, assignmentId, dto);
  }

  @Delete(':id/customer-trucks/:assignmentId')
  @ApiOperation({ summary: 'Remove a not-yet-arrived customer truck from a booking' })
  async removeCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('assignmentId', ParseUUIDPipe) assignmentId: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.removeTruck(id, assignmentId);
  }

  @Get(':id/customer-trucks/loadable-containers')
  @ApiOperation({ summary: 'Booking containers not yet loaded onto a truck' })
  async loadableContainers(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
    }
    return this.customerTruckService.getLoadableContainers(id);
  }

  @Post(':id/customer-trucks/:assignmentId/load')
  @ApiOperation({ summary: 'Truck_dispatch: load selected containers onto a truck (staff)' })
  async loadCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('assignmentId', ParseUUIDPipe) assignmentId: string,
    @Body() dto: LoadCustomerTruckDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      throw new ForbiddenException('Only warehouse staff can load a truck');
    }
    return this.customerTruckService.loadTruck(id, assignmentId, dto);
  }

  @Post(':id/customer-trucks/:assignmentId/depart')
  @ApiOperation({
    summary: 'Register an import truck leaving: containers loaded + weighed gross (staff)',
  })
  async departCustomerTruck(
    @Param('id', ParseUUIDPipe) id: string,
    @Param('assignmentId', ParseUUIDPipe) assignmentId: string,
    @Body() dto: DepartCustomerTruckDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    // Weighing + registering the load on exit is a warehouse/gate staff action.
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      throw new ForbiddenException('Only warehouse staff can register a truck departure');
    }
    return this.customerTruckService.departTruck(id, assignmentId, dto);
  }

  @Get(':id/received-pending-grn')
  @ApiOperation({ summary: 'Containers received into port but not yet on a GRN' })
  async receivedPendingGrn(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    // GRN is a warehouse-staff action — no customer access.
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      throw new ForbiddenException('Only warehouse staff can view or generate GRNs');
    }
    return this.containerReceiptService.listReceivedPendingGrn(id);
  }

  @Post(':id/generate-grn')
  @ApiOperation({
    summary:
      'Generate a GRN over the received containers (all received, or a subset) — one GRN per batch',
  })
  async generateGrn(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: GenerateGrnDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    // GRN is a warehouse-staff action — no customer access.
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      throw new ForbiddenException('Only warehouse staff can view or generate GRNs');
    }
    return this.containerReceiptService.generateGrn(id, dto.containerNumbers);
  }

  @Get(':id/tracking')
  @ApiOperation({
    summary: "Shipment tracking timeline for a booking",
    description:
      "Returns the booking's consignment (once dispatched) and its ordered " +
      "tracking events. Scoped to the customer's own company.",
  })
  async findTracking(
    @Param("id", ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingsService.findById(id);
    // Staff see any booking; customers only their own company's.
    if (!hasFreightPermission(user, FREIGHT_PERMS.bookings.view)) {
      await this.bookingsService.assertCustomerCanAccessBooking(
        user?.id,
        booking,
      );
    }
    return this.bookingsService.getBookingTracking(id);
  }

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

  @Post(":id/documents")
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes("multipart/form-data")
  @ApiOperation({ summary: "Upload documents for a booking (DRAFT only)" })
  async uploadDocuments(
    @Param("id", ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    const booking = await this.bookingsService.uploadDocuments(id, files ?? []);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/generate-price")
  @ApiOperation({
    summary: "Generate price preview (DRAFT or CHANGES_REQUESTED)",
    description:
      "Computes and stores a price preview on the booking. Does not create rate snapshots.",
  })
  @ApiOkResponse({ type: GeneratePriceResponseDto })
  generatePrice(@Param("id", ParseUUIDPipe) id: string) {
    return this.pricingService.generatePrice(id);
  }

  @Post(":id/submit")
  @ApiOperation({
    summary: "Customer submit booking",
    description:
      "Recomputes price against live rates. If unchanged, creates rate snapshots and submits. If changed, updates the booking price and returns priceChanged=true for confirmation.",
  })
  @ApiOkResponse({ type: SubmitBookingResponseDto })
  submit(@Param("id", ParseUUIDPipe) id: string) {
    return this.transitionService.submit(id);
  }

  @Post(":id/confirm-submit")
  @ApiOperation({
    summary: "Confirm submit after price change",
    description:
      "Creates rate snapshots for the updated booking price and moves the booking to SUBMITTED.",
  })
  @ApiOkResponse({ type: SubmitBookingResponseDto })
  confirmSubmit(@Param("id", ParseUUIDPipe) id: string) {
    return this.transitionService.confirmSubmit(id);
  }

  @Post(":id/reject")
  @ApiOperation({
    summary: "Customer reject price estimate",
    description:
      "Customer rejects the priced booking at the confirm step. The booking becomes REJECTED (terminal); the customer must create a new booking.",
  })
  async reject(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: RejectBookingDto,
  ) {
    const booking = await this.transitionService.reject(id, dto.reason);
    return this.transitionService.enrichBookingResponse(booking);
  }

  // ── Document clearance (post counter-sign) ────────────────────────────────

  @Get('clearance/et-queue')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({ summary: 'GL ET queue — general customs bookings awaiting ET action' })
  getBookingEtClearanceQueue() {
    return this.bookingClearanceService.etQueue();
  }

  @Get('clearance/dj-queue')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @ApiOperation({ summary: 'GL DJ queue — general customs bookings awaiting DJ action' })
  getBookingDjClearanceQueue() {
    return this.bookingClearanceService.djQueue();
  }

  @Get(':id/clearance')
  @ApiOperation({
    summary:
      "Document-clearance grid (required docs + upload + GL review status)",
  })
  getClearance(@Param("id", ParseUUIDPipe) id: string) {
    return this.transitionService.getClearanceView(id);
  }

  @Post(":id/clearance/documents")
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes("multipart/form-data")
  @ApiOperation({
    summary: "Customer uploads clearance documents (fieldname = document key)",
  })
  async submitClearanceDocuments(
    @Param("id", ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    const booking = await this.transitionService.submitClearanceDocuments(
      id,
      files ?? [],
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/clearance/proceed")
  @ApiOperation({
    summary:
      "Customer requests operation with a schedule day " +
      "(CLEARANCE_READY | OPERATION_CHANGES_REQUESTED → OPERATION_REQUEST_PENDING)",
  })
  async proceedToOperation(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: RequestOperationDto,
  ) {
    const booking = await this.transitionService.requestOperation(
      id,
      dto.scheduledDate,
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/operation/review")
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({
    summary:
      "Operations reviews an operation request: ACCEPT (→ batch pool), " +
      "REQUEST_CHANGES (→ back to customer), or ADJUST_PRICE (→ customer re-confirm)",
  })
  async reviewOperationRequest(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: OperationReviewDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.transitionService.reviewOperationRequest(
      id,
      dto.decision,
      resolveAuthUserId(user),
      { note: dto.note },
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/clearance/review")
  @BookingStaff(FREIGHT_PERMS.bookings.reviewDocuments)
  @ApiOperation({
    summary: "GL reviews a clearance document (Approve | Query)",
  })
  async reviewClearanceDocument(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: ReviewDocumentDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.transitionService.reviewDocument(
      id,
      dto.fileKey,
      dto.status,
      resolveAuthUserId(user),
      dto.note,
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/clearance/output-documents")
  @BookingStaff(FREIGHT_PERMS.bookings.uploadClearanceOutput)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes("multipart/form-data")
  @ApiOperation({ summary: "GL uploads customs output documents (IM4/EX3/…)" })
  async uploadClearanceOutput(
    @Param("id", ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
  ) {
    const booking = await this.transitionService.uploadClearanceOutputDocuments(
      id,
      files ?? [],
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/clearance/finalize")
  @BookingStaff(FREIGHT_PERMS.bookings.finalizeClearance)
  @ApiOperation({
    summary:
      "GL finalizes clearance (requires 100% approved) → CLEARANCE_READY",
  })
  async finalizeClearance(@Param("id", ParseUUIDPipe) id: string) {
    const booking = await this.transitionService.finalizeClearance(id);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/declaration')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET uploads customs declaration on booking (GENERAL customs)' })
  async uploadBookingDeclaration(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingClearanceService.uploadDeclaration(
      id,
      files ?? [],
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/duty')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDutyAdvise)
  @UseInterceptors(FileInterceptor('attachment'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'GL ET sets duty/tax on booking with notice attachment' })
  async adviseBookingDuty(
    @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: TCurrentUser,
  ) {
    const dutyRequired = dutyRequiredRaw === 'true' || dutyRequiredRaw === '1';
    const dto: AdviseContractDutyDto = {
      dutyRequired,
      amount:
        amountRaw != null && amountRaw !== '' ? Number(amountRaw) : undefined,
      currency: currency ?? 'ETB',
      declarationSerial,
    };
    const booking = await this.bookingClearanceService.adviseDuty(
      id,
      dto,
      resolveAuthUserId(user),
      attachment,
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/finalize-pre-clearance')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @ApiOperation({ summary: 'GL ET finalizes import pre-clearance on booking' })
  async finalizeBookingPreClearance(@Param('id', ParseUUIDPipe) id: string) {
    const booking = await this.bookingClearanceService.finalizePreClearance(id);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/duty-slip')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Customer uploads duty/tax payment slip on booking' })
  async uploadBookingDutySlip(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    const booking = await this.bookingClearanceService.uploadDutySlip(id, file);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/transit-permit')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  @UseInterceptors(AnyFilesInterceptor())
  @ApiConsumes('multipart/form-data')
  async uploadBookingTransitPermit(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFiles() files: Express.Multer.File[],
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingClearanceService.uploadTransitPermit(
      id,
      files ?? [],
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/delivery-order')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  async uploadBookingDeliveryOrder(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
    @Body('vesselArrivalDate') vesselArrivalDate: string | undefined,
    @Body('doCollectedDate') doCollectedDate: string | undefined,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingClearanceService.uploadDeliveryOrder(
      id,
      file,
      resolveAuthUserId(user),
      { vesselArrivalDate, doCollectedDate },
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/release-order')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  async uploadBookingReleaseOrder(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
    @Body('vesselDepartureDate') vesselDepartureDate: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const result = await this.bookingClearanceService.uploadReleaseOrder(
      id,
      file,
      vesselDepartureDate,
      resolveAuthUserId(user),
    );
    return {
      ...this.transitionService.enrichBookingResponse(result.booking),
      hold: result.hold,
      holdReason: result.holdReason,
    };
  }

  @Post(':id/clearance/ro-amendment')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
  async requestBookingRoAmendment(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: RoAmendmentDto,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingClearanceService.requestRoAmendment(
      id,
      dto.note,
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/clearance/export-release')
  @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
  async confirmBookingExportRelease(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: TCurrentUser,
  ) {
    const booking = await this.bookingClearanceService.confirmExportRelease(
      id,
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(':id/staff/request-changes')
  @BookingStaff(FREIGHT_PERMS.bookings.requestChanges)
  @ApiOperation({ summary: "Staff return booking for customer updates" })
  async requestChanges(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: RequestChangesDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.transitionService.requestChanges(
      id,
      dto.note,
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/staff/accept")
  @BookingStaff(FREIGHT_PERMS.bookings.staffAccept)
  @ApiOperation({
    summary:
      "Staff accept intake → set contract validity window + start approval chain",
  })
  async acceptIntake(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: AcceptIntakeDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.transitionService.acceptIntake(
      id,
      resolveAuthUserId(user),
      dto.validityDays,
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/staff/reject")
  @BookingStaff(FREIGHT_PERMS.bookings.reject)
  @ApiOperation({ summary: "Staff final reject" })
  async staffReject(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: StaffRejectDto,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.transitionService.staffReject(
      id,
      dto.reason,
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/government-expedite")
  @BookingStaff(FREIGHT_PERMS.bookings.staffAccept)
  @ApiOperation({
    summary: "Expedite government booking to PAID / ELIGIBLE for scheduling",
  })
  async governmentExpedite(
    @Param("id", ParseUUIDPipe) id: string,
    @CurrentUser() user: AuthUserPayload,
  ) {
    const booking = await this.bookingsService.governmentExpedite(
      id,
      resolveAuthUserId(user),
    );
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/contract/generate")
  @BookingStaff(FREIGHT_PERMS.bookings.generateContract)
  @ApiOperation({ summary: "Generate contract PDF from template" })
  async generateContract(@Param("id", ParseUUIDPipe) id: string) {
    const booking = await this.contractService.generateContract(id);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Get(":id/contract/view")
  @ApiOkResponse({ type: ContractViewDto })
  @ApiOperation({ summary: "Contract HTML view for portal and backoffice" })
  getContractView(
    @Param("id", ParseUUIDPipe) id: string,
    @Request() req: { user?: { id?: string; sub?: string } },
  ) {
    const userId = req.user?.id ?? req.user?.sub;
    return this.contractService.getContractView(id, userId);
  }

  @Get(":id/contract/document")
  @ApiOperation({ summary: "Download contract PDF" })
  async downloadContractDocument(
    @Param("id", ParseUUIDPipe) id: string,
    @Res() res: Response,
  ): Promise<void> {
    const { stream, record } = await this.contractService.streamContract(id);
    res.setHeader("Content-Type", record.mimeType ?? "application/pdf");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename="${record.name}"`,
    );
    stream.pipe(res);
  }

  @Get(":id/contract")
  @ApiOperation({ summary: "Download contract file (alias)" })
  async downloadContract(
    @Param("id", ParseUUIDPipe) id: string,
    @Res() res: Response,
  ): Promise<void> {
    return this.downloadContractDocument(id, res);
  }

  @Post(":id/contract/sign")
  @UseGuards(JwtGuard)
  @ApiOperation({ summary: "Apply digital signature (customer or staff)" })
  async signContract(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: SignContractDto,
    @CurrentUser() user: TCurrentUser,
    @Request() req: { user?: { id?: string; sub?: string }; ip?: string },
  ) {
    // Staff signature needs the sign permission; customer signs their own booking.
    if (dto.role !== "CUSTOMER") {
      assertFreightPermission(user, FREIGHT_PERMS.bookings.signStaff);
    }
    const userId = req.user?.id ?? req.user?.sub;
    const booking = await this.contractService.signContract(id, dto, {
      signerUserId: userId,
      ipAddress: req.ip,
    });
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Get(":id/contract/signatures")
  @ApiOperation({ summary: "List contract signatures" })
  getContractSignatures(@Param("id", ParseUUIDPipe) id: string) {
    return this.contractService.getSignatures(id);
  }

  @Get(":id/summary")
  @ApiOperation({ summary: "Contract summary string for dashboard" })
  getSummary(@Param("id", ParseUUIDPipe) id: string) {
    return this.contractService.getSummary(id);
  }

  @Post(":id/customer/sign")
  @ApiOperation({
    summary: "Customer digital signature (deprecated — use POST contract/sign)",
  })
  async customerSign(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: SignContractDto,
    @Request() req: { user?: { id?: string; sub?: string }; ip?: string },
  ) {
    const payload: SignContractDto = { ...dto, role: "CUSTOMER" };
    const booking = await this.contractService.signContract(id, payload, {
      signerUserId: req.user?.id ?? req.user?.sub,
      ipAddress: req.ip,
    });
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/marketing/approve")
  @BookingStaff(FREIGHT_PERMS.bookings.signStaff)
  @ApiOperation({
    summary:
      "Staff contract signature and fully execute (use contract/sign STAFF preferred)",
  })
  async marketingApprove(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: SignContractDto,
    @CurrentUser() user: AuthUserPayload,
    @Request() req: { ip?: string },
  ) {
    const payload: SignContractDto = {
      ...dto,
      role: "STAFF",
    };
    const booking = await this.contractService.signContract(id, payload, {
      signerUserId: resolveAuthUserId(user),
      ipAddress: req.ip,
    });
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/operations/start-transit")
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: "Mark in transit" })
  async startTransit(@Param("id", ParseUUIDPipe) id: string) {
    const booking = await this.transitionService.startTransit(id);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/operations/complete")
  @BookingStaff(FREIGHT_PERMS.bookings.operations)
  @ApiOperation({ summary: "Mark completed" })
  async complete(@Param("id", ParseUUIDPipe) id: string) {
    const booking = await this.transitionService.complete(id);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/cancel")
  @BookingStaff(FREIGHT_PERMS.bookings.cancel)
  @ApiOperation({ summary: "Cancel booking" })
  async cancel(
    @Param("id", ParseUUIDPipe) id: string,
    @Body() dto: CancelBookingDto,
  ) {
    const booking = await this.transitionService.cancel(id, dto.reason);
    return this.transitionService.enrichBookingResponse(booking);
  }

  @Post(":id/consolidation")
  @ApiOperation({ summary: "Request freight consolidation" })
  requestConsolidation(@Param("id", ParseUUIDPipe) id: string) {
    return this.bookingsService.requestConsolidation(id);
  }

  @Delete(":id/consolidation")
  @ApiOperation({ summary: "Remove consolidation pairing" })
  removeConsolidation(@Param("id", ParseUUIDPipe) id: string) {
    return this.bookingsService.removeConsolidation(id);
  }

  @Get(":id/consolidation")
  @ApiOperation({ summary: "Get consolidation details" })
  getConsolidationDetails(@Param("id", ParseUUIDPipe) id: string) {
    return this.bookingsService.getConsolidationDetails(id);
  }
}
