import { Injectable, Logger, NotFoundException, BadRequestException, Inject } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
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 { StoragePort, STORAGE_PORT } from '@domain/ports/out/storage.port';
import {
  Notification,
  NotificationType,
  NotificationPriority,
} from '@domain/entities/notification.entity';
import {
  CreateCsvBookingDto,
  CsvBookingResponseDto,
  CsvBookingDocumentDto,
  CsvBookingListResponseDto,
  CsvBookingStatsDto,
} from '../dto/csv-booking.dto';

/**
 * 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
  ) {}

  /**
   * 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
    const confirmationToken = uuidv4();
    const bookingId = uuidv4();

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

    // Create domain entity
    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,
      documents,
      confirmationToken,
      new Date(),
      undefined,
      dto.notes
    );

    // Save to database
    const savedBooking = await this.csvBookingRepository.create(booking);
    this.logger.log(`CSV booking created with ID: ${bookingId}`);

    // Send email to carrier
    try {
      await this.emailAdapter.sendCsvBookingRequest(dto.carrierEmail, {
        bookingId,
        origin: dto.origin,
        destination: dto.destination,
        volumeCBM: dto.volumeCBM,
        weightKG: dto.weightKG,
        palletCount: dto.palletCount,
        priceUSD: dto.priceUSD,
        priceEUR: dto.priceEUR,
        primaryCurrency: dto.primaryCurrency,
        transitDays: dto.transitDays,
        containerType: dto.containerType,
        documents: documents.map(doc => ({
          type: doc.type,
          fileName: doc.fileName,
        })),
        confirmationToken,
      });
      this.logger.log(`Email sent to carrier: ${dto.carrierEmail}`);
    } catch (error: any) {
      this.logger.error(`Failed to send email to carrier: ${error?.message}`, error?.stack);
      // Continue even if email fails - booking is created
    }

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

    return this.toResponseDto(savedBooking);
  }

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

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

    // Verify user owns this booking
    if (booking.userId !== userId) {
      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);
  }

  /**
   * 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');
    }

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

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

    // 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 {
      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 {
      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();
      const fileKey = `csv-bookings/${bookingId}/${documentId}-${file.originalname}`;

      // 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(file.originalname);

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

      documents.push(document);
    }

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

  /**
   * 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,
      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),
    };
  }

  /**
   * 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,
    };
  }
}
