import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';

import { assertCanApproveBookingStep } from '../../common/freight-permission.util';
import { RuleEngineService } from '../rule-engine/rule-engine.service';
import { BookingContractService } from './booking-contract.service';
import { BookingPricingService } from './booking-pricing.service';
import { BookingsRepository } from './bookings.repository';
import { assertBookingStatus } from './booking-status.util';
import { computeNextStep, type BookingNextStep } from './booking-next-step.util';
import { Booking } from './entities/booking.entity';
import { BookingsService } from './bookings.service';

@Injectable()
export class BookingTransitionService {
  constructor(
    private readonly bookingsRepository: BookingsRepository,
    private readonly ruleEngineService: RuleEngineService,
    private readonly pricingService: BookingPricingService,
    private readonly contractService: BookingContractService,
    @Inject(forwardRef(() => BookingsService))
    private readonly bookingsService: BookingsService,
  ) {}

  async submit(bookingId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['DRAFT', 'CHANGES_REQUESTED']);

    if (Number(booking.totalAmount) <= 0) {
      throw new BadRequestException(
        'Generate a price before submitting (POST /bookings/:id/generate-price)',
      );
    }

    const priorityScore = await this.pricingService.computeSubmitPriorityScore(booking);
    await this.ruleEngineService.snapshotLiveRates(bookingId);

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'SUBMITTED',
      priorityScore,
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async requestChanges(
    bookingId: string,
    note: string,
    actorId: string,
  ): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['SUBMITTED']);

    await this.bookingsRepository.createReviewNote(
      bookingId,
      note,
      'CHANGES_REQUESTED',
      actorId,
    );

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'CHANGES_REQUESTED',
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  /** Auto-create booking approval steps from system rules when none exist yet. */
  private async ensureBookingApprovalSteps(booking: Booking): Promise<void> {
    if ((booking.approvalSteps?.length ?? 0) > 0) return;

    await this.ruleEngineService.instantiateApprovalSteps(booking.id, {
      freightType: booking.freightType as 'CONTAINER' | 'BULK',
      cargoTypeId: booking.cargoTypeId,
    });
  }

  async acceptIntake(bookingId: string, actorId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['SUBMITTED']);

    await this.ruleEngineService.instantiateApprovalSteps(bookingId, {
      freightType: booking.freightType as 'CONTAINER' | 'BULK',
      cargoTypeId: booking.cargoTypeId,
    });

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'PENDING_APPROVAL',
      approvedByStaffId: actorId,
      approvedByStaffAt: new Date(),
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async staffReject(
    bookingId: string,
    reason: string,
    actorId: string,
  ): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['SUBMITTED', 'PENDING_APPROVAL']);

    await this.bookingsRepository.createReviewNote(
      bookingId,
      reason,
      'REJECTION',
      actorId,
    );

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'REJECTED',
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async approveStep(
    bookingId: string,
    stepId: string,
    actorId: string,
    requiredRole: string,
    authUser?: TCurrentUser,
  ): Promise<Booking> {
    if (authUser) {
      assertCanApproveBookingStep(authUser, requiredRole);
    }

    let booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, [
      'PENDING_APPROVAL',
      'APPROVED_PENDING_SIGNATURE',
    ]);

    if ((booking.approvalSteps?.length ?? 0) === 0) {
      await this.ensureBookingApprovalSteps(booking);
      booking = await this.bookingsService.findById(bookingId);
    }

    const step = await this.bookingsRepository.findApprovalStepById(
      bookingId,
      stepId,
    );
    if (!step || step.status !== 'PENDING') {
      throw new BadRequestException('Approval step not found or already actioned');
    }

    const next = await this.bookingsRepository.findNextPendingApprovalStep(bookingId);
    if (!next || next.id !== step.id) {
      throw new BadRequestException(
        'Approval steps must be completed in order',
      );
    }

    if (step.requiredRole !== requiredRole) {
      throw new BadRequestException(
        `Step requires role ${step.requiredRole}, not ${requiredRole}`,
      );
    }

    const blocksRole = step.blocksRole;
    if (blocksRole && blocksRole === requiredRole) {
      throw new BadRequestException(`Role ${requiredRole} is blocked for this step`);
    }

    await this.bookingsRepository.completeApprovalStep(step.id, actorId, 'APPROVED');

    const updates: Record<string, unknown> = {};
    const now = new Date();

    if (requiredRole === 'LINE_STAFF') {
      updates.status = 'APPROVED_PENDING_SIGNATURE';
      updates.approvedByStaffId = actorId;
      updates.approvedByStaffAt = now;
    } else if (requiredRole === 'DIRECTOR') {
      updates.signedByDirectorId = actorId;
      updates.signedByDirectorAt = now;
    } else if (requiredRole === 'CEO') {
      updates.signedByCeoId = actorId;
      updates.signedByCeoAt = now;
    }

    const allDone = await this.bookingsRepository.allApprovalStepsComplete(bookingId);
    if (allDone) {
      updates.status = 'APPROVED';
    }

    if (Object.keys(updates).length > 0) {
      await this.bookingsRepository.update(bookingId, updates as never);
    }

    if (allDone) {
      const generated = await this.contractService.generateContract(bookingId);
      return this.bookingsService.findById(generated.id);
    }

    return this.bookingsService.findById(bookingId);
  }

  async rejectStep(
    bookingId: string,
    stepId: string,
    actorId: string,
    reason: string,
  ): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE']);

    const step = await this.bookingsRepository.findApprovalStepById(
      bookingId,
      stepId,
    );
    if (!step) throw new BadRequestException('Approval step not found');

    await this.bookingsRepository.completeApprovalStep(
      step.id,
      actorId,
      'REJECTED',
      reason,
    );

    await this.bookingsRepository.createReviewNote(
      bookingId,
      reason,
      'REJECTION',
      actorId,
    );

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'REJECTED',
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async customerSign(bookingId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['CONTRACT_READY']);

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'SIGNED_CUSTOMER',
      customerSignedAt: new Date(),
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async marketingApprove(bookingId: string, actorId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['SIGNED_CUSTOMER']);

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'FULLY_EXECUTED',
      fullyExecutedAt: new Date(),
      marketingApprovedById: actorId,
      marketingApprovedAt: new Date(),
      lockedAt: new Date(),
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async startTransit(bookingId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['PAID']);

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'IN_TRANSIT',
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async complete(bookingId: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, ['IN_TRANSIT']);

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'COMPLETED',
      endDate: new Date(),
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async cancel(bookingId: string, reason: string): Promise<Booking> {
    const booking = await this.bookingsService.findById(bookingId);
    assertBookingStatus(booking, [
      'DRAFT',
      'SUBMITTED',
      'CHANGES_REQUESTED',
      'PENDING_APPROVAL',
      'CONTRACT_READY',
    ]);

    await this.bookingsRepository.createReviewNote(
      bookingId,
      reason,
      'REJECTION',
    );

    const updated = await this.bookingsRepository.update(bookingId, {
      status: 'CANCELLED',
    } as never);
    return this.bookingsService.findById(updated!.id);
  }

  async enrichBookingResponse(booking: Booking): Promise<Booking & {
    latestChangeRequestNote?: string | null;
    contractSummary?: string | null;
    nextStep: BookingNextStep | null;
  }> {
    const note = await this.bookingsRepository.findLatestReviewNote(
      booking.id,
      'CHANGES_REQUESTED',
    );
    const summary =
      booking.contractSummary ??
      this.contractService.buildContractSummary(booking);
    const nextPending =
      booking.status === 'PENDING_APPROVAL' ||
      booking.status === 'APPROVED_PENDING_SIGNATURE'
        ? await this.bookingsRepository.findNextPendingApprovalStep(booking.id)
        : null;
    const nextStep = computeNextStep(booking, nextPending);
    return {
      ...booking,
      latestChangeRequestNote: note?.note ?? null,
      contractSummary: summary,
      nextStep,
    };
  }
}
