import {
  BadRequestException,
  ConflictException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
// import { CustomersService } from '../customers/customers.service';
import { CompaniesService } from '../companies/companies.service';
import { FilesService } from '../files/files.service';
import { MinioService } from '../minio/minio.service';
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
import {
  BookingEvaluationInput,
  RuleEngineService,
} from '../rule-engine/rule-engine.service';
import { BookingsRepository } from './bookings.repository';
import { ConsolidationService } from './consolidation.service';
import { assertFreightShape } from './booking-freight.util';
import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto';
import { mapStatusCountsToTabs } from './booking-list-tabs.config';
import { BookingListSummaryDto } from './dto/booking-list-summary.dto';
import { FilterBookingDto } from './dto/filter-booking.dto';
import { UpdateBookingDto } from './dto/update-booking.dto';
import {
  BOOKING_STATUSES,
  CUSTOMER_EDITABLE_STATUSES,
  FreightType,
} from './entities/booking.entity';
import { Booking } from './entities/booking.entity';
import { FileRecord } from '../files/entities/file.entity';

const URGENT_PRIORITY_THRESHOLD = 1000;
const NEEDS_ACTION_STATUSES = [
  'SUBMITTED',
  'PENDING_APPROVAL',
  'APPROVED_PENDING_SIGNATURE',
] as const;

@Injectable()
export class BookingsService {
  constructor(
    private readonly bookingsRepository: BookingsRepository,
    private readonly filesService: FilesService,
    private readonly minioService: MinioService,
    // private readonly customersService: CustomersService,
    private readonly companiesService: CompaniesService,
    private readonly ruleEngineService: RuleEngineService,
    private readonly containerTypesService: ContainerTypesService,
    private readonly consolidationService: ConsolidationService,
  ) {}

  /** Generate a unique booking reference number. */
  private async generateReference(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.bookingsRepository.countByYear(year);
    return `BK-${year}-${String(count + 1).padStart(6, '0')}`;
  }

  /** Build evaluation input from booking freight shape. */
  private async buildEvalInput(dto: {
    freightType: FreightType;
    cargoTypeId?: string | null;
    serviceTypeId: string;
    paymentCurrency: string;
    tradeDirection: string;
    isHazardous?: boolean;
    allowConsolidation?: boolean;
    shippingLineId?: string | null;
    containers: CreateBookingContainerDto[];
  }): Promise<BookingEvaluationInput> {
    const containerLines =
      dto.freightType === 'CONTAINER' ? dto.containers : [];

    const containers = await Promise.all(
      containerLines.map(async (c) => {
        const ct = await this.containerTypesService.findById(c.containerTypeId);
        const totalVgmTons = c.quantity * c.vgmPerUnitTons;
        return {
          containerTypeId: c.containerTypeId,
          quantity: c.quantity,
          vgmPerUnitTons: c.vgmPerUnitTons,
          totalVgmTons,
          isReefer: ct.isReefer,
        };
      }),
    );

    return {
      freightType: dto.freightType,
      cargoTypeId: dto.cargoTypeId ?? null,
      serviceTypeId: dto.serviceTypeId,
      paymentCurrency: dto.paymentCurrency,
      tradeDirection: dto.tradeDirection,
      isHazardous: dto.isHazardous ?? false,
      allowConsolidation:
        dto.freightType === 'CONTAINER' ? dto.allowConsolidation : false,
      shippingLineId: dto.shippingLineId,
      containers,
    };
  }

  /**
   * Enable consolidation when any container line leaves a wagon partially filled
   * (e.g. 1×20ft on a 2-slot wagon, 1×10ft on a 4-slot wagon), unless opted out.
   */
  private async resolveConsolidation(
    containers: CreateBookingContainerDto[],
    explicit?: boolean,
  ): Promise<boolean> {
    if (explicit === false) return false;
    const needs = await this.consolidationService.needsConsolidation(
      containers.map((c) => ({
        containerTypeId: c.containerTypeId,
        quantity: c.quantity,
      })),
    );
    if (needs) return true;
    return explicit ?? false;
  }

  /** Search for a complementary partner; pair or queue as PENDING_CONSOLIDATION. */
  private async tryAutoConsolidate(booking: Booking): Promise<{
    booking: Booking;
    messages: string[];
  }> {
    const messages: string[] = [];

    if (!booking.allowConsolidation || booking.consolidationPartnerId) {
      return { booking, messages };
    }

    const slots = await this.consolidationService.slotsFromBooking(booking);
    if (slots.length === 0) {
      return { booking, messages };
    }

    const partner = await this.bookingsRepository.findConsolidationPartner(
      booking,
      slots,
    );

    if (partner) {
      await this.bookingsRepository.pairConsolidation(booking.id, partner.id);
      const paired = await this.findById(booking.id);
      messages.push(
        this.consolidationService.describePaired(partner.reference, slots),
      );
      return { booking: paired, messages };
    }

    if (booking.status === 'DRAFT') {
      await this.bookingsRepository.update(booking.id, {
        status: 'PENDING_CONSOLIDATION',
      } as never);
    }

    const pending = await this.findById(booking.id);
    messages.push(this.consolidationService.describePending(pending, slots));
    return { booking: pending, messages };
  }

  /** Create a new freight booking. */
  async create(
    dto: CreateBookingDto,
    files: Express.Multer.File[],
    userId?: string,
  ): Promise<{ booking: Booking; warnings: string[] }> {
    const warnings: string[] = [];

    // let customerId = dto.customerId;
    // if (!customerId) {
    //   if (!userId) {
    //     throw new BadRequestException(
    //       'customerId is required or must be resolvable from auth token',
    //     );
    //   }
    //   const customer = await this.customersService.findByUserId(userId);
    //   customerId = customer.id;
    // }

    let companyId = dto.companyId;
    if (!companyId) {
      if (!userId) {
        throw new BadRequestException(
          'companyId is required or must be resolvable from auth token',
        );
      }
      const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
      companyId = company.id;
    }

    const reference = dto.reference || (await this.generateReference());
    const containers = dto.containers ?? [];
    assertFreightShape({
      freightType: dto.freightType,
      cargoTypeId: dto.cargoTypeId,
      containers,
    });

    const allowConsolidation =
      dto.freightType === 'CONTAINER'
        ? await this.resolveConsolidation(containers, dto.allowConsolidation)
        : false;

    const evalInput = await this.buildEvalInput({
      freightType: dto.freightType as FreightType,
      cargoTypeId: dto.freightType === 'BULK' ? dto.cargoTypeId : null,
      serviceTypeId: dto.serviceTypeId,
      paymentCurrency: dto.paymentCurrency,
      tradeDirection: dto.tradeDirection,
      isHazardous: dto.isHazardous,
      allowConsolidation,
      shippingLineId: dto.shippingLineId,
      containers,
    });
    const ruleResult = await this.ruleEngineService.evaluate(evalInput);
    this.ruleEngineService.assertNoHardBlocks(ruleResult);

    warnings.push(...ruleResult.warnings);

    const booking = await this.bookingsRepository.create({
      reference,
      companyId,
      trainId: dto.trainId,
      contractType: dto.contractType,
      previousContractId: dto.previousContractId,
      serviceTypeId: dto.serviceTypeId,
      firstMilePickupAddress: dto.firstMilePickupAddress,
      lastMileDeliveryAddress: dto.lastMileDeliveryAddress,
      equipmentReturn: dto.equipmentReturn,
      originYardId: dto.originYardId,
      destinationYardId: dto.destinationYardId,
      tradeDirection: dto.tradeDirection,
      freightType: dto.freightType,
      cargoTypeId: dto.freightType === 'BULK' ? dto.cargoTypeId! : null,
      cargoFreeText: dto.cargoFreeText,
      shippingLineId: dto.shippingLineId,
      cargoTotalWeightVgm: dto.cargoTotalWeightVgm,
      isHazardous: dto.isHazardous ?? false,
      paymentCurrency: dto.paymentCurrency,
      pnrCode: dto.pnrCode,
      financialTerms: dto.financialTerms,
      scheduledDate: new Date(dto.scheduledDate),
      startDate: dto.startDate ? new Date(dto.startDate) : undefined,
      endDate: dto.endDate ? new Date(dto.endDate) : undefined,
      status: 'DRAFT',
      allowConsolidation,
      priorityScore: ruleResult.priorityScore,
      totalAmount: 0,
      paymentStatus: 'PENDING',
    });

    if (dto.freightType === 'CONTAINER') {
      await this.bookingsRepository.createContainers(
        booking.id,
        containers.map((c, i) => ({
          containerTypeId: c.containerTypeId,
          quantity: c.quantity,
          vgmPerUnitTons: c.vgmPerUnitTons,
          weightResult: ruleResult.containerWeightResults[i],
        })),
      );
      const wagonCount = await this.bookingsRepository.calculateWagonCount(booking.id);
      warnings.push(`Estimated wagons required: ${wagonCount}`);
    }

    if (files.length > 0) {
      try {
        await this.filesService.uploadMany(booking.id, 'bookings', files);
      } catch {
        warnings.push('File upload failed — booking was created without attached files.');
      }
    }

    let full = await this.findById(booking.id);

    if (allowConsolidation) {
      const consolidation = await this.tryAutoConsolidate(full);
      full = consolidation.booking;
      warnings.push(...consolidation.messages);
    }

    return { booking: full, warnings };
  }

  /** Update a draft booking. */
  async update(
    id: string,
    dto: UpdateBookingDto,
    files: Express.Multer.File[],
  ): Promise<{ booking: Booking; warnings: string[] }> {
    const existing = await this.findById(id);
    if (!CUSTOMER_EDITABLE_STATUSES.includes(existing.status as never)) {
      throw new BadRequestException(
        'Only DRAFT or CHANGES_REQUESTED bookings can be updated',
      );
    }

    const warnings: string[] = [];
    const freightType = (dto.freightType ?? existing.freightType) as FreightType;
    let containers =
      dto.containers ??
      existing.bookingContainers?.map((bc) => ({
        containerTypeId: bc.containerTypeId,
        quantity: bc.quantity,
        vgmPerUnitTons: Number(bc.vgmPerUnitTons),
      })) ??
      [];

    let cargoTypeId =
      dto.cargoTypeId !== undefined ? dto.cargoTypeId : existing.cargoTypeId;

    if (freightType === 'BULK') {
      containers = [];
      if (dto.containers !== undefined) {
        await this.bookingsRepository.deleteContainers(id);
      }
    } else {
      cargoTypeId = null;
      if (dto.cargoTypeId !== undefined && dto.cargoTypeId !== null) {
        throw new BadRequestException('cargoTypeId is not allowed for CONTAINER freight');
      }
    }

    assertFreightShape({ freightType, cargoTypeId, containers });

    const allowConsolidation =
      freightType === 'CONTAINER'
        ? await this.resolveConsolidation(
            containers,
            dto.allowConsolidation ?? existing.allowConsolidation,
          )
        : false;

    const evalInput = await this.buildEvalInput({
      freightType,
      cargoTypeId,
      serviceTypeId: dto.serviceTypeId ?? existing.serviceTypeId,
      paymentCurrency: dto.paymentCurrency ?? existing.paymentCurrency,
      tradeDirection: dto.tradeDirection ?? existing.tradeDirection,
      isHazardous: dto.isHazardous ?? existing.isHazardous,
      allowConsolidation,
      shippingLineId: dto.shippingLineId ?? existing.shippingLineId ?? undefined,
      containers,
    });

    const ruleResult = await this.ruleEngineService.evaluate(evalInput);
    this.ruleEngineService.assertNoHardBlocks(ruleResult);
    warnings.push(...ruleResult.warnings);

    const updates: Record<string, unknown> = {
      ...dto,
      freightType,
      cargoTypeId: freightType === 'BULK' ? cargoTypeId : null,
      allowConsolidation,
      priorityScore: ruleResult.priorityScore,
    };
    if (dto.scheduledDate) updates.scheduledDate = new Date(dto.scheduledDate);
    if (dto.startDate) updates.startDate = new Date(dto.startDate);
    if (dto.endDate) updates.endDate = new Date(dto.endDate);
    delete updates.containers;

    await this.bookingsRepository.update(id, updates);

    if (freightType === 'CONTAINER' && dto.containers) {
      await this.bookingsRepository.deleteContainers(id);
      await this.bookingsRepository.createContainers(
        id,
        dto.containers.map((c, i) => ({
          containerTypeId: c.containerTypeId,
          quantity: c.quantity,
          vgmPerUnitTons: c.vgmPerUnitTons,
          weightResult: ruleResult.containerWeightResults[i],
        })),
      );
    }

    if (files.length > 0) {
      await this.filesService.uploadMany(id, 'bookings', files);
    }

    let booking = await this.findById(id);

    if (allowConsolidation && !booking.consolidationPartnerId) {
      const consolidation = await this.tryAutoConsolidate(booking);
      booking = consolidation.booking;
      warnings.push(...consolidation.messages);
    }

    return { booking, warnings };
  }

  /** Parse comma-separated or repeated status query values. */
  private parseStatusFilter(filter: FilterBookingDto): {
    statuses?: string[];
    status?: string;
  } {
    const allowed = new Set<string>(BOOKING_STATUSES);
    const raw = filter.statuses;
    const statusList = raw
      ? raw
          .split(',')
          .map((s) => s.trim())
          .filter((s) => allowed.has(s))
      : [];

    if (statusList.length > 0) {
      return { statuses: statusList };
    }
    if (filter.status && allowed.has(filter.status)) {
      return { status: filter.status };
    }
    return {};
  }

  /** Return a paginated list of bookings matching the filter. */
  async findAll(
    filter: FilterBookingDto,
  ): Promise<{ items: Booking[]; total: number }> {
    const page = filter.page ?? 1;
    const pageSize = filter.pageSize ?? 20;
    const statusFilter = this.parseStatusFilter(filter);

    return this.bookingsRepository.findAllPaginated({
      page,
      pageSize,
      ...statusFilter,
      companyId: filter.companyId,
      contractType: filter.contractType,
      serviceTypeId: filter.serviceTypeId,
      cargoTypeId: filter.cargoTypeId,
      freightType: filter.freightType,
      tradeDirection: filter.tradeDirection,
      paymentCurrency: filter.paymentCurrency,
      allowConsolidation: filter.allowConsolidation,
      consolidationPaired: filter.consolidationPaired,
      sortBy: filter.sortBy,
      sortOrder: filter.sortOrder,
    });
  }

  /** Aggregate metrics and tab counts for the backoffice booking list. */
  async getListSummary(filter: FilterBookingDto): Promise<BookingListSummaryDto> {
    const page = filter.page ?? 1;
    const pageSize = filter.pageSize ?? 20;
    const statusFilter = this.parseStatusFilter(filter);
    const listFilter = {
      ...statusFilter,
      companyId: filter.companyId,
      contractType: filter.contractType,
      serviceTypeId: filter.serviceTypeId,
      cargoTypeId: filter.cargoTypeId,
      freightType: filter.freightType,
      tradeDirection: filter.tradeDirection,
      paymentCurrency: filter.paymentCurrency,
      allowConsolidation: filter.allowConsolidation,
      consolidationPaired: filter.consolidationPaired,
    };

    const [statusCounts, metrics] = await Promise.all([
      this.bookingsRepository.getStatusCounts(),
      this.bookingsRepository.getListSummaryMetrics({
        ...listFilter,
        page,
        pageSize,
        needsActionStatuses: NEEDS_ACTION_STATUSES,
        urgentPriorityThreshold: URGENT_PRIORITY_THRESHOLD,
      }),
    ]);

    return {
      metrics,
      tabs: mapStatusCountsToTabs(statusCounts),
    };
  }

  /** Get a single booking by ID with files. */
  async findById(id: string): Promise<Booking> {
    const booking = await this.bookingsRepository.findByIdWithFiles(id);
    if (!booking) {
      throw new NotFoundException(`Booking ${id} not found`);
    }

    if (booking.files && booking.files.length > 0) {
      booking.files = await Promise.all(
        booking.files.map(async (file: FileRecord) => {
          const objectName = this.minioService.getObjectNameFromUrl(file.url);
          const signedUrl = await this.minioService.getSignedUrl(objectName, 300);
          return { ...file, signedUrl };
        }),
      );
    }

    return booking;
  }

  async findByReference(reference: string): Promise<Booking> {
    const booking = await this.bookingsRepository.findByReferenceWithFiles(reference);
    if (!booking) {
      throw new NotFoundException(`Booking with reference "${reference}" not found`);
    }
    return this.findById(booking.id);
  }

  /** Upload documents for a DRAFT booking. */
  async uploadDocuments(
    id: string,
    files: Express.Multer.File[],
  ): Promise<Booking> {
    const booking = await this.findById(id);
    if (booking.status !== 'DRAFT') {
      throw new BadRequestException(
        'Documents can only be uploaded for DRAFT bookings',
      );
    }
    await this.filesService.uploadMany(id, 'bookings', files);
    return this.findById(id);
  }

  async remove(id: string): Promise<void> {
    const booking = await this.findById(id);
    if (booking.status !== 'DRAFT') {
      throw new BadRequestException('Only DRAFT bookings can be deleted');
    }
    await this.bookingsRepository.softDelete(id);
  }

  async findQueue(
    queue: string,
    filter: FilterBookingDto,
    options?: { excludeBulk?: boolean },
  ): Promise<{ items: Booking[]; total: number }> {
    const statusMap: Record<string, string | string[]> = {
      intake: 'SUBMITTED',
      approval: ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE'],
      signatures: ['APPROVED_PENDING_SIGNATURE', 'PENDING_APPROVAL'],
      contract: ['APPROVED', 'CONTRACT_READY', 'SIGNED_CUSTOMER', 'FULLY_EXECUTED'],
      marketing: 'SIGNED_CUSTOMER',
      finance: 'FULLY_EXECUTED',
    };

    const status = statusMap[queue];
    if (!status) {
      throw new BadRequestException(`Unknown queue: ${queue}`);
    }

    return this.bookingsRepository.findQueue({
      status,
      page: filter.page,
      pageSize: filter.pageSize,
      excludeBulk: options?.excludeBulk ?? queue === 'approval',
      sortBy: filter.sortBy,
      sortOrder: filter.sortOrder,
    });
  }

  async requestConsolidation(id: string): Promise<{
    booking: Booking;
    partner: Booking | null;
    paired: boolean;
    message: string;
  }> {
    const booking = await this.findById(id);

    if (!booking.allowConsolidation) {
      throw new BadRequestException('Booking is not eligible for consolidation');
    }

    const needs = await this.consolidationService.needsConsolidationFromBooking(
      booking,
    );
    if (!needs) {
      throw new BadRequestException(
        'Booking already fills whole wagon(s) for all container lines; consolidation is not required',
      );
    }

    if (booking.consolidationPartnerId) {
      throw new ConflictException('Booking is already paired for consolidation');
    }

    const result = await this.tryAutoConsolidate(booking);
    const partner = result.booking.consolidationPartnerId
      ? await this.findById(result.booking.consolidationPartnerId)
      : null;

    return {
      booking: result.booking,
      partner,
      paired: partner !== null,
      message: result.messages[0] ?? '',
    };
  }

  async removeConsolidation(id: string): Promise<{ booking: Booking; partner: Booking }> {
    const booking = await this.findById(id);
    if (!booking.consolidationPartnerId) {
      throw new BadRequestException('Booking has no consolidation partner');
    }

    const partnerId = booking.consolidationPartnerId;
    await this.bookingsRepository.unpairConsolidation(id, partnerId);

    return {
      booking: await this.findById(id),
      partner: await this.findById(partnerId),
    };
  }

  async getConsolidationDetails(id: string): Promise<{
    booking: Booking;
    partner: Booking | null;
    splitBilling: { bookingShare: number; partnerShare: number } | null;
    wagonSlots: Awaited<ReturnType<ConsolidationService['slotsFromBooking']>>;
    statusMessage: string;
  }> {
    const booking = await this.findById(id);
    const wagonSlots = await this.consolidationService.slotsFromBooking(booking);

    if (!booking.consolidationPartnerId) {
      const statusMessage =
        booking.status === 'PENDING_CONSOLIDATION'
          ? this.consolidationService.describePending(booking, wagonSlots)
          : wagonSlots.length > 0
            ? 'Consolidation may be required; no partner paired yet.'
            : 'No wagon consolidation needed.';
      return {
        booking,
        partner: null,
        splitBilling: null,
        wagonSlots,
        statusMessage,
      };
    }

    const partner = await this.findById(booking.consolidationPartnerId);
    return {
      booking,
      partner,
      splitBilling: {
        bookingShare: Number(booking.totalAmount),
        partnerShare: Number(partner.totalAmount),
      },
      wagonSlots,
      statusMessage: this.consolidationService.describePaired(
        partner.reference,
        wagonSlots,
      ),
    };
  }
}
