import {
  Controller,
  Get,
  Post,
  Param,
  Body,
  Query,
  HttpCode,
  HttpStatus,
  Logger,
  UsePipes,
  ValidationPipe,
  NotFoundException,
  ParseUUIDPipe,
  ParseIntPipe,
  DefaultValuePipe,
  UseGuards,
  Res,
  StreamableFile,
  Inject,
} from '@nestjs/common';
import {
  ApiTags,
  ApiOperation,
  ApiResponse,
  ApiBadRequestResponse,
  ApiNotFoundResponse,
  ApiInternalServerErrorResponse,
  ApiQuery,
  ApiParam,
  ApiBearerAuth,
  ApiProduces,
} from '@nestjs/swagger';
import { Response } from 'express';
import { CreateBookingRequestDto, BookingResponseDto, BookingListResponseDto } from '../dto';
import { BookingFilterDto } from '../dto/booking-filter.dto';
import { BookingExportDto } from '../dto/booking-export.dto';
import { BookingMapper } from '../mappers';
import { BookingService } from '@domain/services/booking.service';
import { BookingRepository, BOOKING_REPOSITORY } from '@domain/ports/out/booking.repository';
import {
  RateQuoteRepository,
  RATE_QUOTE_REPOSITORY,
} from '@domain/ports/out/rate-quote.repository';
import { BookingNumber } from '@domain/value-objects/booking-number.vo';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
import { ExportService } from '../services/export.service';
import { FuzzySearchService } from '../services/fuzzy-search.service';
import { AuditService } from '../services/audit.service';
import { AuditAction } from '@domain/entities/audit-log.entity';
import { NotificationService } from '../services/notification.service';
import { NotificationsGateway } from '../gateways/notifications.gateway';
import { WebhookService } from '../services/webhook.service';
import { WebhookEvent } from '@domain/entities/webhook.entity';
import {
  ShipmentCounterPort,
  SHIPMENT_COUNTER_PORT,
} from '@domain/ports/out/shipment-counter.port';
import { SubscriptionService } from '../services/subscription.service';
import { ShipmentLimitExceededException } from '@domain/exceptions/shipment-limit-exceeded.exception';

@ApiTags('Bookings')
@Controller('bookings')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
export class BookingsController {
  private readonly logger = new Logger(BookingsController.name);

  constructor(
    private readonly bookingService: BookingService,
    @Inject(BOOKING_REPOSITORY) private readonly bookingRepository: BookingRepository,
    @Inject(RATE_QUOTE_REPOSITORY) private readonly rateQuoteRepository: RateQuoteRepository,
    private readonly exportService: ExportService,
    private readonly fuzzySearchService: FuzzySearchService,
    private readonly auditService: AuditService,
    private readonly notificationService: NotificationService,
    private readonly notificationsGateway: NotificationsGateway,
    private readonly webhookService: WebhookService,
    @Inject(SHIPMENT_COUNTER_PORT) private readonly shipmentCounter: ShipmentCounterPort,
    private readonly subscriptionService: SubscriptionService
  ) {}

  @Post()
  @HttpCode(HttpStatus.CREATED)
  @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
  @ApiOperation({
    summary: 'Create a new booking',
    description:
      'Create a new booking based on a rate quote. The booking will be in "draft" status initially. Requires authentication.',
  })
  @ApiResponse({
    status: HttpStatus.CREATED,
    description: 'Booking created successfully',
    type: BookingResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  @ApiBadRequestResponse({
    description: 'Invalid request parameters',
  })
  @ApiNotFoundResponse({
    description: 'Rate quote not found',
  })
  @ApiInternalServerErrorResponse({
    description: 'Internal server error',
  })
  async createBooking(
    @Body() dto: CreateBookingRequestDto,
    @CurrentUser() user: UserPayload
  ): Promise<BookingResponseDto> {
    this.logger.log(`[User: ${user.email}] Creating booking for rate quote: ${dto.rateQuoteId}`);

    // Check shipment limit for Bronze plan
    const subscription = await this.subscriptionService.getOrCreateSubscription(
      user.organizationId
    );
    const maxShipments = subscription.plan.maxShipmentsPerYear;
    if (maxShipments !== -1) {
      const currentYear = new Date().getFullYear();
      const count = await this.shipmentCounter.countShipmentsForOrganizationInYear(
        user.organizationId,
        currentYear
      );
      if (count >= maxShipments) {
        throw new ShipmentLimitExceededException(user.organizationId, count, maxShipments);
      }
    }

    try {
      // Convert DTO to domain input, using authenticated user's data
      const input = {
        ...BookingMapper.toCreateBookingInput(dto),
        userId: user.id,
        organizationId: user.organizationId,
      };

      // Create booking via domain service
      const booking = await this.bookingService.createBooking(input);

      // Fetch rate quote for response
      const rateQuote = await this.rateQuoteRepository.findById(dto.rateQuoteId);
      if (!rateQuote) {
        throw new NotFoundException(`Rate quote ${dto.rateQuoteId} not found`);
      }

      // Convert to DTO
      const response = BookingMapper.toDto(booking, rateQuote);

      this.logger.log(
        `Booking created successfully: ${booking.bookingNumber.value} (${booking.id})`
      );

      // Audit log: Booking created
      await this.auditService.logSuccess(
        AuditAction.BOOKING_CREATED,
        user.id,
        user.email,
        user.organizationId,
        {
          resourceType: 'booking',
          resourceId: booking.id,
          resourceName: booking.bookingNumber.value,
          metadata: {
            rateQuoteId: dto.rateQuoteId,
            status: booking.status.value,
            carrier: rateQuote.carrierName,
          },
        }
      );

      // Send real-time notification
      try {
        const notification = await this.notificationService.notifyBookingCreated(
          user.id,
          user.organizationId,
          booking.bookingNumber.value,
          booking.id
        );
        await this.notificationsGateway.sendNotificationToUser(user.id, notification);
      } catch (error: any) {
        // Don't fail the booking creation if notification fails
        this.logger.error(`Failed to send notification: ${error?.message}`);
      }

      // Trigger webhooks
      try {
        await this.webhookService.triggerWebhooks(
          WebhookEvent.BOOKING_CREATED,
          user.organizationId,
          {
            bookingId: booking.id,
            bookingNumber: booking.bookingNumber.value,
            status: booking.status.value,
            shipper: booking.shipper,
            consignee: booking.consignee,
            carrier: rateQuote.carrierName,
            origin: rateQuote.origin,
            destination: rateQuote.destination,
            etd: rateQuote.etd?.toISOString(),
            eta: rateQuote.eta?.toISOString(),
            createdAt: booking.createdAt.toISOString(),
          }
        );
      } catch (error: any) {
        // Don't fail the booking creation if webhook fails
        this.logger.error(`Failed to trigger webhooks: ${error?.message}`);
      }

      return response;
    } catch (error: any) {
      this.logger.error(
        `Booking creation failed: ${error?.message || 'Unknown error'}`,
        error?.stack
      );

      // Audit log: Booking creation failed
      await this.auditService.logFailure(
        AuditAction.BOOKING_CREATED,
        user.id,
        user.email,
        user.organizationId,
        error?.message || 'Unknown error',
        {
          resourceType: 'booking',
          metadata: {
            rateQuoteId: dto.rateQuoteId,
          },
        }
      );

      throw error;
    }
  }

  @Get(':id')
  @ApiOperation({
    summary: 'Get booking by ID',
    description: 'Retrieve detailed information about a specific booking. Requires authentication.',
  })
  @ApiParam({
    name: 'id',
    description: 'Booking ID (UUID)',
    example: '550e8400-e29b-41d4-a716-446655440000',
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Booking details retrieved successfully',
    type: BookingResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  @ApiNotFoundResponse({
    description: 'Booking not found',
  })
  async getBooking(
    @Param('id', ParseUUIDPipe) id: string,
    @CurrentUser() user: UserPayload
  ): Promise<BookingResponseDto> {
    this.logger.log(`[User: ${user.email}] Fetching booking: ${id}`);

    const booking = await this.bookingRepository.findById(id);
    if (!booking) {
      throw new NotFoundException(`Booking ${id} not found`);
    }

    // Verify booking belongs to user's organization
    if (booking.organizationId !== user.organizationId) {
      throw new NotFoundException(`Booking ${id} not found`);
    }

    // Fetch rate quote
    const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
    if (!rateQuote) {
      throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`);
    }

    return BookingMapper.toDto(booking, rateQuote);
  }

  @Get('number/:bookingNumber')
  @ApiOperation({
    summary: 'Get booking by booking number',
    description:
      'Retrieve detailed information about a specific booking using its booking number. Requires authentication.',
  })
  @ApiParam({
    name: 'bookingNumber',
    description: 'Booking number',
    example: 'WCM-2025-ABC123',
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Booking details retrieved successfully',
    type: BookingResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  @ApiNotFoundResponse({
    description: 'Booking not found',
  })
  async getBookingByNumber(
    @Param('bookingNumber') bookingNumber: string,
    @CurrentUser() user: UserPayload
  ): Promise<BookingResponseDto> {
    this.logger.log(`[User: ${user.email}] Fetching booking by number: ${bookingNumber}`);

    const bookingNumberVo = BookingNumber.fromString(bookingNumber);
    const booking = await this.bookingRepository.findByBookingNumber(bookingNumberVo);

    if (!booking) {
      throw new NotFoundException(`Booking ${bookingNumber} not found`);
    }

    // Verify booking belongs to user's organization
    if (booking.organizationId !== user.organizationId) {
      throw new NotFoundException(`Booking ${bookingNumber} not found`);
    }

    // Fetch rate quote
    const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
    if (!rateQuote) {
      throw new NotFoundException(`Rate quote ${booking.rateQuoteId} not found`);
    }

    return BookingMapper.toDto(booking, rateQuote);
  }

  @Get()
  @ApiOperation({
    summary: 'List bookings',
    description:
      "Retrieve a paginated list of bookings for the authenticated user's organization. Requires authentication.",
  })
  @ApiQuery({
    name: 'page',
    required: false,
    description: 'Page number (1-based)',
    example: 1,
  })
  @ApiQuery({
    name: 'pageSize',
    required: false,
    description: 'Number of items per page',
    example: 20,
  })
  @ApiQuery({
    name: 'status',
    required: false,
    description: 'Filter by booking status',
    enum: ['draft', 'pending_confirmation', 'confirmed', 'in_transit', 'delivered', 'cancelled'],
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Bookings list retrieved successfully',
    type: BookingListResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  async listBookings(
    @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
    @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number,
    @Query('status') status: string | undefined,
    @CurrentUser() user: UserPayload
  ): Promise<BookingListResponseDto> {
    this.logger.log(
      `[User: ${user.email}] Listing bookings: page=${page}, pageSize=${pageSize}, status=${status}`
    );

    // ADMIN: Fetch ALL bookings from database
    // Others: Fetch only bookings from their organization
    let bookings: any[];
    if (user.role === 'ADMIN') {
      this.logger.log(`[ADMIN] Fetching ALL bookings from database`);
      bookings = await this.bookingRepository.findAll();
    } else {
      this.logger.log(`[User] Fetching bookings from organization: ${user.organizationId}`);
      bookings = await this.bookingRepository.findByOrganization(user.organizationId);
    }

    // Filter by status if provided
    const filteredBookings = status
      ? bookings.filter((b: any) => b.status.value === status)
      : bookings;

    // Paginate
    const startIndex = (page - 1) * pageSize;
    const endIndex = startIndex + pageSize;
    const paginatedBookings = filteredBookings.slice(startIndex, endIndex);

    // Fetch rate quotes for all bookings (filter out those with missing rate quotes)
    const bookingsWithQuotesRaw = await Promise.all(
      paginatedBookings.map(async (booking: any) => {
        const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
        return { booking, rateQuote };
      })
    );

    // Filter out bookings with missing rate quotes to avoid null pointer errors
    const bookingsWithQuotes = bookingsWithQuotesRaw.filter(
      (item): item is { booking: any; rateQuote: NonNullable<typeof item.rateQuote> } =>
        item.rateQuote !== null && item.rateQuote !== undefined
    );

    // Convert to DTOs
    const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes);

    const totalPages = Math.ceil(filteredBookings.length / pageSize);

    return {
      bookings: bookingDtos,
      total: filteredBookings.length,
      page,
      pageSize,
      totalPages,
    };
  }

  @Get('search/fuzzy')
  @ApiOperation({
    summary: 'Fuzzy search bookings',
    description:
      'Search bookings using fuzzy matching. Tolerant to typos and partial matches. Searches across booking number, shipper, and consignee names.',
  })
  @ApiQuery({
    name: 'q',
    required: true,
    description: 'Search query (minimum 2 characters)',
    example: 'WCM-2025',
  })
  @ApiQuery({
    name: 'limit',
    required: false,
    description: 'Maximum number of results',
    example: 20,
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Search results retrieved successfully',
    type: [BookingResponseDto],
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  async fuzzySearch(
    @Query('q') searchTerm: string,
    @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
    @CurrentUser() user: UserPayload
  ): Promise<BookingResponseDto[]> {
    this.logger.log(`[User: ${user.email}] Fuzzy search: "${searchTerm}"`);

    if (!searchTerm || searchTerm.length < 2) {
      return [];
    }

    // Perform fuzzy search
    const bookingOrms = await this.fuzzySearchService.search(
      searchTerm,
      user.organizationId,
      limit
    );

    // Map ORM entities to domain and fetch rate quotes
    const bookingsWithQuotesRaw = await Promise.all(
      bookingOrms.map(async bookingOrm => {
        const booking = await this.bookingRepository.findById(bookingOrm.id);
        const rateQuote = await this.rateQuoteRepository.findById(bookingOrm.rateQuoteId);
        return { booking, rateQuote };
      })
    );

    // Filter out bookings or rate quotes that are null
    const bookingsWithQuotes = bookingsWithQuotesRaw.filter(
      (
        item
      ): item is {
        booking: NonNullable<typeof item.booking>;
        rateQuote: NonNullable<typeof item.rateQuote>;
      } =>
        item.booking !== null &&
        item.booking !== undefined &&
        item.rateQuote !== null &&
        item.rateQuote !== undefined
    );

    // Convert to DTOs
    const bookingDtos = bookingsWithQuotes.map(({ booking, rateQuote }) =>
      BookingMapper.toDto(booking, rateQuote)
    );

    this.logger.log(`Fuzzy search returned ${bookingDtos.length} results`);

    return bookingDtos;
  }

  @Get('advanced/search')
  @ApiOperation({
    summary: 'Advanced booking search with filtering',
    description:
      'Search bookings with advanced filtering options including status, date ranges, carrier, ports, shipper/consignee. Supports sorting and pagination.',
  })
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Filtered bookings retrieved successfully',
    type: BookingListResponseDto,
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  async advancedSearch(
    @Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
    @CurrentUser() user: UserPayload
  ): Promise<BookingListResponseDto> {
    this.logger.log(
      `[User: ${user.email}] Advanced search with filters: ${JSON.stringify(filter)}`
    );

    // Fetch all bookings for organization
    let bookings = await this.bookingRepository.findByOrganization(user.organizationId);

    // Apply filters
    bookings = this.applyFilters(bookings, filter);

    // Sort bookings (use defaults if not provided)
    const sortBy = filter.sortBy || 'createdAt';
    const sortOrder = filter.sortOrder || 'desc';
    bookings = this.sortBookings(bookings, sortBy, sortOrder);

    // Total count before pagination
    const total = bookings.length;

    // Paginate
    const startIndex = ((filter.page || 1) - 1) * (filter.pageSize || 20);
    const endIndex = startIndex + (filter.pageSize || 20);
    const paginatedBookings = bookings.slice(startIndex, endIndex);

    // Fetch rate quotes (filter out those with missing rate quotes)
    const bookingsWithQuotesRaw = await Promise.all(
      paginatedBookings.map(async booking => {
        const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
        return { booking, rateQuote };
      })
    );

    // Filter out bookings with missing rate quotes to avoid null pointer errors
    const bookingsWithQuotes = bookingsWithQuotesRaw.filter(
      (item): item is { booking: any; rateQuote: NonNullable<typeof item.rateQuote> } =>
        item.rateQuote !== null && item.rateQuote !== undefined
    );

    // Convert to DTOs
    const bookingDtos = BookingMapper.toListItemDtoArray(bookingsWithQuotes);

    const totalPages = Math.ceil(total / (filter.pageSize || 20));

    return {
      bookings: bookingDtos,
      total,
      page: filter.page || 1,
      pageSize: filter.pageSize || 20,
      totalPages,
    };
  }

  @Post('export')
  @HttpCode(HttpStatus.OK)
  @ApiOperation({
    summary: 'Export bookings to CSV/Excel/JSON',
    description:
      'Export bookings with optional filtering. Supports CSV, Excel (xlsx), and JSON formats.',
  })
  @ApiProduces(
    'text/csv',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    'application/json'
  )
  @ApiResponse({
    status: HttpStatus.OK,
    description: 'Export file generated successfully',
  })
  @ApiResponse({
    status: 401,
    description: 'Unauthorized - missing or invalid token',
  })
  async exportBookings(
    @Body(new ValidationPipe({ transform: true })) exportDto: BookingExportDto,
    @Query(new ValidationPipe({ transform: true })) filter: BookingFilterDto,
    @CurrentUser() user: UserPayload,
    @Res({ passthrough: true }) res: Response
  ): Promise<StreamableFile> {
    this.logger.log(`[User: ${user.email}] Exporting bookings to ${exportDto.format}`);

    let bookings: any[];

    // If specific booking IDs provided, use those
    if (exportDto.bookingIds && exportDto.bookingIds.length > 0) {
      bookings = await Promise.all(
        exportDto.bookingIds.map(id => this.bookingRepository.findById(id))
      );
      bookings = bookings.filter(b => b !== null && b.organizationId === user.organizationId);
    } else {
      // Otherwise, use filter criteria
      bookings = await this.bookingRepository.findByOrganization(user.organizationId);
      bookings = this.applyFilters(bookings, filter);
    }

    // Fetch rate quotes (filter out those with missing rate quotes)
    const bookingsWithQuotesRaw = await Promise.all(
      bookings.map(async booking => {
        const rateQuote = await this.rateQuoteRepository.findById(booking.rateQuoteId);
        return { booking, rateQuote };
      })
    );

    // Filter out bookings with missing rate quotes to avoid null pointer errors
    const bookingsWithQuotes = bookingsWithQuotesRaw.filter(
      (item): item is { booking: any; rateQuote: NonNullable<typeof item.rateQuote> } =>
        item.rateQuote !== null && item.rateQuote !== undefined
    );

    // Generate export file
    const exportResult = await this.exportService.exportBookings(
      bookingsWithQuotes,
      exportDto.format,
      exportDto.fields
    );

    // Set response headers
    res.set({
      'Content-Type': exportResult.contentType,
      'Content-Disposition': `attachment; filename="${exportResult.filename}"`,
    });

    // Audit log: Data exported
    await this.auditService.logSuccess(
      AuditAction.DATA_EXPORTED,
      user.id,
      user.email,
      user.organizationId,
      {
        resourceType: 'booking',
        metadata: {
          format: exportDto.format,
          bookingCount: bookings.length,
          fields: exportDto.fields?.join(', ') || 'all',
          filename: exportResult.filename,
        },
      }
    );

    return new StreamableFile(exportResult.buffer);
  }

  /**
   * Apply filters to bookings array
   */
  private applyFilters(bookings: any[], filter: BookingFilterDto): any[] {
    let filtered = bookings;

    // Filter by status
    if (filter.status && filter.status.length > 0) {
      filtered = filtered.filter(b => filter.status!.includes(b.status.value));
    }

    // Filter by search (booking number partial match)
    if (filter.search) {
      const searchLower = filter.search.toLowerCase();
      filtered = filtered.filter(b => b.bookingNumber.value.toLowerCase().includes(searchLower));
    }

    // Filter by shipper
    if (filter.shipper) {
      const shipperLower = filter.shipper.toLowerCase();
      filtered = filtered.filter(b => b.shipper.name.toLowerCase().includes(shipperLower));
    }

    // Filter by consignee
    if (filter.consignee) {
      const consigneeLower = filter.consignee.toLowerCase();
      filtered = filtered.filter(b => b.consignee.name.toLowerCase().includes(consigneeLower));
    }

    // Filter by creation date range
    if (filter.createdFrom) {
      const fromDate = new Date(filter.createdFrom);
      filtered = filtered.filter(b => b.createdAt >= fromDate);
    }
    if (filter.createdTo) {
      const toDate = new Date(filter.createdTo);
      filtered = filtered.filter(b => b.createdAt <= toDate);
    }

    return filtered;
  }

  /**
   * Sort bookings array
   */
  private sortBookings(bookings: any[], sortBy: string, sortOrder: string): any[] {
    return [...bookings].sort((a, b) => {
      let aValue: any;
      let bValue: any;

      switch (sortBy) {
        case 'bookingNumber':
          aValue = a.bookingNumber.value;
          bValue = b.bookingNumber.value;
          break;
        case 'status':
          aValue = a.status.value;
          bValue = b.status.value;
          break;
        case 'createdAt':
        default:
          aValue = a.createdAt;
          bValue = b.createdAt;
          break;
      }

      if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
      if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
      return 0;
    });
  }
}
