import {
  Injectable,
  Logger,
  NotFoundException,
  BadRequestException,
  Inject,
  UnauthorizedException,
} from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import * as argon2 from 'argon2';
import { CsvBooking, CsvBookingStatus, DocumentType } from '@domain/entities/csv-booking.entity';
import { PortCode } from '@domain/value-objects/port-code.vo';
import { TypeOrmCsvBookingRepository } from '../../infrastructure/persistence/typeorm/repositories/csv-booking.repository';
import {
  NotificationRepository,
  NOTIFICATION_REPOSITORY,
} from '@domain/ports/out/notification.repository';
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
import { UserRepository, USER_REPOSITORY } from '@domain/ports/out/user.repository';
import { StoragePort, STORAGE_PORT } from '@domain/ports/out/storage.port';
import { StripePort, STRIPE_PORT } from '@domain/ports/out/stripe.port';
import {
  Notification,
  NotificationType,
  NotificationPriority,
} from '@domain/entities/notification.entity';
import {
  CreateCsvBookingDto,
  CsvBookingResponseDto,
  CsvBookingDocumentDto,
  CsvBookingListResponseDto,
  CsvBookingStatsDto,
} from '../dto/csv-booking.dto';
import { CarrierDocumentsResponseDto } from '../dto/carrier-documents.dto';
import { SubscriptionService } from './subscription.service';

/**
 * CSV Booking Document (simple class for domain)
 */
class CsvBookingDocumentImpl {
  constructor(
    public readonly id: string,
    public readonly type: DocumentType,
    public readonly fileName: string,
    public readonly filePath: string,
    public readonly mimeType: string,
    public readonly size: number,
    public readonly uploadedAt: Date
  ) {}
}

/**
 * CSV Booking Service
 *
 * Handles business logic for CSV-based booking requests
 */
@Injectable()
export class CsvBookingService {
  private readonly logger = new Logger(CsvBookingService.name);

  constructor(
    private readonly csvBookingRepository: TypeOrmCsvBookingRepository,
    @Inject(NOTIFICATION_REPOSITORY)
    private readonly notificationRepository: NotificationRepository,
    @Inject(EMAIL_PORT)
    private readonly emailAdapter: EmailPort,
    @Inject(STORAGE_PORT)
    private readonly storageAdapter: StoragePort,
    @Inject(STRIPE_PORT)
    private readonly stripeAdapter: StripePort,
    private readonly subscriptionService: SubscriptionService,
    @Inject(USER_REPOSITORY)
    private readonly userRepository: UserRepository
  ) {}

  /**
   * Generate a unique booking number
   * Format: XPD-YYYY-XXXXXX (e.g., XPD-2025-A3B7K9)
   */
  private generateBookingNumber(): string {
    const year = new Date().getFullYear();
    const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No 0, O, 1, I for clarity
    let code = '';
    for (let i = 0; i < 6; i++) {
      code += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return `XPD-${year}-${code}`;
  }

  /**
   * Extract the password from booking number (last 6 characters)
   */
  private extractPasswordFromBookingNumber(bookingNumber: string): string {
    return bookingNumber.split('-').pop() || bookingNumber.slice(-6);
  }

  /**
   * Create a new CSV booking request
   */
  async createBooking(
    dto: CreateCsvBookingDto,
    files: Express.Multer.File[],
    userId: string,
    organizationId: string
  ): Promise<CsvBookingResponseDto> {
    this.logger.log(`Creating CSV booking for user ${userId}`);

    // Validate minimum document requirement
    if (!files || files.length === 0) {
      throw new BadRequestException('At least one document is required');
    }

    // Generate unique confirmation token and booking number
    const confirmationToken = uuidv4();
    const bookingId = uuidv4();
    const bookingNumber = this.generateBookingNumber();
    const documentPassword = this.extractPasswordFromBookingNumber(bookingNumber);

    // Hash the password for storage
    const passwordHash = await argon2.hash(documentPassword);

    // Upload documents to S3
    const documents = await this.uploadDocuments(files, bookingId);

    // Calculate commission based on organization's subscription plan
    let commissionRate = 5; // default Bronze
    let commissionAmountEur = 0;
    try {
      const subscription = await this.subscriptionService.getOrCreateSubscription(organizationId);
      commissionRate = subscription.plan.commissionRatePercent;
    } catch (error: any) {
      this.logger.error(`Failed to get subscription for commission: ${error?.message}`);
    }
    commissionAmountEur = Math.round(dto.priceEUR * commissionRate) / 100;

    // Create domain entity in PENDING_PAYMENT status (no email sent yet)
    const booking = new CsvBooking(
      bookingId,
      userId,
      organizationId,
      dto.carrierName,
      dto.carrierEmail,
      PortCode.create(dto.origin),
      PortCode.create(dto.destination),
      dto.volumeCBM,
      dto.weightKG,
      dto.palletCount,
      dto.priceUSD,
      dto.priceEUR,
      dto.primaryCurrency,
      dto.transitDays,
      dto.containerType,
      CsvBookingStatus.PENDING_PAYMENT,
      documents,
      confirmationToken,
      new Date(),
      undefined,
      dto.notes,
      undefined,
      bookingNumber,
      commissionRate,
      commissionAmountEur
    );

    // Save to database
    const savedBooking = await this.csvBookingRepository.create(booking);

    // Update ORM entity with booking number and password hash
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });
    if (ormBooking) {
      ormBooking.bookingNumber = bookingNumber;
      ormBooking.passwordHash = passwordHash;
      await this.csvBookingRepository['repository'].save(ormBooking);
    }

    this.logger.log(
      `CSV booking created with ID: ${bookingId}, number: ${bookingNumber}, status: PENDING_PAYMENT, commission: ${commissionRate}% = ${commissionAmountEur}€`
    );

    // NO email sent to carrier yet - will be sent after commission payment
    // NO notification yet - will be created after payment confirmation

    return this.toResponseDto(savedBooking);
  }

  /**
   * Create a Stripe Checkout session for commission payment
   */
  async createCommissionPayment(
    bookingId: string,
    userId: string,
    userEmail: string,
    frontendUrl: string
  ): Promise<{ sessionUrl: string; sessionId: string; commissionAmountEur: number }> {
    const booking = await this.csvBookingRepository.findById(bookingId);

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

    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    if (booking.status !== CsvBookingStatus.PENDING_PAYMENT) {
      throw new BadRequestException(
        `Booking is not awaiting payment. Current status: ${booking.status}`
      );
    }

    const commissionAmountEur = booking.commissionAmountEur || 0;
    if (commissionAmountEur <= 0) {
      throw new BadRequestException('Commission amount is invalid');
    }

    const amountCents = Math.round(commissionAmountEur * 100);

    const result = await this.stripeAdapter.createCommissionCheckout({
      bookingId: booking.id,
      amountCents,
      currency: 'eur',
      customerEmail: userEmail,
      organizationId: booking.organizationId,
      bookingDescription: `Commission booking ${booking.bookingNumber || booking.id} - ${booking.origin.getValue()} → ${booking.destination.getValue()}`,
      successUrl: `${frontendUrl}/dashboard/booking/${booking.id}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
      cancelUrl: `${frontendUrl}/dashboard/booking/${booking.id}/pay`,
    });

    this.logger.log(
      `Created Stripe commission checkout for booking ${bookingId}: ${amountCents} cents EUR`
    );

    return {
      sessionUrl: result.sessionUrl,
      sessionId: result.sessionId,
      commissionAmountEur,
    };
  }

  /**
   * Confirm commission payment and activate booking
   * Called after Stripe redirect with session_id
   */
  async confirmCommissionPayment(
    bookingId: string,
    sessionId: string,
    userId: string
  ): Promise<CsvBookingResponseDto> {
    const booking = await this.csvBookingRepository.findById(bookingId);

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

    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    if (booking.status !== CsvBookingStatus.PENDING_PAYMENT) {
      // Already confirmed - return current state
      if (booking.status === CsvBookingStatus.PENDING) {
        return this.toResponseDto(booking);
      }
      throw new BadRequestException(
        `Booking is not awaiting payment. Current status: ${booking.status}`
      );
    }

    // Verify payment with Stripe
    const session = await this.stripeAdapter.getCheckoutSession(sessionId);
    if (!session || session.status !== 'complete') {
      throw new BadRequestException('Payment has not been completed');
    }

    // Verify the session is for this booking
    if (session.metadata?.bookingId !== bookingId) {
      throw new BadRequestException('Payment session does not match this booking');
    }

    // Transition to PENDING
    booking.markPaymentCompleted();
    booking.stripePaymentIntentId = sessionId;

    // Save updated booking
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(`Booking ${bookingId} payment confirmed, status now PENDING`);

    // Get ORM entity for booking number
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });
    const bookingNumber = ormBooking?.bookingNumber;
    const documentPassword = bookingNumber
      ? this.extractPasswordFromBookingNumber(bookingNumber)
      : undefined;

    // NOW send email to carrier
    try {
      await this.emailAdapter.sendCsvBookingRequest(booking.carrierEmail, {
        bookingId: booking.id,
        bookingNumber: bookingNumber || '',
        documentPassword: documentPassword || '',
        origin: booking.origin.getValue(),
        destination: booking.destination.getValue(),
        volumeCBM: booking.volumeCBM,
        weightKG: booking.weightKG,
        palletCount: booking.palletCount,
        priceUSD: booking.priceUSD,
        priceEUR: booking.priceEUR,
        primaryCurrency: booking.primaryCurrency,
        transitDays: booking.transitDays,
        containerType: booking.containerType,
        documents: booking.documents.map(doc => ({
          type: doc.type,
          fileName: doc.fileName,
        })),
        confirmationToken: booking.confirmationToken,
        notes: booking.notes,
      });
      this.logger.log(`Email sent to carrier: ${booking.carrierEmail}`);
    } catch (error: any) {
      this.logger.error(`Failed to send email to carrier: ${error?.message}`, error?.stack);
    }

    // Create notification for user
    try {
      const notification = Notification.create({
        id: uuidv4(),
        userId: booking.userId,
        organizationId: booking.organizationId,
        type: NotificationType.CSV_BOOKING_REQUEST_SENT,
        priority: NotificationPriority.MEDIUM,
        title: 'Booking Request Sent',
        message: `Your booking request to ${booking.carrierName} for ${booking.getRouteDescription()} has been sent successfully after payment.`,
        metadata: { bookingId: booking.id, carrierName: booking.carrierName },
      });
      await this.notificationRepository.save(notification);
    } catch (error: any) {
      this.logger.error(`Failed to create notification: ${error?.message}`, error?.stack);
    }

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Declare bank transfer — user confirms they have sent the wire transfer
   * Transitions booking from PENDING_PAYMENT → PENDING_BANK_TRANSFER
   * Sends an email notification to all ADMIN users
   */
  async declareBankTransfer(bookingId: string, userId: string): Promise<CsvBookingResponseDto> {
    const booking = await this.csvBookingRepository.findById(bookingId);

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

    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    if (booking.status !== CsvBookingStatus.PENDING_PAYMENT) {
      throw new BadRequestException(
        `Booking is not awaiting payment. Current status: ${booking.status}`
      );
    }

    // Get booking number before update
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });
    const bookingNumber = ormBooking?.bookingNumber || bookingId.slice(0, 8).toUpperCase();

    booking.markBankTransferDeclared();
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(
      `Booking ${bookingId} bank transfer declared, status now PENDING_BANK_TRANSFER`
    );

    // Send email to all ADMIN users
    try {
      const allUsers = await this.userRepository.findAll();
      const adminEmails = allUsers.filter(u => u.role === 'ADMIN' && u.isActive).map(u => u.email);

      if (adminEmails.length > 0) {
        const commissionAmount = booking.commissionAmountEur
          ? new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(
              booking.commissionAmountEur
            )
          : 'N/A';

        await this.emailAdapter.send({
          to: adminEmails,
          subject: `[XPEDITIS] Virement à valider — ${bookingNumber}`,
          html: `
            <div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
              <h2 style="color: #10183A;">Nouveau virement à valider</h2>
              <p>Un client a déclaré avoir effectué un virement bancaire pour le booking suivant :</p>
              <table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
                <tr style="background: #f5f5f5;">
                  <td style="padding: 8px 12px; font-weight: bold;">Numéro de booking</td>
                  <td style="padding: 8px 12px;">${bookingNumber}</td>
                </tr>
                <tr>
                  <td style="padding: 8px 12px; font-weight: bold;">Transporteur</td>
                  <td style="padding: 8px 12px;">${booking.carrierName}</td>
                </tr>
                <tr style="background: #f5f5f5;">
                  <td style="padding: 8px 12px; font-weight: bold;">Trajet</td>
                  <td style="padding: 8px 12px;">${booking.getRouteDescription()}</td>
                </tr>
                <tr>
                  <td style="padding: 8px 12px; font-weight: bold;">Montant commission</td>
                  <td style="padding: 8px 12px; color: #10183A; font-weight: bold;">${commissionAmount}</td>
                </tr>
              </table>
              <p>Rendez-vous dans la <strong>console d'administration</strong> pour valider ce virement et activer le booking.</p>
              <a href="${process.env.APP_URL || 'http://localhost:3000'}/dashboard/admin/bookings"
                 style="display: inline-block; background: #10183A; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none; margin-top: 8px;">
                Voir les bookings en attente
              </a>
            </div>
          `,
        });
        this.logger.log(`Admin notification email sent to: ${adminEmails.join(', ')}`);
      }
    } catch (error: any) {
      this.logger.error(`Failed to send admin notification email: ${error?.message}`, error?.stack);
    }

    // In-app notification for the user
    try {
      const notification = Notification.create({
        id: uuidv4(),
        userId: booking.userId,
        organizationId: booking.organizationId,
        type: NotificationType.BOOKING_UPDATED,
        priority: NotificationPriority.MEDIUM,
        title: 'Virement déclaré',
        message: `Votre virement pour le booking ${bookingNumber} a été enregistré. Un administrateur va vérifier la réception et activer votre booking.`,
        metadata: { bookingId: booking.id },
      });
      await this.notificationRepository.save(notification);
    } catch (error: any) {
      this.logger.error(`Failed to create user notification: ${error?.message}`, error?.stack);
    }

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Resend carrier email for a booking (admin action)
   * Works regardless of payment status — useful for retrying failed emails or testing without Stripe.
   */
  async resendCarrierEmail(bookingId: string): Promise<void> {
    const booking = await this.csvBookingRepository.findById(bookingId);

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

    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });
    const bookingNumber = ormBooking?.bookingNumber;
    const documentPassword = bookingNumber
      ? this.extractPasswordFromBookingNumber(bookingNumber)
      : undefined;

    await this.emailAdapter.sendCsvBookingRequest(booking.carrierEmail, {
      bookingId: booking.id,
      bookingNumber: bookingNumber || '',
      documentPassword: documentPassword || '',
      origin: booking.origin.getValue(),
      destination: booking.destination.getValue(),
      volumeCBM: booking.volumeCBM,
      weightKG: booking.weightKG,
      palletCount: booking.palletCount,
      priceUSD: booking.priceUSD,
      priceEUR: booking.priceEUR,
      primaryCurrency: booking.primaryCurrency,
      transitDays: booking.transitDays,
      containerType: booking.containerType,
      documents: booking.documents.map(doc => ({
        type: doc.type,
        fileName: doc.fileName,
      })),
      confirmationToken: booking.confirmationToken,
      notes: booking.notes,
    });

    this.logger.log(
      `[ADMIN] Carrier email resent to ${booking.carrierEmail} for booking ${bookingId}`
    );
  }

  /**
   * Admin validates bank transfer — confirms receipt and activates booking
   * Transitions booking from PENDING_BANK_TRANSFER → PENDING then sends email to carrier
   */
  async validateBankTransfer(bookingId: string): Promise<CsvBookingResponseDto> {
    const booking = await this.csvBookingRepository.findById(bookingId);

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

    if (booking.status !== CsvBookingStatus.PENDING_BANK_TRANSFER) {
      throw new BadRequestException(
        `Booking is not awaiting bank transfer validation. Current status: ${booking.status}`
      );
    }

    booking.markBankTransferValidated();
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(`Booking ${bookingId} bank transfer validated by admin, status now PENDING`);

    // Get booking number for email
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });
    const bookingNumber = ormBooking?.bookingNumber;
    const documentPassword = bookingNumber
      ? this.extractPasswordFromBookingNumber(bookingNumber)
      : undefined;

    // Send email to carrier
    try {
      await this.emailAdapter.sendCsvBookingRequest(booking.carrierEmail, {
        bookingId: booking.id,
        bookingNumber: bookingNumber || '',
        documentPassword: documentPassword || '',
        origin: booking.origin.getValue(),
        destination: booking.destination.getValue(),
        volumeCBM: booking.volumeCBM,
        weightKG: booking.weightKG,
        palletCount: booking.palletCount,
        priceUSD: booking.priceUSD,
        priceEUR: booking.priceEUR,
        primaryCurrency: booking.primaryCurrency,
        transitDays: booking.transitDays,
        containerType: booking.containerType,
        documents: booking.documents.map(doc => ({
          type: doc.type,
          fileName: doc.fileName,
        })),
        confirmationToken: booking.confirmationToken,
        notes: booking.notes,
      });
      this.logger.log(
        `Email sent to carrier after bank transfer validation: ${booking.carrierEmail}`
      );
    } catch (error: any) {
      this.logger.error(`Failed to send email to carrier: ${error?.message}`, error?.stack);
    }

    // In-app notification for the user
    try {
      const notification = Notification.create({
        id: uuidv4(),
        userId: booking.userId,
        organizationId: booking.organizationId,
        type: NotificationType.BOOKING_CONFIRMED,
        priority: NotificationPriority.HIGH,
        title: 'Virement validé — Booking activé',
        message: `Votre virement pour le booking ${bookingNumber || booking.id.slice(0, 8)} a été confirmé. Votre demande auprès de ${booking.carrierName} a été transmise au transporteur.`,
        metadata: { bookingId: booking.id },
      });
      await this.notificationRepository.save(notification);
    } catch (error: any) {
      this.logger.error(`Failed to create user notification: ${error?.message}`, error?.stack);
    }

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Get booking by ID
   * Accessible by: booking owner OR assigned carrier
   */
  async getBookingById(
    id: string,
    userId: string,
    carrierId?: string
  ): Promise<CsvBookingResponseDto> {
    const booking = await this.csvBookingRepository.findById(id);

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

    // Get ORM booking to access carrierId
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id },
    });

    // Verify user owns this booking OR is the assigned carrier
    const isOwner = booking.userId === userId;
    const isAssignedCarrier = carrierId && ormBooking?.carrierId === carrierId;

    if (!isOwner && !isAssignedCarrier) {
      throw new NotFoundException(`Booking with ID ${id} not found`);
    }

    return this.toResponseDto(booking);
  }

  /**
   * Get booking by confirmation token (public endpoint)
   */
  async getBookingByToken(token: string): Promise<CsvBookingResponseDto> {
    const booking = await this.csvBookingRepository.findByToken(token);

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

    return this.toResponseDto(booking);
  }

  /**
   * Verify password and get booking documents for carrier (public endpoint)
   * Only accessible for ACCEPTED bookings with correct password
   */
  async getDocumentsForCarrier(
    token: string,
    password?: string
  ): Promise<CarrierDocumentsResponseDto> {
    this.logger.log(`Getting documents for carrier with token: ${token}`);

    // Get ORM entity to access passwordHash
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { confirmationToken: token },
    });

    if (!ormBooking) {
      throw new NotFoundException('Réservation introuvable');
    }

    // Only allow access for ACCEPTED bookings
    if (ormBooking.status !== 'ACCEPTED') {
      throw new BadRequestException("Cette réservation n'a pas encore été acceptée");
    }

    // Check if password protection is enabled for this booking
    if (ormBooking.passwordHash) {
      if (!password) {
        throw new UnauthorizedException('Mot de passe requis pour accéder aux documents');
      }

      const isPasswordValid = await argon2.verify(ormBooking.passwordHash, password);
      if (!isPasswordValid) {
        throw new UnauthorizedException('Mot de passe incorrect');
      }
    }

    // Get domain booking for business logic
    const booking = await this.csvBookingRepository.findByToken(token);
    if (!booking) {
      throw new NotFoundException('Réservation introuvable');
    }

    // Generate signed URLs for all documents
    const documentsWithUrls = await Promise.all(
      booking.documents.map(async doc => {
        const signedUrl = await this.generateSignedUrlForDocument(doc.filePath);
        return {
          id: doc.id,
          type: doc.type,
          fileName: doc.fileName,
          mimeType: doc.mimeType,
          size: doc.size,
          downloadUrl: signedUrl,
        };
      })
    );

    const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR';

    return {
      booking: {
        id: booking.id,
        bookingNumber: ormBooking.bookingNumber || undefined,
        carrierName: booking.carrierName,
        origin: booking.origin.getValue(),
        destination: booking.destination.getValue(),
        routeDescription: booking.getRouteDescription(),
        volumeCBM: booking.volumeCBM,
        weightKG: booking.weightKG,
        palletCount: booking.palletCount,
        price: booking.getPriceInCurrency(primaryCurrency),
        currency: primaryCurrency,
        transitDays: booking.transitDays,
        containerType: booking.containerType,
        acceptedAt: booking.respondedAt!,
      },
      documents: documentsWithUrls,
    };
  }

  /**
   * Check if a booking requires password for document access
   */
  async checkDocumentAccessRequirements(
    token: string
  ): Promise<{ requiresPassword: boolean; bookingNumber?: string; status: string }> {
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { confirmationToken: token },
    });

    if (!ormBooking) {
      throw new NotFoundException('Réservation introuvable');
    }

    return {
      requiresPassword: !!ormBooking.passwordHash,
      bookingNumber: ormBooking.bookingNumber || undefined,
      status: ormBooking.status,
    };
  }

  /**
   * Generate signed URL for a document file path
   */
  private async generateSignedUrlForDocument(filePath: string): Promise<string> {
    const bucket = 'xpeditis-documents';

    // Extract key from the file path
    let key = filePath;
    if (filePath.includes('xpeditis-documents/')) {
      key = filePath.split('xpeditis-documents/')[1];
    } else if (filePath.startsWith('http')) {
      const url = new URL(filePath);
      key = url.pathname.replace(/^\//, '');
      if (key.startsWith('xpeditis-documents/')) {
        key = key.replace('xpeditis-documents/', '');
      }
    }

    // Generate signed URL with 1 hour expiration
    const signedUrl = await this.storageAdapter.getSignedUrl({ bucket, key }, 3600);
    return signedUrl;
  }

  /**
   * Accept a booking request
   */
  async acceptBooking(token: string): Promise<CsvBookingResponseDto> {
    this.logger.log(`Accepting booking with token: ${token}`);

    const booking = await this.csvBookingRepository.findByToken(token);

    if (!booking) {
      throw new NotFoundException('Booking not found');
    }

    // Get ORM entity for bookingNumber
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { confirmationToken: token },
    });

    // Accept the booking (domain logic validates status)
    booking.accept();

    // Apply commission based on organization's subscription plan
    try {
      const subscription = await this.subscriptionService.getOrCreateSubscription(
        booking.organizationId
      );
      const commissionRate = subscription.plan.commissionRatePercent;
      const baseAmountEur = booking.priceEUR;
      booking.applyCommission(commissionRate, baseAmountEur);
      this.logger.log(
        `Commission applied: ${commissionRate}% on ${baseAmountEur}€ = ${booking.commissionAmountEur}€`
      );
    } catch (error: any) {
      this.logger.error(`Failed to apply commission: ${error?.message}`, error?.stack);
    }

    // Save updated booking
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(`Booking ${booking.id} accepted`);

    // Extract password from booking number for the email
    const bookingNumber = ormBooking?.bookingNumber;
    const documentPassword = bookingNumber
      ? this.extractPasswordFromBookingNumber(bookingNumber)
      : undefined;

    // Send document access email to carrier
    try {
      await this.emailAdapter.sendDocumentAccessEmail(booking.carrierEmail, {
        carrierName: booking.carrierName,
        bookingId: booking.id,
        bookingNumber: bookingNumber || undefined,
        documentPassword: documentPassword,
        origin: booking.origin.getValue(),
        destination: booking.destination.getValue(),
        volumeCBM: booking.volumeCBM,
        weightKG: booking.weightKG,
        documentCount: booking.documents.length,
        confirmationToken: booking.confirmationToken,
      });
      this.logger.log(`Document access email sent to carrier: ${booking.carrierEmail}`);
    } catch (error: any) {
      this.logger.error(`Failed to send document access email: ${error?.message}`, error?.stack);
    }

    // Create notification for user
    try {
      const notification = Notification.create({
        id: uuidv4(),
        userId: booking.userId,
        organizationId: booking.organizationId,
        type: NotificationType.CSV_BOOKING_ACCEPTED,
        priority: NotificationPriority.HIGH,
        title: 'Booking Request Accepted',
        message: `Your booking request to ${booking.carrierName} for ${booking.getRouteDescription()} has been accepted!`,
        metadata: { bookingId: booking.id, carrierName: booking.carrierName },
      });
      await this.notificationRepository.save(notification);
    } catch (error: any) {
      this.logger.error(`Failed to create notification: ${error?.message}`, error?.stack);
    }

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Reject a booking request
   */
  async rejectBooking(token: string, reason?: string): Promise<CsvBookingResponseDto> {
    this.logger.log(`Rejecting booking with token: ${token}`);

    const booking = await this.csvBookingRepository.findByToken(token);

    if (!booking) {
      throw new NotFoundException('Booking not found');
    }

    // Reject the booking (domain logic validates status)
    booking.reject(reason);

    // Save updated booking
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(`Booking ${booking.id} rejected`);

    // Create notification for user
    try {
      const notification = Notification.create({
        id: uuidv4(),
        userId: booking.userId,
        organizationId: booking.organizationId,
        type: NotificationType.CSV_BOOKING_REJECTED,
        priority: NotificationPriority.HIGH,
        title: 'Booking Request Rejected',
        message: `Your booking request to ${booking.carrierName} for ${booking.getRouteDescription()} was rejected. ${reason ? `Reason: ${reason}` : ''}`,
        metadata: {
          bookingId: booking.id,
          carrierName: booking.carrierName,
          rejectionReason: reason,
        },
      });
      await this.notificationRepository.save(notification);
    } catch (error: any) {
      this.logger.error(`Failed to create notification: ${error?.message}`, error?.stack);
    }

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Cancel a booking (user action)
   */
  async cancelBooking(id: string, userId: string): Promise<CsvBookingResponseDto> {
    this.logger.log(`Cancelling booking ${id} by user ${userId}`);

    const booking = await this.csvBookingRepository.findById(id);

    if (!booking) {
      throw new NotFoundException('Booking not found');
    }

    // Verify user owns this booking
    if (booking.userId !== userId) {
      throw new NotFoundException('Booking not found');
    }

    // Cancel the booking (domain logic validates status)
    booking.cancel();

    // Save updated booking
    const updatedBooking = await this.csvBookingRepository.update(booking);
    this.logger.log(`Booking ${id} cancelled`);

    return this.toResponseDto(updatedBooking);
  }

  /**
   * Get bookings for a user (paginated)
   */
  async getUserBookings(
    userId: string,
    page: number = 1,
    limit: number = 10
  ): Promise<CsvBookingListResponseDto> {
    const bookings = await this.csvBookingRepository.findByUserId(userId);

    // Simple pagination (in-memory)
    const start = (page - 1) * limit;
    const end = start + limit;
    const paginatedBookings = bookings.slice(start, end);

    return {
      bookings: paginatedBookings.map(b => this.toResponseDto(b)),
      total: bookings.length,
      page,
      limit,
      totalPages: Math.ceil(bookings.length / limit),
    };
  }

  /**
   * Get bookings for an organization (paginated)
   */
  async getOrganizationBookings(
    organizationId: string,
    page: number = 1,
    limit: number = 10
  ): Promise<CsvBookingListResponseDto> {
    const bookings = await this.csvBookingRepository.findByOrganizationId(organizationId);

    // Simple pagination (in-memory)
    const start = (page - 1) * limit;
    const end = start + limit;
    const paginatedBookings = bookings.slice(start, end);

    return {
      bookings: paginatedBookings.map(b => this.toResponseDto(b)),
      total: bookings.length,
      page,
      limit,
      totalPages: Math.ceil(bookings.length / limit),
    };
  }

  /**
   * Get booking statistics for user
   */
  async getUserStats(userId: string): Promise<CsvBookingStatsDto> {
    const stats = await this.csvBookingRepository.countByStatusForUser(userId);

    return {
      pendingPayment: stats[CsvBookingStatus.PENDING_PAYMENT] || 0,
      pending: stats[CsvBookingStatus.PENDING] || 0,
      accepted: stats[CsvBookingStatus.ACCEPTED] || 0,
      rejected: stats[CsvBookingStatus.REJECTED] || 0,
      cancelled: stats[CsvBookingStatus.CANCELLED] || 0,
      total: Object.values(stats).reduce((sum, count) => sum + count, 0),
    };
  }

  /**
   * Get booking statistics for organization
   */
  async getOrganizationStats(organizationId: string): Promise<CsvBookingStatsDto> {
    const stats = await this.csvBookingRepository.countByStatusForOrganization(organizationId);

    return {
      pendingPayment: stats[CsvBookingStatus.PENDING_PAYMENT] || 0,
      pending: stats[CsvBookingStatus.PENDING] || 0,
      accepted: stats[CsvBookingStatus.ACCEPTED] || 0,
      rejected: stats[CsvBookingStatus.REJECTED] || 0,
      cancelled: stats[CsvBookingStatus.CANCELLED] || 0,
      total: Object.values(stats).reduce((sum, count) => sum + count, 0),
    };
  }

  /**
   * Upload documents to S3 and create document entities
   */
  private async uploadDocuments(
    files: Express.Multer.File[],
    bookingId: string
  ): Promise<CsvBookingDocumentImpl[]> {
    const bucket = 'xpeditis-documents'; // You can make this configurable
    const documents: CsvBookingDocumentImpl[] = [];

    for (const file of files) {
      const documentId = uuidv4();
      // Multer decodes originalname as latin1; re-encode to utf8 to preserve accented characters
      const fileName = Buffer.from(file.originalname, 'latin1').toString('utf8');
      const fileKey = `csv-bookings/${bookingId}/${documentId}-${fileName}`;

      // Upload to S3
      const uploadResult = await this.storageAdapter.upload({
        bucket,
        key: fileKey,
        body: file.buffer,
        contentType: file.mimetype,
      });

      // Determine document type from filename or default to OTHER
      const documentType = this.inferDocumentType(fileName);

      const document = new CsvBookingDocumentImpl(
        documentId,
        documentType,
        fileName,
        uploadResult.url,
        file.mimetype,
        file.size,
        new Date()
      );

      documents.push(document);
    }

    this.logger.log(`Uploaded ${documents.length} documents for booking ${bookingId}`);
    return documents;
  }

  /**
   * Link a booking to a carrier profile
   */
  async linkBookingToCarrier(bookingId: string, carrierId: string): Promise<void> {
    this.logger.log(`Linking booking ${bookingId} to carrier ${carrierId}`);

    const booking = await this.csvBookingRepository.findById(bookingId);

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

    // Update the booking with carrier ID (using the ORM repository directly)
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });

    if (ormBooking) {
      ormBooking.carrierId = carrierId;
      await this.csvBookingRepository['repository'].save(ormBooking);
      this.logger.log(`Successfully linked booking ${bookingId} to carrier ${carrierId}`);
    }
  }

  /**
   * Add documents to an existing booking
   */
  async addDocuments(
    bookingId: string,
    files: Express.Multer.File[],
    userId: string
  ): Promise<{ success: boolean; message: string; documentsAdded: number }> {
    this.logger.log(`Adding ${files.length} documents to booking ${bookingId}`);

    const booking = await this.csvBookingRepository.findById(bookingId);

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

    // Verify user owns this booking
    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    // Allow adding documents to PENDING_PAYMENT, PENDING_BANK_TRANSFER, PENDING, or ACCEPTED bookings
    if (
      booking.status !== CsvBookingStatus.PENDING_PAYMENT &&
      booking.status !== CsvBookingStatus.PENDING_BANK_TRANSFER &&
      booking.status !== CsvBookingStatus.PENDING &&
      booking.status !== CsvBookingStatus.ACCEPTED
    ) {
      throw new BadRequestException(
        'Cannot add documents to a booking that is rejected or cancelled'
      );
    }

    // Upload new documents
    const newDocuments = await this.uploadDocuments(files, bookingId);

    // Add documents to booking
    const updatedDocuments = [...booking.documents, ...newDocuments];

    // Update booking in database using ORM repository directly
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });

    if (ormBooking) {
      ormBooking.documents = updatedDocuments.map(doc => ({
        id: doc.id,
        type: doc.type,
        fileName: doc.fileName,
        filePath: doc.filePath,
        mimeType: doc.mimeType,
        size: doc.size,
        uploadedAt: doc.uploadedAt,
      }));
      await this.csvBookingRepository['repository'].save(ormBooking);
    }

    this.logger.log(`Added ${newDocuments.length} documents to booking ${bookingId}`);

    // If booking is ACCEPTED, notify carrier about new documents
    if (booking.status === CsvBookingStatus.ACCEPTED) {
      try {
        await this.emailAdapter.sendNewDocumentsNotification(booking.carrierEmail, {
          carrierName: booking.carrierName,
          bookingId: booking.id,
          origin: booking.origin.getValue(),
          destination: booking.destination.getValue(),
          newDocumentsCount: newDocuments.length,
          totalDocumentsCount: updatedDocuments.length,
          confirmationToken: booking.confirmationToken,
        });
        this.logger.log(`New documents notification sent to carrier: ${booking.carrierEmail}`);
      } catch (error: any) {
        this.logger.error(
          `Failed to send new documents notification: ${error?.message}`,
          error?.stack
        );
      }
    }

    return {
      success: true,
      message: 'Documents added successfully',
      documentsAdded: newDocuments.length,
    };
  }

  /**
   * Delete a document from a booking
   */
  async deleteDocument(
    bookingId: string,
    documentId: string,
    userId: string
  ): Promise<{ success: boolean; message: string }> {
    this.logger.log(`Deleting document ${documentId} from booking ${bookingId}`);

    const booking = await this.csvBookingRepository.findById(bookingId);

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

    // Verify user owns this booking
    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    // Verify booking is still pending or awaiting payment
    if (
      booking.status !== CsvBookingStatus.PENDING_PAYMENT &&
      booking.status !== CsvBookingStatus.PENDING
    ) {
      throw new BadRequestException('Cannot delete documents from a booking that is not pending');
    }

    // Find the document
    const documentIndex = booking.documents.findIndex(doc => doc.id === documentId);

    if (documentIndex === -1) {
      throw new NotFoundException(`Document with ID ${documentId} not found`);
    }

    // Ensure at least one document remains
    if (booking.documents.length <= 1) {
      throw new BadRequestException(
        'Cannot delete the last document. At least one document is required.'
      );
    }

    // Get the document to delete (for potential S3 cleanup - currently kept for audit)
    const _documentToDelete = booking.documents[documentIndex];

    // Remove document from array
    const updatedDocuments = booking.documents.filter(doc => doc.id !== documentId);

    // Update booking in database using ORM repository directly
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });

    if (ormBooking) {
      ormBooking.documents = updatedDocuments.map(doc => ({
        id: doc.id,
        type: doc.type,
        fileName: doc.fileName,
        filePath: doc.filePath,
        mimeType: doc.mimeType,
        size: doc.size,
        uploadedAt: doc.uploadedAt,
      }));
      await this.csvBookingRepository['repository'].save(ormBooking);
    }

    // Optionally delete from S3 (commented out for safety - keep files for audit)
    // try {
    //   await this.storageAdapter.delete({
    //     bucket: 'xpeditis-documents',
    //     key: documentToDelete.filePath,
    //   });
    // } catch (error) {
    //   this.logger.warn(`Failed to delete file from S3: ${documentToDelete.filePath}`);
    // }

    this.logger.log(`Deleted document ${documentId} from booking ${bookingId}`);

    return {
      success: true,
      message: 'Document deleted successfully',
    };
  }

  /**
   * Replace a document in an existing booking
   */
  async replaceDocument(
    bookingId: string,
    documentId: string,
    file: Express.Multer.File,
    userId: string
  ): Promise<{ success: boolean; message: string; newDocument: any }> {
    this.logger.log(`Replacing document ${documentId} in booking ${bookingId}`);

    const booking = await this.csvBookingRepository.findById(bookingId);

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

    // Verify user owns this booking
    if (booking.userId !== userId) {
      throw new NotFoundException(`Booking with ID ${bookingId} not found`);
    }

    // Find the document to replace
    const documentIndex = booking.documents.findIndex(doc => doc.id === documentId);

    if (documentIndex === -1) {
      throw new NotFoundException(`Document with ID ${documentId} not found`);
    }

    // Upload the new document
    const newDocuments = await this.uploadDocuments([file], bookingId);
    const newDocument = newDocuments[0];

    // Replace the document in the array
    const updatedDocuments = [...booking.documents];
    updatedDocuments[documentIndex] = newDocument;

    // Update booking in database using ORM repository directly
    const ormBooking = await this.csvBookingRepository['repository'].findOne({
      where: { id: bookingId },
    });

    if (ormBooking) {
      ormBooking.documents = updatedDocuments.map(doc => ({
        id: doc.id,
        type: doc.type,
        fileName: doc.fileName,
        filePath: doc.filePath,
        mimeType: doc.mimeType,
        size: doc.size,
        uploadedAt: doc.uploadedAt,
      }));
      await this.csvBookingRepository['repository'].save(ormBooking);
    }

    this.logger.log(
      `Replaced document ${documentId} with ${newDocument.id} in booking ${bookingId}`
    );

    return {
      success: true,
      message: 'Document replaced successfully',
      newDocument: {
        id: newDocument.id,
        type: newDocument.type,
        fileName: newDocument.fileName,
        filePath: newDocument.filePath,
        mimeType: newDocument.mimeType,
        size: newDocument.size,
        uploadedAt: newDocument.uploadedAt,
      },
    };
  }

  /**
   * Infer document type from filename
   */
  private inferDocumentType(filename: string): DocumentType {
    const lowerFilename = filename.toLowerCase();

    if (
      lowerFilename.includes('bill') ||
      lowerFilename.includes('bol') ||
      lowerFilename.includes('lading')
    ) {
      return DocumentType.BILL_OF_LADING;
    }
    if (lowerFilename.includes('packing') || lowerFilename.includes('list')) {
      return DocumentType.PACKING_LIST;
    }
    if (lowerFilename.includes('invoice') || lowerFilename.includes('commercial')) {
      return DocumentType.COMMERCIAL_INVOICE;
    }
    if (lowerFilename.includes('certificate') || lowerFilename.includes('origin')) {
      return DocumentType.CERTIFICATE_OF_ORIGIN;
    }

    return DocumentType.OTHER;
  }

  /**
   * Convert domain entity to response DTO
   */
  private toResponseDto(booking: CsvBooking): CsvBookingResponseDto {
    const primaryCurrency = booking.primaryCurrency as 'USD' | 'EUR';

    return {
      id: booking.id,
      bookingNumber: booking.bookingNumber,
      userId: booking.userId,
      organizationId: booking.organizationId,
      carrierName: booking.carrierName,
      carrierEmail: booking.carrierEmail,
      origin: booking.origin.getValue(),
      destination: booking.destination.getValue(),
      volumeCBM: booking.volumeCBM,
      weightKG: booking.weightKG,
      palletCount: booking.palletCount,
      priceUSD: booking.priceUSD,
      priceEUR: booking.priceEUR,
      primaryCurrency: booking.primaryCurrency,
      transitDays: booking.transitDays,
      containerType: booking.containerType,
      status: booking.status,
      documents: booking.documents.map(this.toDocumentDto),
      confirmationToken: booking.confirmationToken,
      requestedAt: booking.requestedAt,
      respondedAt: booking.respondedAt || null,
      notes: booking.notes,
      rejectionReason: booking.rejectionReason,
      routeDescription: booking.getRouteDescription(),
      isExpired: booking.isExpired(),
      price: booking.getPriceInCurrency(primaryCurrency),
      commissionRate: booking.commissionRate,
      commissionAmountEur: booking.commissionAmountEur,
    };
  }

  /**
   * Convert domain document to DTO
   */
  private toDocumentDto(document: any): CsvBookingDocumentDto {
    return {
      id: document.id,
      type: document.type,
      fileName: document.fileName,
      filePath: document.filePath,
      mimeType: document.mimeType,
      size: document.size,
      uploadedAt: document.uploadedAt,
    };
  }
}
