import {
  Controller,
  Get,
  Post,
  Patch,
  Delete,
  Body,
  Param,
  Query,
  UseGuards,
  UseInterceptors,
  UploadedFile,
  ParseUUIDPipe,
  Header,
  BadRequestException,
  Req,
  StreamableFile,
  Res,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Throttle } from '@nestjs/throttler';
import { memoryStorage } from 'multer';
import { extname } from 'node:path';
import { BookingService } from './booking.service';
import { BookingPanService, PAN_CARD_MAX_BYTES } from './services/booking-pan.service';
import { DriverAssignmentService } from './services/driver-assignment.service';
import { MovementChartExportService } from './services/movement-chart-export.service';
import { isAllowedPanExtension } from '../../common/storage/file-sniff';
import {
  UpdateBookingDto,
  AssignTransportItemDto,
  BulkAssignTransportItemDto,
  CreateQuickBookingDto,
  PatchActivityItemDto,
  BulkPatchActivityItemDto,
  PatchHotelItemDto,
  PatchSpecialItemDto,
  UpdateActualSupplierCostDto,
  CreateAmendedTransportDto,
  BookingListQueryDto,
  SetBookingGuaranteeDto,
} from './dto';
import { BookingItineraryService } from './services/booking-itinerary.service';
import { Lookup } from '../../common/decorators/lookup.decorator';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
import { PermissionGuard } from '../../auth/guards/permission.guard';
import { Permissions } from '../../auth/decorators/permissions.decorator';
import { AccessLevel, ModuleName } from '../../entities/role-permission.entity';
import { paginatedResponse, successResponse } from '../../common/responses/api-response';
import { Request, Response } from 'express';
import { ActivityTicketMailService } from './services/activity-ticket-mail.service';
import { BookingStatus } from '../../entities/booking.entity';

/** Longest Movement Chart export range (days) — mirrors the FE cap. */
const MD_EXPORT_MAX_DAYS = 92;

@Controller('bookings')
@UseGuards(JwtAuthGuard, PermissionGuard)
export class BookingController {
  constructor(
    private readonly service: BookingService,
    private readonly pan: BookingPanService,
    // Direct injection for movement-chart + assignment endpoints.
    // (Fleet Planner injection removed 2026-05-13 — service deleted.)
    private readonly driverAssignment: DriverAssignmentService,
    private readonly itinerary: BookingItineraryService,
    private readonly mdExport: MovementChartExportService,
    private readonly ticketMail: ActivityTicketMailService,
  ) {}

  @Get()
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.VIEW })
  async findAll(@Query() query: BookingListQueryDto) {
    const { items, meta } = await this.service.findAll(
      { page: query.page, limit: query.limit, search: query.search },
      {
        status: query.status,
        destinationId: query.destination_id,
        sourceId: query.source_id,
        dateFrom: query.date_from,
        dateTo: query.date_to,
      },
    );
    return paginatedResponse(items, meta, 'Bookings fetched successfully');
  }

  @Get('transport-dates')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.VIEW })
  async getTransportDates(@Query('destination_id') destinationId: string) {
    const data = await this.driverAssignment.getTransportDates(destinationId);
    return successResponse(data, 'Transport dates fetched');
  }

  // Fleet Planner endpoints (GET /fleet-planner, /fleet-planner/suggest,
  // POST /fleet-planner/reassign) and the standalone GET /driver-
  // assignments endpoint were removed 2026-05-13 — the Movement Chart
  // is now the single operational view for transport assignments.
  // See chore/delete-driver-assignment-fleet-planner.

  @Get('movement-chart')
  @Permissions({ module: ModuleName.MOVEMENT_CHART, accessLevel: AccessLevel.VIEW })
  async getMovementChart(
    @Query('destination_id') destinationId: string | undefined,
    @Query('date') date: string,
    // MD Payment Collection filter — one of REQUIRED / PENDING /
    // COLLECTED. Empty/missing → no filter. Rejects unknown values so
    // a mistyped query string surfaces immediately instead of silently
    // returning everything.
    @Query('payment_collection_status') paymentCollectionStatus?: string,
  ) {
    // destination_id is optional now — empty / missing means "all
    // destinations" (the new top-of-page filter). Tenant scope still
    // applies inside the service.
    const pcFilter =
      paymentCollectionStatus &&
      ['REQUIRED', 'PENDING', 'COLLECTED'].includes(paymentCollectionStatus)
        ? (paymentCollectionStatus as 'REQUIRED' | 'PENDING' | 'COLLECTED')
        : undefined;
    const data = await this.driverAssignment.getMovementChart(
      destinationId || undefined,
      date,
      { payment_collection_status: pcFilter },
    );
    return successResponse(data, 'Movement chart fetched successfully');
  }

  @Get('movement-chart/export')
  @Permissions({ module: ModuleName.MOVEMENT_CHART, accessLevel: AccessLevel.VIEW })
  async exportMovementChart(
    @Res() res: Response,
    @Query('destination_id') destinationId: string | undefined,
    @Query('date') date: string,
    @Query('date_to') dateTo?: string,
  ) {
    // Date-range export only — validate the range before building a
    // potentially large workbook (dates also flow into the filename).
    const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
    if (!ISO_DATE.test(date || '') || (dateTo && !ISO_DATE.test(dateTo))) {
      throw new BadRequestException('date / date_to must be YYYY-MM-DD');
    }
    if (destinationId && !/^[0-9a-f-]{36}$/i.test(destinationId)) {
      throw new BadRequestException('Invalid destination_id');
    }
    const days =
      Math.round(
        (Date.parse(`${dateTo || date}T00:00:00Z`) - Date.parse(`${date}T00:00:00Z`)) / 86_400_000,
      ) + 1;
    if (!Number.isFinite(days) || days < 1) {
      throw new BadRequestException('date_to must be on or after date');
    }
    if (days > MD_EXPORT_MAX_DAYS) {
      throw new BadRequestException(`Export range can be at most ${MD_EXPORT_MAX_DAYS} days`);
    }
    const { buffer, filename } = await this.mdExport.buildWorkbook(
      destinationId || undefined,
      date,
      dateTo || undefined,
    );
    res.setHeader(
      'Content-Type',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    );
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.send(buffer);
  }

  @Patch('transport-items/bulk-assign')
  @Permissions({ module: ModuleName.TRANSPORT_ASSIGNMENT, accessLevel: AccessLevel.EDIT })
  async bulkAssign(@Body() dto: BulkAssignTransportItemDto) {
    const result = await this.driverAssignment.bulkAssignTransportItems(dto);
    return successResponse(result, result.message);
  }

  /**
   * §12 — Movement Chart inline supplier-cost edit (internal margin).
   * Does NOT create a settlement; does NOT affect the customer total.
   * Gated by TRANSPORT_ASSIGNMENT:edit (Ops Manager + Admin).
   */
  @Patch('transport-items/:id/actual-supplier-cost')
  @Permissions({ module: ModuleName.TRANSPORT_ASSIGNMENT, accessLevel: AccessLevel.EDIT })
  async updateActualSupplierCost(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: UpdateActualSupplierCostDto,
  ) {
    const result = await this.itinerary.updateActualSupplierCost(
      id,
      dto.actual_supplier_cost ?? null,
    );
    return successResponse(result, result.message);
  }

  /**
   * §12 — Movement Chart "+ Add amended transport". Creates a row with
   * `is_amended = true` and raises one COLLECTION settlement.
   * Permission key chosen for symmetry with the supplier-cost edit:
   * Ops Manager + Admin can amend the booking on the road.
   */
  @Post('transport-items/amended')
  @Permissions({ module: ModuleName.PAYMENT_SETTLEMENT, accessLevel: AccessLevel.EDIT })
  async addAmendedTransport(@Body() dto: CreateAmendedTransportDto) {
    const result = await this.itinerary.createAmendedTransport(dto);
    return successResponse(result, result.message);
  }

  @Patch('transport-items/:id/assign')
  @Permissions({ module: ModuleName.TRANSPORT_ASSIGNMENT, accessLevel: AccessLevel.EDIT })
  async assignTransportItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: AssignTransportItemDto,
  ) {
    const result = await this.driverAssignment.assignTransportItem(id, dto);
    return successResponse(result, result.message);
  }

  /**
   * Email the activity's ticket link — Phase 1 recipient is the
   * logged-in user. Triggered automatically after a ticket is assigned
   * / uploaded on the Movement Dashboard, and by the row's ✉ button.
   */
  @Post('activity-items/:id/ticket-email')
  @Permissions({ module: ModuleName.ACTIVITY_ASSIGNMENT, accessLevel: AccessLevel.VIEW })
  @Throttle({ default: { limit: 60, ttl: 60 * 60 * 1000 } })
  async emailActivityTicket(@Param('id', ParseUUIDPipe) id: string, @Req() req: Request) {
    const apiOrigin =
      process.env.API_PUBLIC_URL?.trim() || `${req.protocol}://${req.get('host') ?? ''}`;
    const result = await this.ticketMail.emailTicket(id, apiOrigin);
    return successResponse(result, `Ticket emailed to ${result.sent_to}`);
  }

  /**
   * Activity-item per-row autosave from the Movement Chart Planner
   * tab. Siblings across age groups are kept in sync by the service.
   */
  @Patch('activity-items/bulk-patch')
  @Permissions({ module: ModuleName.ACTIVITY_ASSIGNMENT, accessLevel: AccessLevel.EDIT })
  async bulkPatchActivity(@Body() dto: BulkPatchActivityItemDto) {
    const result = await this.driverAssignment.bulkPatchActivityItems(dto);
    return successResponse(result, result.message);
  }

  @Patch('activity-items/:id')
  @Permissions({ module: ModuleName.ACTIVITY_ASSIGNMENT, accessLevel: AccessLevel.EDIT })
  async patchActivityItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: PatchActivityItemDto,
  ) {
    const result = await this.driverAssignment.patchActivityItem(id, dto);
    return successResponse(result, result.message);
  }

  /**
   * Hotel-item patch — supplier assignment + cost edit. Uses the
   * BOOKING_EDIT module since hotels don't have a dedicated assignment
   * permission today; same gate Ops Manager + Admin have for itinerary
   * edits.
   */
  @Patch('hotel-items/:id')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async patchHotelItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: PatchHotelItemDto,
  ) {
    const result = await this.driverAssignment.patchHotelItem(id, dto);
    return successResponse(result, result.message);
  }

  /** Special-item patch — same as hotel: supplier + cost. */
  @Patch('special-items/:id')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async patchSpecialItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() dto: PatchSpecialItemDto,
  ) {
    const result = await this.driverAssignment.patchSpecialItem(id, dto);
    return successResponse(result, result.message);
  }

  /**
   * Booking lookup — returns minimal shape for ticket-batch booking selector.
   * Filters to upcoming + ongoing (converted) bookings only.
   * MUST come before @Get(':id').
   */
  @Get('lookup')
  @Lookup()
  async lookup() {
    const data = await this.service.findAllForLookup();
    return successResponse(data, 'Bookings lookup');
  }

  /**
   * Guest lookup by mobile — used by the Quick Booking form to prefill
   * customer info for returning walk-ins. Any authed tenant user.
   * MUST come before @Get(':id') so Nest matches this path first.
   */
  @Get('guest-lookup')
  @Lookup()
  async guestLookup(@Query('mobile') mobile: string) {
    const data = await this.service.lookupGuestByMobile(mobile);
    return successResponse(data, data ? 'Guest found' : 'No match');
  }

  @Get(':id')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.VIEW })
  async findById(@Param('id', ParseUUIDPipe) id: string) {
    const data = await this.service.findById(id);
    return successResponse(data, 'Booking fetched successfully');
  }

  @Post(':id/pan-card')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  @Throttle({ default: { limit: 30, ttl: 60 * 60 * 1000 } })
  @UseInterceptors(
    FileInterceptor('file', {
      limits: { fileSize: PAN_CARD_MAX_BYTES },
      fileFilter: (_req, file, cb) => {
        const ext = extname(file.originalname).toLowerCase();
        if (!isAllowedPanExtension(ext)) {
          cb(new BadRequestException('PAN card must be an image or PDF'), false);
          return;
        }
        cb(null, true);
      },
      storage: memoryStorage(),
    }),
  )
  async uploadPanCard(
    @Param('id', ParseUUIDPipe) id: string,
    @UploadedFile() file: Express.Multer.File,
  ) {
    const data = await this.pan.upload(id, file);
    return successResponse(data, 'PAN card uploaded');
  }

  @Get(':id/pan-card')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.VIEW })
  @Header('X-Content-Type-Options', 'nosniff')
  @Header('Cache-Control', 'private, no-store')
  async downloadPanCard(@Param('id', ParseUUIDPipe) id: string): Promise<StreamableFile> {
    return this.pan.download(id);
  }

  @Post('from-query/:queryId')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.CREATE })
  async createFromQuery(@Param('queryId', ParseUUIDPipe) queryId: string) {
    const result = await this.service.createFromQuery(queryId);
    return successResponse(result, result.message);
  }

  /**
   * Quick Booking — walk-in / ad-hoc flow. Creates a query + quote + booking
   * atomically. See `BookingService.createQuick` for details.
   */
  @Post('quick')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.CREATE })
  async createQuick(@Body() dto: CreateQuickBookingDto) {
    const result = await this.service.createQuick(dto);
    return successResponse(result, result.message);
  }

  @Patch(':id')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateBookingDto) {
    const result = await this.service.update(id, dto);
    return successResponse(result, result.message);
  }

  @Patch(':id/status')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async changeStatus(
    @Param('id', ParseUUIDPipe) id: string,
    @Body('status') status: BookingStatus,
    @Body('hold_reason') holdReason?: string,
  ) {
    const result = await this.service.changeStatus(id, status, holdReason);
    return successResponse(result, result.message);
  }

  /** Mark / unmark the booking as guaranteed — bypasses the MD day-wise payment gate. */
  @Patch(':id/guarantee')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async setGuarantee(@Param('id', ParseUUIDPipe) id: string, @Body() dto: SetBookingGuaranteeDto) {
    const result = await this.service.setGuarantee(
      id,
      dto.is_guaranteed,
      dto.guarantee_description,
    );
    return successResponse(result, result.message);
  }

  @Delete(':id')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.DELETE })
  async remove(@Param('id', ParseUUIDPipe) id: string) {
    const result = await this.service.remove(id);
    return successResponse(result, result.message);
  }

  // ── Activity Cancel / Refund (ops workflow) ─────────────────────────

  @Patch('activity-items/:id/cancel')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async cancelActivityItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() body: { count: number; notes?: string },
  ) {
    const result = await this.service.cancelActivityItem(id, body.count, body.notes);
    return successResponse(result, result.message);
  }

  @Patch('activity-items/:id/refund')
  @Permissions({ module: ModuleName.BOOKING, accessLevel: AccessLevel.EDIT })
  async refundActivityItem(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() body: { notes?: string },
  ) {
    const result = await this.service.refundActivityItem(id, body.notes);
    return successResponse(result, result.message);
  }
}
