import {
  BadRequestException,
  ConflictException,
  ForbiddenException,
  Injectable,
  NotFoundException,
} from '@nestjs/common';
import type { Freight } from '@edr/types';

import { BookingRequestRepository } from './booking-request.repository';
import { ContractsService } from './contracts.service';
import { ContractBookingService } from './contract-booking.service';
import { ContractNotifierService } from './contract-notifier.service';
import { BookingRequest } from './entities/booking-request.entity';
import { Contract } from './entities/contract.entity';
import { CreateBookingRequestDto } from './dto/create-booking-request.dto';

/**
 * Customer shipment requests on GENERAL customs (Path B) contracts. The customer
 * submits a request (date + quantities); GL reviews the queue and, on accept,
 * creates the booking — after which per-booking clearance begins. ONE_TIME and
 * Path A do not use this flow.
 */
@Injectable()
export class BookingRequestService {
  constructor(
    private readonly repo: BookingRequestRepository,
    private readonly contractsService: ContractsService,
    private readonly contractBookingService: ContractBookingService,
    private readonly notifier: ContractNotifierService,
  ) {}

  /**
   * Only GENERAL contracts that bundle customs use the request → GL → clearance
   * flow. A ONE_TIME customs contract runs its clearance at the contract level
   * and GL books it directly, with no customer-facing request step.
   */
  private assertGeneralCustoms(contract: Contract): void {
    if (
      contract.contractKind !== 'GENERAL' ||
      !contract.customsClearingEnabled
    ) {
      throw new BadRequestException(
        'Shipment requests apply only to general customs-clearance contracts.',
      );
    }
  }

  /** Customer submits a shipment request. */
  async submit(
    contractId: string,
    dto: CreateBookingRequestDto,
    userId?: string,
  ): Promise<BookingRequest> {
    const contract = await this.contractsService.findById(contractId);
    await this.contractsService.assertCustomerCanAccessContract(userId, contract);
    this.assertGeneralCustoms(contract);
    if (contract.status === 'CONTRACT_CLOSED') {
      throw new ConflictException(
        'This contract is completed — the full contracted quantity has been booked.',
      );
    }
    if (contract.status !== 'CONTRACT_ACTIVE') {
      throw new ConflictException(
        'The contract must be active before requesting a shipment.',
      );
    }

    const isContainer = contract.freightType === 'CONTAINER';
    const hasLines = isContainer
      ? (dto.containers?.length ?? 0) > 0
      : Boolean(dto.bulk);
    if (!hasLines) {
      throw new BadRequestException(
        isContainer
          ? 'Add at least one container line.'
          : 'Enter the bulk cargo amount.',
      );
    }

    // Validate requested container sizes against the contract cargo scope and
    // remaining draw-down capacity (reuses the booking quantity-cap check).
    if (isContainer) {
      const allowed = new Set(
        (contract.cargoScope ?? [])
          .map((s) => s.containerSize)
          .filter((s): s is string => !!s),
      );
      for (const line of dto.containers ?? []) {
        if (allowed.size && !allowed.has(line.containerSize)) {
          throw new BadRequestException(
            `Container size ${line.containerSize} is not in this contract's scope.`,
          );
        }
      }
    }
    await this.contractBookingService.assertRequestWithinCapacity(contract, {
      containers: dto.containers,
      bulk: dto.bulk,
    });

    const requestedLines: Freight.RequestedShipmentLines = isContainer
      ? {
          containers: (dto.containers ?? []).map((l) => ({
            containerSize: l.containerSize,
            quantity: l.quantity,
            hazardousQuantity: l.hazardousQuantity,
            reeferQuantity: l.reeferQuantity,
          })),
        }
      : {
          bulk: {
            cargoTypeId: dto.bulk?.cargoTypeId ?? null,
            cargoWeightTons: dto.bulk?.cargoWeightTons,
            itemCount: dto.bulk?.itemCount,
            hazardousQuantity: dto.bulk?.hazardousQuantity,
          },
        };

    // Clearance-first flow: the request immediately initiates a BARE booking
    // instance (no cargo, no date, no price) that enters per-booking phased
    // customs clearance. GL no longer screens the request up front — it
    // reviews the documents in the clearance queue and completes the booking
    // (container numbers, VGM, shipment day) once clearance is ready. The
    // instance is created first so a failure leaves no half-linked request.
    const booking = await this.contractBookingService.initiateForShipmentRequest(
      contract,
      {
        contractRouteId: dto.contractRouteId,
        userId,
        paymentCurrency: dto.paymentCurrency,
      },
    );

    const reference = await this.generateReference();
    const request = await this.repo.create({
      reference,
      contractId,
      requestedByUserId: userId ?? null,
      contractRouteId: dto.contractRouteId ?? null,
      scheduledDate: dto.scheduledDate ? new Date(dto.scheduledDate) : null,
      status: 'ACCEPTED',
      createdBookingId: booking.id,
      requestedLines,
      // Intercity is invoiced in birr whatever the customer picked.
      paymentCurrency:
        contract.tradeDirection === 'DOMESTIC'
          ? 'ETB'
          : (dto.paymentCurrency ?? contract.paymentCurrency ?? 'USD'),
      notes: dto.notes ?? null,
    } as never);
    this.notifier.shipmentRequestedToStaff(contract, request.id, request.reference);
    return request;
  }

  listForContract(contractId: string): Promise<BookingRequest[]> {
    return this.repo.findForContract(contractId);
  }

  async findOne(requestId: string): Promise<BookingRequest> {
    const request = await this.repo.findById(requestId);
    if (!request) throw new NotFoundException(`Booking request ${requestId} not found`);
    return request;
  }

  queue(): Promise<BookingRequest[]> {
    return this.repo.findQueue();
  }

  private async findPending(requestId: string): Promise<BookingRequest> {
    const request = await this.repo.findById(requestId);
    if (!request) throw new NotFoundException(`Booking request ${requestId} not found`);
    if (request.status !== 'PENDING') {
      throw new ConflictException(
        `This request is already ${request.status.toLowerCase()}.`,
      );
    }
    return request;
  }

  /**
   * GL accepts a request. The booking itself is created via the GL booking form
   * (POST /contracts/:id/bookings) which carries the per-unit container data the
   * request omits; this endpoint records the acceptance + links the created
   * booking. `bookingId` is supplied by the GL form on success.
   */
  async accept(
    requestId: string,
    bookingId: string,
    staffId?: string,
  ): Promise<BookingRequest> {
    const request = await this.findPending(requestId);
    await this.repo.update(requestId, {
      status: 'ACCEPTED',
      createdBookingId: bookingId,
      reviewedByStaffId: staffId ?? null,
      reviewedAt: new Date(),
    } as never);
    return (await this.repo.findById(requestId)) ?? request;
  }

  /** GL rejects a request with a note. */
  async reject(
    requestId: string,
    note?: string,
    staffId?: string,
  ): Promise<BookingRequest> {
    const request = await this.findPending(requestId);
    await this.repo.update(requestId, {
      status: 'REJECTED',
      reviewNote: note ?? null,
      reviewedByStaffId: staffId ?? null,
      reviewedAt: new Date(),
    } as never);
    const contract = await this.contractsService.findById(request.contractId);
    this.notifier.shipmentRequestRejected(contract, request.reference, note);
    return (await this.repo.findById(requestId)) ?? request;
  }

  /** Customer cancels their own pending request. */
  async cancel(requestId: string, userId?: string): Promise<BookingRequest> {
    const request = await this.findPending(requestId);
    if (request.requestedByUserId && request.requestedByUserId !== userId) {
      throw new ForbiddenException('You can only cancel your own requests.');
    }
    await this.repo.update(requestId, { status: 'CANCELLED' } as never);
    return (await this.repo.findById(requestId)) ?? request;
  }

  private async generateReference(): Promise<string> {
    const seq = await this.repo.maxReferenceSequence();
    return `SR-${String(seq + 1).padStart(6, '0')}`;
  }
}
