import { Injectable, BadRequestException, NotFoundException, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PrismaService } from '../../common/prisma.service';
import { SeatsService } from '../seats/seats.service';
import { VerifaydaService } from '../verifayda/verifayda.service';
import { CurrencyService } from '../currency/currency.service';
import { PassengerAuthService } from '../auth/passenger-auth.service';
import { FareEngineService } from '../fare-engine/fare-engine.service';
import { AuditService } from '../../common/audit.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { PaymentsService } from '../payments/payments.service';
import { SmsClientService } from '../notifications/sms-client.service';
import { CreateGuestBookingDto, SavedPassengerProfileDto, IssueReservationBookingDto, ReservationBookingKind } from './guest-booking.dto';
import { Currency, PassengerCategory, IdDocumentType, PaymentMethodType, PaymentIntentStatus } from '@prisma/client';
import { resolveCheckinCutoff } from '../../common/utils/checkin-cutoff.utils';
import { computePaymentDeadline } from '../../common/utils/payment-deadline.utils';
import { randomUUID } from 'crypto';

/**
 * Throws if the given boarding stop's own configurable check-in cutoff (route/stop
 * checkinMinutesBefore, same mechanism the seat hold and search results already enforce) has
 * passed. Must be checked against the actual boarding stop, not the schedule's origin — a
 * downstream stop's cutoff is independent of how long ago the train left its origin.
 */
function assertWithinCheckinCutoff(schedule: any, stopTime: any, stationId: string | null | undefined): void {
  const { cutoffAt, checkinMinutes } = resolveCheckinCutoff(schedule, stopTime, stationId);
  if (Date.now() >= cutoffAt.getTime()) {
    throw new BadRequestException(
      `Bookings are not accepted within ${checkinMinutes} minute${checkinMinutes !== 1 ? 's' : ''} of departure`,
    );
  }
}

/**
 * Resolves the seat class for a specific, already-known seat (its coach type only ever
 * offers a fixed set of classes) given the traveler's nationality tier — mirrors
 * search.service.ts's own LOCAL/INTERNATIONAL + bed-position matching so the price a
 * reservation-issued booking charges is the exact same "already configured price setup"
 * search results would have quoted, without asking the admin to redundantly re-pick a class
 * for a seat whose class is already fixed.
 */
function resolveSeatClassForSeat(seat: any, nationality: string): { id: string; name: string } {
  const nationalityUpper = (nationality ?? '').toUpperCase();
  const resolvedNationalityType = nationalityUpper === 'ETHIOPIAN' || nationalityUpper === 'DJIBOUTIAN' ? 'LOCAL' : 'INTERNATIONAL';
  const candidates = (seat.coach?.coachType?.seatClasses ?? []).filter(
    (sc: any) => !sc.nationalityType || sc.nationalityType === resolvedNationalityType,
  );
  const matchingClass = seat.bedPosition
    ? candidates.find((sc: any) => sc.bedPosition?.toLowerCase() === seat.bedPosition)
    : candidates[0];
  if (!matchingClass) {
    throw new BadRequestException('No seat class is configured for this seat and nationality — set up seat classes for this coach type first.');
  }
  return matchingClass;
}

function generateRef(): string {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  return Array.from({ length: 6 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}

// Ethiopian mobile prefixes: Ethio Telecom (09xx) and Safaricom ET (07xx)
const ETH_MOBILE_PREFIXES = ['911', '912', '913', '914', '915', '916', '917', '921', '922', '923', '924', '930', '931', '932', '933', '934', '935', '936', '937', '938', '939', '961', '962', '963', '964'];

function generateEthiopianPhone(): string {
  const prefix = ETH_MOBILE_PREFIXES[Math.floor(Math.random() * ETH_MOBILE_PREFIXES.length)];
  const suffix = String(Math.floor(Math.random() * 1_000_000)).padStart(6, '0');
  return `+251${prefix}${suffix}`;
}

function generateGuestEmail(uniqueId: string): string {
  const domains = ['gmail.com', 'yahoo.com', 'ethionet.et', 'telecom.et'];
  const domain = domains[Math.floor(Math.random() * domains.length)];
  return `guest.edr.${uniqueId}@${domain}`;
}

function calculateAge(dateOfBirth: Date): number {
  const today = new Date();
  let age = today.getFullYear() - dateOfBirth.getFullYear();
  const monthDiff = today.getMonth() - dateOfBirth.getMonth();
  if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dateOfBirth.getDate())) age--;
  return age;
}

@Injectable()
export class GuestBookingService {
  private readonly logger = new Logger(GuestBookingService.name);

  constructor(
    private prisma: PrismaService,
    @InjectDataSource() private readonly dataSource: DataSource,
    private seatsService: SeatsService,
    private verifaydaService: VerifaydaService,
    private currencyService: CurrencyService,
    private passengerAuthService: PassengerAuthService,
    private fareEngine: FareEngineService,
    private auditService: AuditService,
    private eventEmitter: EventEmitter2,
    private paymentsService: PaymentsService,
    private smsClient: SmsClientService,
  ) { }

  /**
   * C-1 protection (guest path): reject a booking whose ETB charge basis is below the
   * server-recomputed authoritative fare. A floor (not equality) so legitimate berth surcharges —
   * which only raise the total — still pass; a 1% tolerance absorbs FX-conversion rounding. A forged
   * seatFareMinor / reviewedTotalMinor that lowers the charge (e.g. to 0) is refused with a 400 and
   * nothing is persisted.
   */
  private assertTotalNotUnderAuthoritative(resolvedTotalMinor: number, authoritativeMinor: number, context: string): void {
    const tolerance = Math.max(1, Math.round(authoritativeMinor * 0.01));
    if (resolvedTotalMinor < authoritativeMinor - tolerance) {
      this.logger.warn(`${context}: rejecting booking — resolvedTotalMinor=${resolvedTotalMinor} below authoritative fare=${authoritativeMinor}`);
      throw new BadRequestException('Booking total does not match the authoritative fare');
    }
  }

  async createGuestBooking(dto: CreateGuestBookingDto, req?: any) {
    // Enrich passengers with phone/email from SavedPassengerProfile when not supplied inline.
    // The portal calls /passengers/save-details before booking but doesn't re-send contact
    // fields in the booking payload, so we pull them from the saved profile by deviceId.
    if (dto.deviceId && dto.passengers?.length) {
      const saved = await this.prisma.savedPassengerProfile.findMany({
        where: { deviceId: dto.deviceId },
        orderBy: { createdAt: 'desc' },
        select: { passengerName: true, phone: true, email: true },
      });
      if (saved.length) {
        dto.passengers = dto.passengers.map(p => {
          if (p.phone && p.email) return p;
          const match = saved.find(s => s.passengerName?.toLowerCase() === p.passengerName?.toLowerCase());
          return { ...p, phone: p.phone || match?.phone || undefined, email: p.email || match?.email || undefined };
        });
      }
    }
    if (dto.bookingType === 'ROUND_TRIP') return this.createGuestRoundTripBooking(dto, req);
    if (dto.bookingType === 'TRANSIT') return this.createGuestTransitBooking(dto, req);
    if (dto.bookingType === 'ROUND_TRIP_TRANSIT') return this.createGuestRoundTripTransitBooking(dto, req);
    return this.createGuestOneWayBooking(dto, req);
  }

  private async createGuestOneWayBooking(dto: CreateGuestBookingDto, req?: any) {
    const authUserId: string | null = req?.user?.id ?? null;
    // Validate hold
    const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
    if (!hold || hold.expiresAt < new Date()) {
      throw new BadRequestException('Seat hold expired or not found');
    }

    // Get schedule
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: dto.scheduleId },
      include: {
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        route: { include: { stops: true } },
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId)
      ?? (schedule.stopTimes.length === 0 ? { stationId: schedule.originStationId, sequence: 0, station: schedule.originStation } : undefined);
    const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId)
      ?? (schedule.stopTimes.length === 0 ? { stationId: schedule.destinationStationId, sequence: 1, station: schedule.destinationStation } : undefined);
    if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');

    // Cut off relative to the passenger's actual boarding stop, using the same
    // configurable per-stop/route checkinMinutesBefore that already gated the seat hold
    // and the search result — not a separate, hardcoded 30 minutes off the train's origin.
    assertWithinCheckinCutoff(schedule, originStop, dto.originStationId);

    const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
    const fullRoute = `${schedule.originStation.code}-${schedule.destinationStation.code}`;

    // Process passengers with Verifayda verification
    const passengersData = [];
    let adultCount = 0, childCount = 0;

    for (const passenger of dto.passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const age = calculateAge(dateOfBirth);
      const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;
      if (category === PassengerCategory.ADULT) adultCount++; else childCount++;

      let passengerName = passenger.passengerName;
      let verifaydaVerified = false;
      let verifaydaData: Record<string, any> | undefined;
      let nationality = passenger.nationality;

      const isEthiopian = passenger.nationality === 'Ethiopian' ||
        passenger.nationality === 'ETHIOPIAN' ||
        passenger.idDocumentType === IdDocumentType.NATIONAL_ID;

      if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
        if (passenger.idDocumentNumber) {
          const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
          if (!verification.verified) {
            throw new BadRequestException(
              `Verifayda verification failed for ${passenger.passengerName}: ${verification.failureReason}`
            );
          }
          passengerName = verification.passengerData?.fullName || passengerName;
          verifaydaVerified = true;
          verifaydaData = verification.passengerData?.profileData;
        }
        nationality = 'Ethiopian';
      } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        if (!passenger.passportNumber || !passenger.passportCountry) {
          throw new BadRequestException(`Passport number and country required for ${passenger.passengerName}`);
        }
        nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
      } else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        nationality = 'Ethiopian';
      } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
        nationality = nationality || 'Other';
      }

      passengersData.push({
        ...passenger,
        passengerName,
        dateOfBirth,
        category,
        verifaydaVerified,
        verifaydaData,
        nationality,
      });
    }

    // Calculate fare — package bookings use the fixed tier price, bypassing the fare engine
    const isPackageOneway = !!dto.packageId && !!dto.priceTierId;
    let baseFareMinor: number;
    let paidChildrenCount: number;
    let childUnitFare: number;

    if (isPackageOneway) {
      const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: dto.priceTierId! } });
      baseFareMinor = tier.priceMinor;
      paidChildrenCount = childCount;
      childUnitFare = Math.round(baseFareMinor * 0.1);
    } else {
      const primaryNationality = passengersData[0]?.nationality;
      baseFareMinor = await this.getBaseFare(
        dto.scheduleId,
        dto.seatClassId,
        segmentRoute,
        fullRoute,
        primaryNationality,
        dto.originStationId,
        dto.destinationStationId,
      );
      paidChildrenCount = Math.max(0, childCount - 1);
      childUnitFare = baseFareMinor;
    }

    const adultFareMinor = baseFareMinor * adultCount;
    const childFareMinor = childUnitFare * paidChildrenCount;
    const totalBaseFareMinor = adultFareMinor + childFareMinor;

    let discountMinor = 0;
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > new Date()) {
        discountMinor = promo.percentOff
          ? Math.round(totalBaseFareMinor * promo.percentOff / 100)
          : (promo.amountOffMinor ?? 0);
      }
    }

    const taxesMinor = 0;

    // Per-seat fare: use client-supplied seatFareMinor when present (berth-specific pricing).
    const passengersWithFares = passengersData.map(p => {
      let fareMinor: number;
      if (p.category === PassengerCategory.ADULT) {
        fareMinor = p.seatFareMinor ?? baseFareMinor;
      } else {
        // Free children have no seat (frontend excludes them from the DTO).
        // Guard by seatId: unseated = free (0), seated = paid child.
        // Applies to both package and regular bookings.
        fareMinor = p.seatId ? (p.seatFareMinor ?? childUnitFare) : 0;
      }
      return { ...p, fareMinor };
    });

    // Server-computed sum from per-seat fares is the authoritative total when all
    // seated passengers supplied seatFareMinor. This prevents a frontend race
    // condition (fareBreakdown not yet loaded → only partial fares summed →
    // reviewedTotalMinor reflects one passenger's fare instead of all).
    const displayCurrency = dto.displayCurrency || Currency.ETB;
    const seatedPassengers = passengersData.filter(p => p.seatId);
    const allFaresProvided = seatedPassengers.length > 0 && seatedPassengers.every(p => p.seatFareMinor != null);

    let displayTotalMinor: number;
    let resolvedTotalMinor: number;
    if (allFaresProvided && !isPackageOneway) {
      displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0);
    } else if (isPackageOneway && dto.reviewedTotalMinor != null) {
      displayTotalMinor = dto.reviewedTotalMinor;
      if (seatedPassengers.length > 0) {
        const perSeatFare = Math.round(dto.reviewedTotalMinor / seatedPassengers.length);
        passengersWithFares.forEach(p => {
          if (p.fareMinor > 0) p.fareMinor = p.seatFareMinor ?? perSeatFare;
        });
      }
    } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
      displayTotalMinor = dto.reviewedTotalMinor;
    } else {
      // fare engine returns ETB — convert forward to display currency
      const etbTotal = Math.max(0, totalBaseFareMinor - discountMinor);
      displayTotalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(etbTotal, Currency.ETB, displayCurrency)
        : etbTotal;
    }
    resolvedTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
      : displayTotalMinor;

    // C-1 guard: never charge less than the server-recomputed authoritative ETB fare (net of promo).
    this.assertTotalNotUnderAuthoritative(resolvedTotalMinor, Math.max(0, totalBaseFareMinor - discountMinor), 'createGuestBooking');

    // Resolve or create the guest Passenger record
    const firstPassenger = passengersData[0];
    const { guestPassengerId, iamUserId, createdAccount } = await this.resolveGuestPassenger(dto, firstPassenger, req);
    const contact = await this.resolveActorContact(req, firstPassenger);

    // Save passenger details for future use (if requested)
    if (dto.savePassengerDetails && (dto.createAccount || dto.deviceId)) {
      for (const passenger of passengersData) {
        await this.prisma.savedPassengerProfile.create({
          data: {
            userId: iamUserId ?? undefined,
            deviceId: dto.deviceId,
            passengerName: passenger.passengerName,
            dateOfBirth: passenger.dateOfBirth,
            idDocumentType: passenger.idDocumentType,
            passportNumber: passenger.passportNumber,
            passportCountry: passenger.passportCountry,
            nationality: passenger.nationality,
            phone: passenger.phone,
            email: passenger.email,
          },
        });
      }
    }

    // Create booking
    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: guestPassengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.destinationStationId,
        status: 'PENDING_PAYMENT',
        totalMinor: resolvedTotalMinor,
        // Charge basis is ETB (resolvedTotalMinor). The passenger's currency and amount live in
        // displayCurrency/displayTotalMinor — keep currency coherent with totalMinor's units.
        currency: Currency.ETB,
        adultCount,
        childCount,
        displayCurrency,
        displayTotalMinor,
        bookingType: 'ONE_WAY',
        ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
        userAgent: dto.deviceId,
        contactEmail: contact.contactEmail,
        contactPhone: contact.contactPhone,
        seats: {
          create: passengersWithFares.map((p) => ({
            seat: { connect: { id: p.seatId } },
            scheduleId: dto.scheduleId,
            passengerName: p.passengerName,
            dateOfBirth: p.dateOfBirth,
            passengerCategory: p.category,
            idDocumentType: p.idDocumentType,
            passportNumber: p.passportNumber,
            passportCountry: p.passportCountry,
            verifaydaVerified: p.verifaydaVerified,
            verifaydaData: p.verifaydaData || undefined,
            fareMinor: p.fareMinor,
            displayCurrency,
          })),
        },
      },
      include: {
        seats: { include: { seat: { include: { coach: true } } } },
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
      },
    });

    // Save passenger details as traveler profiles — guest bookings only.
    // Authenticated passengers already have a profile, matching the old BookingsService.
    if (!authUserId) await this.createTravelerProfiles(guestPassengerId, passengersData);

    // Confirm seats
    await this.seatsService.confirmSeats(dto.passengers.map(p => p.seatId).filter((id): id is string => !!id));

    // Authenticated-only side effect: audit the booking creation.
    if (authUserId) {
      await this.auditService.log({ userId: guestPassengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ONE_WAY', totalMinor: resolvedTotalMinor } });
    }

    this.eventEmitter.emit('booking.created', { booking });

    return {
      ...booking,
      createdAccount,
      iamUserId,
      fareBreakdown: {
        baseFareMinor,
        adultCount,
        adultFareMinor,
        childCount,
        freeChildrenCount: isPackageOneway ? 0 : Math.min(childCount, 1),
        paidChildrenCount,
        childFareMinor,
        totalBaseFareMinor,
        discountMinor,
        taxesFeesMinor: taxesMinor,
        totalMinor: resolvedTotalMinor,
        currency: booking.displayCurrency,
        displayCurrency,
        displayTotalMinor,
      },
    };
  }

  /**
   * Converts an admin-reserved seat (SeatBlock) into a real booking for one traveler —
   * no SeatHold involved (the seat was already set aside), so this mirrors
   * createGuestOneWayBooking's schedule/fare/passenger resolution but skips the hold check
   * and instead validates+releases the SeatBlock. STAFF bookings are fee-waived and
   * finalized immediately via the same PaymentsService.finalizePaymentSuccess() path every
   * real payment webhook uses; PASSENGER bookings are left PENDING_PAYMENT with a payToken
   * texted to the traveler so they can pay via the existing, already-public /payments/*
   * endpoints without a portal session.
   */
  async issueBookingFromReservation(
    seatId: string,
    dto: IssueReservationBookingDto,
    actingUserId: string | null,
  ): Promise<{ booking: any; payUrl?: string }> {
    if (dto.bookingKind === ReservationBookingKind.PASSENGER && !dto.phone) {
      throw new BadRequestException('Phone number is required for a passenger booking');
    }
    if (dto.idDocumentType === IdDocumentType.NATIONAL_ID && dto.nationality !== 'Ethiopian') {
      throw new BadRequestException('National ID is only valid for Ethiopian nationality — use a passport instead');
    }

    const seatBlock = await this.prisma.seatBlock.findFirst({
      where: { seatId, OR: [{ scheduleId: dto.scheduleId }, { scheduleId: null }] },
    });
    if (!seatBlock) throw new NotFoundException('Seat is not reserved');

    // The seat (and therefore its coach) is already fixed by the reservation — resolve the
    // seat class from the seat's own coach type + the traveler's nationality tier, the same
    // LOCAL/INTERNATIONAL + bed-position matching search results already use, instead of
    // asking the admin to redundantly pick a class.
    const seat = await this.prisma.seat.findUnique({
      where: { id: seatId },
      include: { coach: { include: { coachType: { include: { seatClasses: true } } } } },
    });
    if (!seat) throw new NotFoundException('Seat not found');
    const resolvedSeatClass = resolveSeatClassForSeat(seat, dto.nationality);

    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: dto.scheduleId },
      include: {
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        route: { include: { stops: true } },
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId)
      ?? (schedule.stopTimes.length === 0 ? { stationId: schedule.originStationId, sequence: 0, station: schedule.originStation } : undefined);
    const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId)
      ?? (schedule.stopTimes.length === 0 ? { stationId: schedule.destinationStationId, sequence: 1, station: schedule.destinationStation } : undefined);
    if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');

    assertWithinCheckinCutoff(schedule, originStop, dto.originStationId);

    const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
    const fullRoute = `${schedule.originStation.code}-${schedule.destinationStation.code}`;

    // Single-traveler passenger processing. nationality is a fixed dropdown value (Ethiopian/
    // Djiboutian/Other), so — unlike the guest-booking loop this mirrors — there's no need to
    // infer it from document type/country; only the Verifayda check (NATIONAL_ID) and the
    // passport-number requirement (PASSPORT) still depend on the chosen document type.
    const dateOfBirth = new Date(dto.dateOfBirth);
    const age = calculateAge(dateOfBirth);
    const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;

    let passengerName = dto.passengerName;
    let verifaydaVerified = false;
    let verifaydaData: Record<string, any> | undefined;

    if (dto.idDocumentType === IdDocumentType.NATIONAL_ID) {
      if (dto.idDocumentNumber) {
        const verification = await this.verifaydaService.verifyNationalId(dto.idDocumentNumber);
        if (!verification.verified) {
          throw new BadRequestException(`Verifayda verification failed for ${dto.passengerName}: ${verification.failureReason}`);
        }
        passengerName = verification.passengerData?.fullName || passengerName;
        verifaydaVerified = true;
        verifaydaData = verification.passengerData?.profileData;
      }
    } else if (dto.idDocumentType === IdDocumentType.PASSPORT) {
      if (!dto.passportNumber) {
        throw new BadRequestException(`Passport number required for ${dto.passengerName}`);
      }
    }

    const passengerData = {
      passengerName,
      dateOfBirth,
      category,
      verifaydaVerified,
      verifaydaData,
      nationality: dto.nationality,
      idDocumentType: dto.idDocumentType,
      idDocumentNumber: dto.idDocumentNumber,
      passportNumber: dto.passportNumber,
      phone: dto.phone,
      email: dto.email,
    };

    const baseFareMinor = await this.getBaseFare(
      dto.scheduleId,
      resolvedSeatClass.id,
      segmentRoute,
      fullRoute,
      dto.nationality,
      dto.originStationId,
      dto.destinationStationId,
    );

    const isStaff = dto.bookingKind === ReservationBookingKind.STAFF;
    const displayCurrency = dto.displayCurrency || Currency.ETB;
    const totalMinor = isStaff ? 0 : baseFareMinor;
    if (!isStaff) {
      // Defense-in-depth — there's no client-forgeable price on this DTO, but keep the
      // same authoritative-fare floor every other booking path enforces.
      this.assertTotalNotUnderAuthoritative(totalMinor, baseFareMinor, 'issueBookingFromReservation');
    }
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    const { guestPassengerId } = await this.resolveGuestPassenger({}, passengerData);

    const payToken = isStaff ? undefined : randomUUID();
    const payTokenExpiresAt = isStaff ? undefined : computePaymentDeadline(new Date(), schedule.departureAt);

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: guestPassengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.destinationStationId,
        status: 'PENDING_PAYMENT',
        totalMinor,
        currency: Currency.ETB,
        adultCount: category === PassengerCategory.ADULT ? 1 : 0,
        childCount: category === PassengerCategory.CHILD ? 1 : 0,
        displayCurrency,
        displayTotalMinor,
        bookingType: 'ONE_WAY',
        source: 'BACKOFFICE_RESERVATION',
        contactEmail: dto.email || null,
        contactPhone: dto.phone || null,
        payToken,
        payTokenExpiresAt,
        seats: {
          create: [{
            seat: { connect: { id: seatId } },
            scheduleId: dto.scheduleId,
            passengerName: passengerData.passengerName,
            dateOfBirth: passengerData.dateOfBirth,
            passengerCategory: passengerData.category,
            idDocumentType: passengerData.idDocumentType,
            passportNumber: passengerData.passportNumber,
            verifaydaVerified: passengerData.verifaydaVerified,
            verifaydaData: passengerData.verifaydaData || undefined,
            fareMinor: totalMinor,
            displayCurrency,
          }],
        },
      },
      include: {
        seats: { include: { seat: { include: { coach: true } } } },
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
      },
    });

    await this.createTravelerProfiles(guestPassengerId, [passengerData]);

    // Release the reservation using the SAME scope it was created with (global vs
    // schedule-scoped) — unblockSeat already correctly resets Seat.status for a global
    // block; reimplementing that here would risk missing that reset. This alone leaves a
    // window where the seat has no SeatBlock, no SeatHold, and no JourneySegment (the
    // latter is only created on payment success — see PaymentsService.createJourneySegments)
    // — i.e. fully available to the public — the instant this returns, since confirmSeats()
    // is a no-op with no existing hold to extend. holdSeats() immediately re-reserves the
    // seat with the same createdBy segment-range metadata the search/hold-conflict checks
    // already rely on (getSeatAvailabilityMap); confirmSeats() then extends that hold to the
    // real payment deadline (same mechanism createGuestOneWayBooking uses), so the seat stays
    // unavailable to everyone else until the passenger pays or the hold/booking expires.
    await this.seatsService.unblockSeat(seatId, seatBlock.scheduleId ?? undefined);
    await this.seatsService.holdSeats({
      scheduleId: dto.scheduleId,
      originStationId: dto.originStationId,
      destinationStationId: dto.destinationStationId,
      passengers: [{ passengerId: guestPassengerId, seatId }],
    });
    await this.seatsService.confirmSeats([seatId]);
    this.eventEmitter.emit('booking.created', { booking });

    if (isStaff) {
      await this.auditService.log({
        userId: actingUserId ?? undefined,
        action: 'CREATE',
        entityType: 'Booking',
        entityId: booking.id,
        newData: { feeWaived: true, waivedBy: actingUserId, originalFareMinor: baseFareMinor },
      });

      const intent = await this.prisma.paymentIntent.create({
        data: {
          bookingId: booking.id,
          amountMinor: 0,
          currency: 'ETB',
          // WALLET is an internal-only method that never leaves this app (see
          // payments.service.ts) — safe, inert placeholder for a zero-charge waiver.
          method: PaymentMethodType.WALLET,
          status: PaymentIntentStatus.REQUIRES_ACTION,
        },
      });
      await this.paymentsService.finalizePaymentSuccess({ intentId: intent.id });
      // finalizePaymentSuccess mutates the booking (status -> CONFIRMED) in the DB —
      // re-fetch so the caller sees the actual outcome, not the pre-finalization snapshot.
      const confirmedBooking = await this.prisma.booking.findUnique({ where: { id: booking.id } });
      return { booking: confirmedBooking };
    }

    const portalUrl = process.env.PORTAL_URL ?? 'http://localhost:5174';
    const payUrl = `${portalUrl}/reserve/pay/${payToken}`;
    const amountStr = (totalMinor / 100).toFixed(2);
    try {
      await this.smsClient.sendSms({
        to: dto.phone!,
        message: `EDR: Your seat is reserved. Pay ${amountStr} ETB to confirm your ticket: ${payUrl}`,
      });
    } catch (err) {
      this.logger.warn(`Reservation payment-link SMS failed for booking ${booking.bookingRef}: ${err}`);
    }

    return { booking, payUrl };
  }

  private async createGuestRoundTripBooking(dto: CreateGuestBookingDto, req?: any) {
    const authUserId: string | null = req?.user?.id ?? null;
    if (!dto.returnScheduleId || !dto.returnHoldId || !dto.returnOriginStationId || !dto.returnDestinationStationId) {
      throw new BadRequestException('returnScheduleId, returnHoldId, returnOriginStationId and returnDestinationStationId are required for ROUND_TRIP');
    }

    // Validate both holds
    const [outboundHold, returnHold] = await Promise.all([
      this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }),
      this.prisma.seatHold.findUnique({ where: { id: dto.returnHoldId } }),
    ]);
    if (!outboundHold || outboundHold.expiresAt < new Date()) throw new BadRequestException('Outbound seat hold expired or not found');
    if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired or not found');

    // Validate passengers have returnSeatId
    for (const p of dto.passengers) {
      if (!p.returnSeatId) throw new BadRequestException(`returnSeatId is required for each passenger in a ROUND_TRIP booking (missing for ${p.passengerName})`);
    }

    // Load both schedules
    const [outboundSchedule, returnSchedule] = await Promise.all([
      this.prisma.trainSchedule.findUnique({
        where: { id: dto.scheduleId },
        include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, route: { include: { stops: true } } },
      }),
      this.prisma.trainSchedule.findUnique({
        where: { id: dto.returnScheduleId },
        include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } },
      }),
    ]);
    if (!outboundSchedule) throw new NotFoundException('Outbound schedule not found');
    if (!returnSchedule) throw new NotFoundException('Return schedule not found');

    const synth = (sched: any, stationId: string, seq: number) => {
      const station = sched.originStationId === stationId ? sched.originStation : sched.destinationStation;
      return { stationId, sequence: seq, station };
    };
    const obStops = outboundSchedule.stopTimes.length > 0 ? outboundSchedule.stopTimes : [synth(outboundSchedule, outboundSchedule.originStationId, 0), synth(outboundSchedule, outboundSchedule.destinationStationId, 1)];
    const retStops = returnSchedule.stopTimes.length > 0 ? returnSchedule.stopTimes : [synth(returnSchedule, returnSchedule.originStationId, 0), synth(returnSchedule, returnSchedule.destinationStationId, 1)];
    const outboundOriginStop = obStops.find((s: any) => s.stationId === dto.originStationId) ?? obStops[0];
    const outboundDestStop = obStops.find((s: any) => s.stationId === dto.destinationStationId) ?? obStops[obStops.length - 1];
    const returnOriginStop = retStops.find((s: any) => s.stationId === dto.returnOriginStationId) ?? retStops[0];
    const returnDestStop = retStops.find((s: any) => s.stationId === dto.returnDestinationStationId) ?? retStops[retStops.length - 1];
    if (!outboundOriginStop || !outboundDestStop) throw new NotFoundException('Outbound origin or destination not found on schedule');
    if (!returnOriginStop || !returnDestStop) throw new NotFoundException('Return origin or destination not found on schedule');

    // Cut off relative to the passenger's actual boarding stop, using the same configurable
    // per-stop/route checkinMinutesBefore that already gated the seat hold and search result.
    assertWithinCheckinCutoff(outboundSchedule, outboundOriginStop, dto.originStationId);

    const outboundSegmentRoute = `${outboundOriginStop.station.code}-${outboundDestStop.station.code}`;
    const outboundFullRoute = `${outboundSchedule.originStation.code}-${outboundSchedule.destinationStation.code}`;
    const returnSegmentRoute = `${returnOriginStop.station.code}-${returnDestStop.station.code}`;
    const returnFullRoute = `${returnSchedule.originStation.code}-${returnSchedule.destinationStation.code}`;

    // Process passengers (verify identity once — same person travels both legs)
    const passengersData: any[] = [];
    let adultCount = 0, childCount = 0;

    for (const passenger of dto.passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const age = calculateAge(dateOfBirth);
      const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;
      if (category === PassengerCategory.ADULT) adultCount++; else childCount++;

      let passengerName = passenger.passengerName;
      let verifaydaVerified = false;
      let verifaydaData: Record<string, any> | undefined;
      let nationality = passenger.nationality;

      const isEthiopian = passenger.nationality === 'Ethiopian' ||
        passenger.nationality === 'ETHIOPIAN' ||
        passenger.idDocumentType === IdDocumentType.NATIONAL_ID;

      if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID) {
        if (passenger.idDocumentNumber) {
          const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
          if (!verification.verified) throw new BadRequestException(`Verifayda verification failed for ${passenger.passengerName}: ${verification.failureReason}`);
          passengerName = verification.passengerData?.fullName || passengerName;
          verifaydaVerified = true;
          verifaydaData = verification.passengerData?.profileData;
        }
        nationality = 'Ethiopian';
      } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        if (!passenger.passportNumber || !passenger.passportCountry) throw new BadRequestException(`Passport number and country required for ${passenger.passengerName}`);
        nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
      } else if (isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        nationality = 'Ethiopian';
      } else {
        nationality = nationality || 'Other';
      }

      passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
    }

    // Calculate fares for both legs — package bookings use the fixed tier price split across legs
    const returnSeatClassId = dto.returnSeatClassId || dto.seatClassId;
    const isPackageRoundTrip = !!dto.packageId && !!dto.priceTierId;
    let outboundBaseFare: number;
    let returnBaseFare: number;
    let paidChildrenCount: number;
    let outboundChildUnitFare: number;
    let returnChildUnitFare: number;

    if (isPackageRoundTrip) {
      const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: dto.priceTierId! } });
      // tier.priceMinor is the full round-trip price per adult; split evenly across legs
      const halfMinor = Math.round(tier.priceMinor / 2);
      outboundBaseFare = halfMinor;
      returnBaseFare = tier.priceMinor - halfMinor;
      paidChildrenCount = childCount;
      outboundChildUnitFare = Math.round(outboundBaseFare * 0.1);
      returnChildUnitFare = Math.round(returnBaseFare * 0.1);
    } else {
      const primaryNationality = passengersData[0]?.nationality;
      [outboundBaseFare, returnBaseFare] = await Promise.all([
        this.getBaseFare(dto.scheduleId, dto.seatClassId, outboundSegmentRoute, outboundFullRoute, primaryNationality, dto.originStationId, dto.destinationStationId),
        this.getBaseFare(dto.returnScheduleId, returnSeatClassId, returnSegmentRoute, returnFullRoute, primaryNationality, dto.returnOriginStationId, dto.returnDestinationStationId),
      ]);
      paidChildrenCount = Math.max(0, childCount - 1);
      outboundChildUnitFare = outboundBaseFare;
      returnChildUnitFare = returnBaseFare;
    }
    const outboundTotalBase = outboundBaseFare * adultCount + outboundChildUnitFare * paidChildrenCount;
    const returnTotalBase = returnBaseFare * adultCount + returnChildUnitFare * paidChildrenCount;
    const combinedBaseFareMinor = outboundTotalBase + returnTotalBase;

    let discountMinor = 0;
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > new Date()) {
        discountMinor = promo.percentOff
          ? Math.round(combinedBaseFareMinor * promo.percentOff / 100)
          : (promo.amountOffMinor ?? 0);
      }
    }

    const taxesMinor = 0;
    let totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor);
    // C-1 guard: authoritative ETB fare for both legs, captured before the client-driven branches
    // below may overwrite totalMinor with a per-seat sum or reviewedTotalMinor.
    const authoritativeTotalMinor = totalMinor;

    const displayCurrency = dto.displayCurrency || Currency.ETB;
    let displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    // Per-seat fares: use client-supplied seatFareMinor/returnSeatFareMinor when present.
    const passengersWithFares = passengersData.map(p => {
      let outboundFareMinor: number;
      let returnFareMinor: number;
      if (p.category === PassengerCategory.ADULT) {
        outboundFareMinor = p.seatFareMinor ?? outboundBaseFare;
        returnFareMinor = p.returnSeatFareMinor ?? returnBaseFare;
      } else {
        // Free children have no seat (frontend excludes them from the DTO).
        // Guard by seatId: unseated = free (0), seated = paid child.
        // Applies to both package and regular bookings.
        outboundFareMinor = p.seatId ? (p.seatFareMinor ?? outboundChildUnitFare) : 0;
        returnFareMinor   = p.seatId ? (p.returnSeatFareMinor ?? returnChildUnitFare) : 0;
      }
      return { ...p, outboundFareMinor, returnFareMinor };
    });

    // Override totalMinor with reviewedTotalMinor when provided, or sum of per-seat fares
    // when all seated passengers supplied their fares.
    const rtSeatedPassengers = passengersData.filter(p => p.seatId);
    const allRTFaresProvided = rtSeatedPassengers.length > 0 &&
      rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null);

    if (allRTFaresProvided && !isPackageRoundTrip) {
      // Server-computed sum is authoritative — prevents race-condition under-count.
      displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0);
      totalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
        : displayTotalMinor;
    } else if (isPackageRoundTrip && dto.reviewedTotalMinor != null) {
      displayTotalMinor = dto.reviewedTotalMinor;
      totalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
        : displayTotalMinor;
      const seatedCount = passengersData.filter(p => p.seatId).length;
      if (seatedCount > 0) {
        const perSeatPerLeg = Math.round(dto.reviewedTotalMinor / (seatedCount * 2));
        passengersWithFares.forEach(p => {
          if (p.outboundFareMinor > 0) p.outboundFareMinor = p.seatFareMinor ?? perSeatPerLeg;
          if (p.returnFareMinor > 0)   p.returnFareMinor   = p.returnSeatFareMinor ?? perSeatPerLeg;
        });
      }
    } else if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor > 0) {
      displayTotalMinor = dto.reviewedTotalMinor;
      totalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
        : displayTotalMinor;
    }

    // C-1 guard: never charge less than the server-recomputed authoritative round-trip fare.
    this.assertTotalNotUnderAuthoritative(totalMinor, authoritativeTotalMinor, 'createGuestRoundTripBooking');

    // Create or resolve guest passenger (same as one-way)
    const { guestPassengerId, iamUserId, createdAccount } = await this.resolveGuestPassenger(dto, passengersData[0], req);
    const contact = await this.resolveActorContact(req, passengersData[0]);

    // Create booking with outbound seats; return seats confirmed separately
    const outboundSeatIds = dto.passengers.map(p => p.seatId).filter((id): id is string => !!id);
    const returnSeatIds = dto.passengers.map(p => p.returnSeatId!);

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: guestPassengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.destinationStationId,
        status: 'PENDING_PAYMENT',
        bookingType: 'ROUND_TRIP',
        totalMinor,
        currency: Currency.ETB, // ETB charge basis; passenger currency in displayCurrency/displayTotalMinor
        adultCount,
        childCount,
        displayCurrency,
        displayTotalMinor,
        returnScheduleId: dto.returnScheduleId,
        returnOriginStationId: dto.returnOriginStationId,
        returnDestinationStationId: dto.returnDestinationStationId,
        returnHoldId: dto.returnHoldId,
        returnSeatClassId,
        returnLegStatus: 'NEITHER_USED',
        ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
        userAgent: dto.deviceId,
        contactEmail: contact.contactEmail,
        contactPhone: contact.contactPhone,
        seats: {
          create: [
            ...passengersWithFares.map((p) => ({
              seat: { connect: { id: p.seatId } },
              leg: 1,
              scheduleId: dto.scheduleId,
              passengerName: p.passengerName,
              dateOfBirth: p.dateOfBirth,
              passengerCategory: p.category,
              idDocumentType: p.idDocumentType,
              passportNumber: p.passportNumber,
              passportCountry: p.passportCountry,
              verifaydaVerified: p.verifaydaVerified,
              verifaydaData: p.verifaydaData || undefined,
              fareMinor: p.outboundFareMinor,
              displayCurrency,
            })),
            ...passengersWithFares.map((p) => ({
              seat: { connect: { id: p.returnSeatId } },
              leg: 2,
              scheduleId: dto.returnScheduleId,
              passengerName: p.passengerName,
              dateOfBirth: p.dateOfBirth,
              passengerCategory: p.category,
              idDocumentType: p.idDocumentType,
              passportNumber: p.passportNumber,
              passportCountry: p.passportCountry,
              verifaydaVerified: p.verifaydaVerified,
              verifaydaData: p.verifaydaData || undefined,
              fareMinor: p.returnFareMinor,
              displayCurrency,
            })),
          ],
        },
      } as any,
      include: {
        seats: { include: { seat: { include: { coach: true } } } },
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
      },
    });

    // Traveler profiles: guest bookings only (authenticated passengers already have one).
    if (!authUserId) await this.createTravelerProfiles(guestPassengerId, passengersData);

    await Promise.all([
      this.seatsService.confirmSeats(outboundSeatIds),
      this.seatsService.confirmSeats(returnSeatIds),
    ]);

    // Authenticated-only side effect: audit the booking creation.
    if (authUserId) {
      await this.auditService.log({ userId: guestPassengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ROUND_TRIP', totalMinor } });
    }

    this.eventEmitter.emit('booking.created', { booking });

    return {
      ...booking,
      createdAccount,
      iamUserId,
      fareBreakdown: {
        outboundBaseFareMinor: outboundBaseFare,
        returnBaseFareMinor: returnBaseFare,
        adultCount,
        childCount,
        freeChildrenCount: isPackageRoundTrip ? 0 : Math.min(childCount, 1),
        paidChildrenCount,
        combinedBaseFareMinor,
        discountMinor,
        taxesFeesMinor: taxesMinor,
        totalMinor,
        currency: displayCurrency,
        displayCurrency,
        displayTotalMinor,
      },
    };
  }

  private async createGuestTransitBooking(dto: CreateGuestBookingDto, req?: any) {
    const authUserId: string | null = req?.user?.id ?? null;
    if (!dto.leg2ScheduleId || !dto.leg2HoldId || !dto.transitStationId || !dto.leg2DestinationStationId) {
      throw new BadRequestException('leg2ScheduleId, leg2HoldId, transitStationId and leg2DestinationStationId are required for TRANSIT bookings');
    }

    const [leg1Hold, leg2Hold] = await Promise.all([
      this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }),
      this.prisma.seatHold.findUnique({ where: { id: dto.leg2HoldId } }),
    ]);
    if (!leg1Hold || leg1Hold.expiresAt < new Date()) throw new BadRequestException('Leg-1 seat hold expired or not found');
    if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired or not found');

    for (const p of dto.passengers) {
      if (!p.leg2SeatId) throw new BadRequestException(`leg2SeatId is required for each passenger in a TRANSIT booking (missing for ${p.passengerName})`);
    }

    const [leg1Schedule, leg2Schedule] = await Promise.all([
      this.prisma.trainSchedule.findUnique({
        where: { id: dto.scheduleId },
        include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, route: { include: { stops: true } } },
      }),
      this.prisma.trainSchedule.findUnique({
        where: { id: dto.leg2ScheduleId },
        include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } },
      }),
    ]);
    if (!leg1Schedule) throw new NotFoundException('Leg-1 schedule not found');
    if (!leg2Schedule) throw new NotFoundException('Leg-2 schedule not found');

    const leg1OriginStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.originStationId);
    const leg1DestStop = leg1Schedule.stopTimes.find(s => s.stationId === dto.transitStationId);
    const leg2OriginStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.transitStationId);
    const leg2DestStop = leg2Schedule.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId);
    if (!leg1OriginStop || !leg1DestStop) throw new NotFoundException('Leg-1 origin or transit station not found on schedule');
    if (!leg2OriginStop || !leg2DestStop) throw new NotFoundException('Transit or leg-2 destination not found on leg-2 schedule');

    // Cut off relative to the passenger's actual boarding stop, using the same configurable
    // per-stop/route checkinMinutesBefore that already gated the seat hold and search result.
    assertWithinCheckinCutoff(leg1Schedule, leg1OriginStop, dto.originStationId);

    // Process passengers (verify identity once)
    const passengersData: any[] = [];
    let adultCount = 0, childCount = 0;
    for (const passenger of dto.passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const age = calculateAge(dateOfBirth);
      const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;
      if (category === PassengerCategory.ADULT) adultCount++; else childCount++;

      let passengerName = passenger.passengerName;
      let verifaydaVerified = false;
      let verifaydaData: Record<string, any> | undefined;
      let nationality = passenger.nationality;

      const isEthiopian = passenger.nationality === 'Ethiopian' || passenger.nationality === 'ETHIOPIAN' || passenger.idDocumentType === IdDocumentType.NATIONAL_ID;
      if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID && passenger.idDocumentNumber) {
        const verification = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
        if (!verification.verified) throw new BadRequestException(`Verifayda verification failed for ${passenger.passengerName}: ${verification.failureReason}`);
        passengerName = verification.passengerData?.fullName || passengerName;
        verifaydaVerified = true;
        verifaydaData = verification.passengerData?.profileData;
        nationality = 'Ethiopian';
      } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        if (!passenger.passportNumber || !passenger.passportCountry) throw new BadRequestException(`Passport details required for ${passenger.passengerName}`);
        nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
      } else {
        nationality = nationality || 'Other';
      }
      passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
    }

    const leg2SeatClassId = dto.leg2SeatClassId || dto.seatClassId;
    const primaryNationality = passengersData[0]?.nationality;
    const paidChildrenCount = Math.max(0, childCount - 1);

    const [leg1BaseFare, leg2BaseFare] = await Promise.all([
      this.getBaseFare(dto.scheduleId, dto.seatClassId,
        `${leg1OriginStop.station.code}-${leg1DestStop.station.code}`,
        `${leg1Schedule.originStation.code}-${leg1Schedule.destinationStation.code}`,
        primaryNationality, dto.originStationId, dto.transitStationId),
      this.getBaseFare(dto.leg2ScheduleId, leg2SeatClassId,
        `${leg2OriginStop.station.code}-${leg2DestStop.station.code}`,
        `${leg2Schedule.originStation.code}-${leg2Schedule.destinationStation.code}`,
        primaryNationality, dto.transitStationId, dto.leg2DestinationStationId),
    ]);

    const leg1Total = leg1BaseFare * adultCount + leg1BaseFare * paidChildrenCount;
    const leg2Total = leg2BaseFare * adultCount + leg2BaseFare * paidChildrenCount;
    const combinedBase = leg1Total + leg2Total;

    let discountMinor = 0;
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > new Date()) {
        discountMinor = promo.percentOff ? Math.round(combinedBase * promo.percentOff / 100) : (promo.amountOffMinor ?? 0);
      }
    }
    const taxesMinor = 0;
    const totalMinor = Math.max(0, combinedBase - discountMinor);

    const displayCurrency = dto.displayCurrency || Currency.ETB;
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    const { guestPassengerId, iamUserId, createdAccount } = await this.resolveGuestPassenger(dto, passengersData[0], req);
    const contact = await this.resolveActorContact(req, passengersData[0]);

    // Single booking — leg-1 seats at leg=1, leg-2 seats at leg=2
    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: guestPassengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.leg2DestinationStationId,
        status: 'PENDING_PAYMENT',
        bookingType: 'TRANSIT',
        totalMinor,
        currency: Currency.ETB, // ETB charge basis; passenger currency in displayCurrency/displayTotalMinor
        adultCount,
        childCount,
        displayCurrency,
        displayTotalMinor,
        leg2ScheduleId: dto.leg2ScheduleId,
        leg2OriginStationId: dto.transitStationId,
        leg2DestinationStationId: dto.leg2DestinationStationId,
        leg2SeatClassId: leg2SeatClassId,
        userAgent: dto.deviceId,
        contactEmail: contact.contactEmail,
        contactPhone: contact.contactPhone,
        seats: {
          create: [
            ...passengersData.map(p => ({
              seat: { connect: { id: p.seatId } },
              leg: 1,
              scheduleId: dto.scheduleId,
              passengerName: p.passengerName,
              dateOfBirth: p.dateOfBirth,
              passengerCategory: p.category,
              idDocumentType: p.idDocumentType,
              passportNumber: p.passportNumber,
              passportCountry: p.passportCountry,
              verifaydaVerified: p.verifaydaVerified,
              verifaydaData: p.verifaydaData || undefined,
              fareMinor: p.category === PassengerCategory.ADULT ? leg1BaseFare : (paidChildrenCount > 0 ? leg1BaseFare : 0),
              displayCurrency,
            })),
            ...passengersData.map(p => ({
              seat: { connect: { id: p.leg2SeatId! } },
              leg: 2,
              scheduleId: dto.leg2ScheduleId,
              passengerName: p.passengerName,
              dateOfBirth: p.dateOfBirth,
              passengerCategory: p.category,
              idDocumentType: p.idDocumentType,
              passportNumber: p.passportNumber,
              passportCountry: p.passportCountry,
              verifaydaVerified: p.verifaydaVerified,
              verifaydaData: p.verifaydaData || undefined,
              fareMinor: p.category === PassengerCategory.ADULT ? leg2BaseFare : (paidChildrenCount > 0 ? leg2BaseFare : 0),
              displayCurrency,
            })),
          ],
        },
      } as any,
      include: {
        seats: { include: { seat: { include: { coach: true } } } },
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
      },
    });

    // Traveler profiles: guest bookings only (authenticated passengers already have one).
    if (!authUserId) await this.createTravelerProfiles(guestPassengerId, passengersData);

    await Promise.all([
      this.seatsService.confirmSeats(dto.passengers.map(p => p.seatId).filter((id): id is string => !!id)),
      this.seatsService.confirmSeats(dto.passengers.map(p => p.leg2SeatId!)),
    ]);
    this.eventEmitter.emit('booking.created', { booking });

    return {
      ...booking,
      createdAccount,
      iamUserId,
      fareBreakdown: {
        leg1BaseFareMinor: leg1BaseFare,
        leg2BaseFareMinor: leg2BaseFare,
        adultCount, childCount,
        freeChildrenCount: Math.min(childCount, 1),
        paidChildrenCount,
        combinedBaseFareMinor: combinedBase,
        discountMinor, taxesFeesMinor: taxesMinor, totalMinor,
        currency: displayCurrency, displayTotalMinor,
      },
    };
  }

  private async createGuestRoundTripTransitBooking(dto: CreateGuestBookingDto, req?: any) {
    const authUserId: string | null = req?.user?.id ?? null;
    if (!dto.leg2ScheduleId || !dto.leg2HoldId || !dto.transitStationId || !dto.leg2DestinationStationId ||
      !dto.returnScheduleId || !dto.returnHoldId || !dto.returnOriginStationId || !dto.returnDestinationStationId ||
      !dto.returnLeg2ScheduleId || !dto.returnLeg2HoldId || !dto.returnTransitStationId || !dto.returnLeg2DestinationStationId) {
      throw new BadRequestException(
        'ROUND_TRIP_TRANSIT requires all 4 holds and all transit/return station fields',
      );
    }
    for (const p of dto.passengers) {
      if (!p.leg2SeatId) throw new BadRequestException(`leg2SeatId required for ${p.passengerName}`);
      if (!p.returnSeatId) throw new BadRequestException(`returnSeatId required for ${p.passengerName}`);
      if (!p.returnLeg2SeatId) throw new BadRequestException(`returnLeg2SeatId required for ${p.passengerName}`);
    }

    const now = new Date();
    const [obL1Hold, obL2Hold, retL1Hold, retL2Hold] = await Promise.all([
      this.prisma.seatHold.findUnique({ where: { id: dto.holdId } }),
      this.prisma.seatHold.findUnique({ where: { id: dto.leg2HoldId } }),
      this.prisma.seatHold.findUnique({ where: { id: dto.returnHoldId } }),
      this.prisma.seatHold.findUnique({ where: { id: dto.returnLeg2HoldId } }),
    ]);
    if (!obL1Hold || obL1Hold.expiresAt < now) throw new BadRequestException('Outbound leg-1 hold expired');
    if (!obL2Hold || obL2Hold.expiresAt < now) throw new BadRequestException('Outbound leg-2 hold expired');
    if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 hold expired');
    if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 hold expired');

    const [obL1Sched, obL2Sched, retL1Sched, retL2Sched] = await Promise.all([
      this.prisma.trainSchedule.findUnique({ where: { id: dto.scheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } }, route: { include: { stops: true } } } }),
      this.prisma.trainSchedule.findUnique({ where: { id: dto.leg2ScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }),
      this.prisma.trainSchedule.findUnique({ where: { id: dto.returnScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }),
      this.prisma.trainSchedule.findUnique({ where: { id: dto.returnLeg2ScheduleId }, include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } } }),
    ]);
    if (!obL1Sched) throw new NotFoundException('Outbound leg-1 schedule not found');
    if (!obL2Sched) throw new NotFoundException('Outbound leg-2 schedule not found');
    if (!retL1Sched) throw new NotFoundException('Return leg-1 schedule not found');
    if (!retL2Sched) throw new NotFoundException('Return leg-2 schedule not found');

    const obL1Origin = obL1Sched.stopTimes.find(s => s.stationId === dto.originStationId);
    const obL1Dest = obL1Sched.stopTimes.find(s => s.stationId === dto.transitStationId);
    const obL2Origin = obL2Sched.stopTimes.find(s => s.stationId === dto.transitStationId);
    const obL2Dest = obL2Sched.stopTimes.find(s => s.stationId === dto.leg2DestinationStationId);
    const retL1Origin = retL1Sched.stopTimes.find(s => s.stationId === dto.returnOriginStationId);
    const retL1Dest = retL1Sched.stopTimes.find(s => s.stationId === dto.returnTransitStationId);
    const retL2Origin = retL2Sched.stopTimes.find(s => s.stationId === dto.returnTransitStationId);
    const retL2Dest = retL2Sched.stopTimes.find(s => s.stationId === dto.returnLeg2DestinationStationId);
    if (!obL1Origin || !obL1Dest) throw new NotFoundException('Outbound leg-1: origin or transit stop not found');
    if (!obL2Origin || !obL2Dest) throw new NotFoundException('Outbound leg-2: transit or destination stop not found');
    if (!retL1Origin || !retL1Dest) throw new NotFoundException('Return leg-1: origin or transit stop not found');
    if (!retL2Origin || !retL2Dest) throw new NotFoundException('Return leg-2: transit or destination stop not found');

    // Cut off relative to the passenger's actual boarding stop, using the same configurable
    // per-stop/route checkinMinutesBefore that already gated the seat hold and search result.
    assertWithinCheckinCutoff(obL1Sched, obL1Origin, dto.originStationId);

    // Process passengers (verify once)
    const passengersData: any[] = [];
    let adultCount = 0, childCount = 0;
    for (const passenger of dto.passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const category: PassengerCategory = calculateAge(dateOfBirth) < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;
      if (category === PassengerCategory.ADULT) adultCount++; else childCount++;
      let passengerName = passenger.passengerName;
      let verifaydaVerified = false;
      let verifaydaData: Record<string, any> | undefined;
      let nationality = passenger.nationality;
      const isEthiopian = nationality === 'Ethiopian' || nationality === 'ETHIOPIAN' || passenger.idDocumentType === IdDocumentType.NATIONAL_ID;
      if (isEthiopian && passenger.idDocumentType === IdDocumentType.NATIONAL_ID && passenger.idDocumentNumber) {
        const v = await this.verifaydaService.verifyNationalId(passenger.idDocumentNumber);
        if (!v.verified) throw new BadRequestException(`Verifayda failed for ${passenger.passengerName}: ${v.failureReason}`);
        passengerName = v.passengerData?.fullName || passengerName;
        verifaydaVerified = true;
        verifaydaData = v.passengerData?.profileData;
        nationality = 'Ethiopian';
      } else if (!isEthiopian && passenger.idDocumentType === IdDocumentType.PASSPORT) {
        if (!passenger.passportNumber || !passenger.passportCountry) throw new BadRequestException(`Passport details required for ${passenger.passengerName}`);
        nationality = nationality || (passenger.passportCountry === 'Djibouti' ? 'Djiboutian' : 'Other');
      } else {
        nationality = nationality || 'Other';
      }
      passengersData.push({ ...passenger, passengerName, dateOfBirth, category, verifaydaVerified, verifaydaData, nationality });
    }

    const nat = passengersData[0]?.nationality;
    const paidChildren = Math.max(0, childCount - 1);
    const obL2ClassId = dto.leg2SeatClassId ?? dto.seatClassId;
    const retL1ClassId = dto.returnSeatClassId ?? dto.seatClassId;
    const retL2ClassId = dto.returnLeg2SeatClassId ?? dto.seatClassId;

    const [obL1Fare, obL2Fare, retL1Fare, retL2Fare] = await Promise.all([
      this.getBaseFare(dto.scheduleId, dto.seatClassId, `${obL1Origin.station.code}-${obL1Dest.station.code}`, `${obL1Sched.originStation.code}-${obL1Sched.destinationStation.code}`, nat, dto.originStationId, dto.transitStationId),
      this.getBaseFare(dto.leg2ScheduleId!, obL2ClassId, `${obL2Origin.station.code}-${obL2Dest.station.code}`, `${obL2Sched.originStation.code}-${obL2Sched.destinationStation.code}`, nat, dto.transitStationId, dto.leg2DestinationStationId),
      this.getBaseFare(dto.returnScheduleId!, retL1ClassId, `${retL1Origin.station.code}-${retL1Dest.station.code}`, `${retL1Sched.originStation.code}-${retL1Sched.destinationStation.code}`, nat, dto.returnOriginStationId, dto.returnTransitStationId),
      this.getBaseFare(dto.returnLeg2ScheduleId!, retL2ClassId, `${retL2Origin.station.code}-${retL2Dest.station.code}`, `${retL2Sched.originStation.code}-${retL2Sched.destinationStation.code}`, nat, dto.returnTransitStationId, dto.returnLeg2DestinationStationId),
    ]);

    const combinedBase = (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * adultCount +
      (obL1Fare + obL2Fare + retL1Fare + retL2Fare) * paidChildren;
    let discountMinor = 0;
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > new Date()) {
        discountMinor = promo.percentOff ? Math.round(combinedBase * promo.percentOff / 100) : (promo.amountOffMinor ?? 0);
      }
    }
    const taxesMinor = Math.round(combinedBase * 0.05);
    const totalMinor = Math.max(0, combinedBase - discountMinor + taxesMinor);
    const displayCurrency = dto.displayCurrency || Currency.ETB;
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    const { guestPassengerId, iamUserId, createdAccount } = await this.resolveGuestPassenger(dto, passengersData[0], req);
    const contact = await this.resolveActorContact(req, passengersData[0]);

    const makeSeat = (p: any, seatId: string, leg: number, scheduleId: string, fare: number) => ({
      seat: { connect: { id: seatId } },
      leg,
      scheduleId,
      passengerName: p.passengerName,
      dateOfBirth: p.dateOfBirth,
      passengerCategory: p.category,
      idDocumentType: p.idDocumentType,
      passportNumber: p.passportNumber,
      passportCountry: p.passportCountry,
      verifaydaVerified: p.verifaydaVerified,
      verifaydaData: p.verifaydaData || undefined,
      fareMinor: p.category === PassengerCategory.ADULT ? fare : (paidChildren > 0 ? fare : 0),
      displayCurrency,
    });

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: guestPassengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.returnLeg2DestinationStationId,
        status: 'PENDING_PAYMENT',
        bookingType: 'ROUND_TRIP_TRANSIT',
        totalMinor, currency: Currency.ETB, adultCount, childCount, displayCurrency, displayTotalMinor, // ETB charge basis; passenger currency in displayCurrency/displayTotalMinor
        leg2ScheduleId: dto.leg2ScheduleId,
        leg2OriginStationId: dto.transitStationId,
        leg2DestinationStationId: dto.leg2DestinationStationId,
        leg2SeatClassId: obL2ClassId,
        returnScheduleId: dto.returnScheduleId,
        returnOriginStationId: dto.returnOriginStationId,
        returnDestinationStationId: dto.returnDestinationStationId,
        returnSeatClassId: retL1ClassId,
        returnLeg2ScheduleId: dto.returnLeg2ScheduleId,
        returnLeg2OriginStationId: dto.returnTransitStationId,
        returnLeg2DestStationId: dto.returnLeg2DestinationStationId,
        returnLeg2SeatClassId: retL2ClassId,
        returnLegStatus: 'NEITHER_USED',
        userAgent: dto.deviceId,
        contactEmail: contact.contactEmail,
        contactPhone: contact.contactPhone,
        seats: {
          create: [
            ...passengersData.map(p => makeSeat(p, p.seatId, 1, dto.scheduleId, obL1Fare)),
            ...passengersData.map(p => makeSeat(p, p.leg2SeatId!, 2, dto.leg2ScheduleId!, obL2Fare)),
            ...passengersData.map(p => makeSeat(p, p.returnSeatId!, 3, dto.returnScheduleId!, retL1Fare)),
            ...passengersData.map(p => makeSeat(p, p.returnLeg2SeatId!, 4, dto.returnLeg2ScheduleId!, retL2Fare)),
          ],
        },
      } as any,
      include: {
        seats: { include: { seat: { include: { coach: true } } } },
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
      },
    });

    // Traveler profiles: guest bookings only (authenticated passengers already have one).
    if (!authUserId) await this.createTravelerProfiles(guestPassengerId, passengersData);

    await Promise.all([
      this.seatsService.confirmSeats(dto.passengers.map(p => p.seatId).filter((id): id is string => !!id)),
      this.seatsService.confirmSeats(dto.passengers.map(p => p.leg2SeatId!)),
      this.seatsService.confirmSeats(dto.passengers.map(p => p.returnSeatId!)),
      this.seatsService.confirmSeats(dto.passengers.map(p => p.returnLeg2SeatId!)),
    ]);
    this.eventEmitter.emit('booking.created', { booking });

    return {
      ...booking,
      createdAccount,
      iamUserId,
      fareBreakdown: {
        outboundLeg1FareMinor: obL1Fare,
        outboundLeg2FareMinor: obL2Fare,
        returnLeg1FareMinor: retL1Fare,
        returnLeg2FareMinor: retL2Fare,
        adultCount, childCount,
        freeChildrenCount: Math.min(childCount, 1),
        paidChildrenCount: paidChildren,
        combinedBaseFareMinor: combinedBase,
        discountMinor, taxesFeesMinor: taxesMinor, totalMinor,
        currency: displayCurrency, displayCurrency, displayTotalMinor,
      },
    };
  }

  /**
   * Resolves the booking contact. Authenticated callers get their contact from iam.users
   * (matching the old BookingsService.resolveIamContact); guests fall back to the first
   * passenger's inline phone/email exactly as before.
   */
  private async resolveActorContact(
    req: any,
    firstPassenger: any,
  ): Promise<{ contactEmail: string | null; contactPhone: string | null }> {
    const iamUserId = req?.user?.id;
    if (iamUserId) {
      const rows = await this.dataSource.query<{ email: string; phone_number: string | null }[]>(
        `SELECT email, phone_number FROM iam.users WHERE id = $1 LIMIT 1`,
        [iamUserId],
      );
      return { contactEmail: rows[0]?.email ?? null, contactPhone: rows[0]?.phone_number ?? null };
    }
    return { contactEmail: firstPassenger?.email || null, contactPhone: firstPassenger?.phone || null };
  }

  private async resolveGuestPassenger(
    dto: Pick<CreateGuestBookingDto, 'createAccount' | 'password' | 'deviceId'>,
    firstPassenger: any,
    req?: any,
  ): Promise<{ guestPassengerId: string; iamUserId: string | null; createdAccount: boolean }> {
    // Authenticated caller: resolve the existing passenger from the JWT subject.
    // Never trust a client-supplied passengerId — identity comes from the token only.
    const authUserId = req?.user?.id;
    if (authUserId) {
      const passenger = await this.prisma.passenger.findUnique({ where: { iamUserId: authUserId }, select: { id: true } });
      if (!passenger) throw new NotFoundException('Passenger profile not found for this account');
      return { guestPassengerId: passenger.id, iamUserId: authUserId, createdAccount: false };
    }

    if (dto.createAccount && firstPassenger.email && dto.password) {
      const guestName = firstPassenger.passengerName ?? 'Guest';
      const result = await this.passengerAuthService.registerWithPassword(
        {
          email: firstPassenger.email,
          username: firstPassenger.email,
          phoneNumber: firstPassenger.phone || `+251900000000`,
          name: { en: guestName, am: guestName },
          password: dto.password,
        },
        req,
      );
      return { guestPassengerId: result.passengerId, iamUserId: result.iamUserId, createdAccount: true };
    }

    // Create guest passenger with basic profile
    const guestPassenger = await this.prisma.passenger.create({ data: {} });
    await this.prisma.loyaltyAccount.create({ data: { passengerId: guestPassenger.id, pointsBalance: 0, tier: 'BRONZE' } });
    await this.prisma.walletAccount.create({ data: { passengerId: guestPassenger.id, balanceMinor: 0 } });

    return { guestPassengerId: guestPassenger.id, iamUserId: null, createdAccount: false };
  }

  private async createTravelerProfiles(passengerId: string, passengersData: any[]): Promise<void> {
    for (const passenger of passengersData) {
      let gender: string | null = null;
      if (passenger.verifaydaData && typeof passenger.verifaydaData === 'object') {
        gender = passenger.verifaydaData.gender || passenger.verifaydaData.Gender || null;
      }

      await this.prisma.travelerProfile.create({
        data: {
          passengerId,
          fullName: passenger.passengerName,
          gender,
          dateOfBirth: passenger.dateOfBirth,
          nationalId: passenger.idDocumentType === IdDocumentType.NATIONAL_ID ? passenger.idDocumentNumber : null,
          relationship: 'self',
          notes: JSON.stringify({
            idDocumentType: passenger.idDocumentType,
            idDocumentNumber: passenger.idDocumentNumber,
            passportNumber: passenger.passportNumber,
            passportCountry: passenger.passportCountry,
            nationality: passenger.nationality,
            phone: passenger.phone,
            email: passenger.email,
            verifaydaVerified: passenger.verifaydaVerified,
          }),
        },
      });
    }
  }

  async getSavedPassengers(userId?: string, deviceId?: string): Promise<SavedPassengerProfileDto[]> {
    if (!userId && !deviceId) {
      throw new BadRequestException('Either userId or deviceId is required');
    }

    const profiles = await this.prisma.savedPassengerProfile.findMany({
      where: {
        OR: [
          userId ? { userId } : {},
          deviceId ? { deviceId } : {},
        ],
      },
      orderBy: { createdAt: 'desc' },
    });

    return profiles.map((p: any) => ({
      passengerName: p.passengerName,
      dateOfBirth: p.dateOfBirth.toISOString().split('T')[0],
      idDocumentType: p.idDocumentType,
      idDocumentNumber: undefined,
      passportNumber: p.passportNumber || undefined,
      passportCountry: p.passportCountry || undefined,
      nationality: p.nationality || undefined,
      phone: p.phone || undefined,
      email: p.email || undefined,
    }));
  }

  private async getBaseFare(
    scheduleId: string,
    seatClassId: string,
    segmentRoute?: string,
    fullRoute?: string,
    nationality?: string,
    originStationId?: string,
    destinationStationId?: string,
  ): Promise<number> {
    const now = new Date();

    // 1. FareRule table — explicit override rules (same priority logic as the fare engine)
    const [candidates, seatClass] = await Promise.all([
      this.prisma.fareRule.findMany({
        where: {
          seatClassId,
          validFrom: { lte: now },
          OR: [{ validUntil: null }, { validUntil: { gte: now } }],
        },
      }),
      this.prisma.seatClass.findUnique({
        where: { id: seatClassId },
        select: { premiumMinor: true, insuranceFeeMinor: true },
      }),
    ]);

    const premiumMinor = seatClass?.premiumMinor ?? 0;
    const insuranceMinor = seatClass?.insuranceFeeMinor ?? 0;

    const priorities = [
      { tripId: scheduleId, route: segmentRoute, nationality },
      { tripId: scheduleId, route: segmentRoute, nationality: null },
      { tripId: scheduleId, route: fullRoute, nationality },
      { tripId: scheduleId, route: fullRoute, nationality: null },
      { tripId: scheduleId, route: null, nationality },
      { tripId: scheduleId, route: null, nationality: null },
      { tripId: null, route: segmentRoute, nationality },
      { tripId: null, route: segmentRoute, nationality: null },
      { tripId: null, route: fullRoute, nationality },
      { tripId: null, route: fullRoute, nationality: null },
      { tripId: null, route: null, nationality },
      { tripId: null, route: null, nationality: null },
    ];

    for (const priority of priorities) {
      const match = candidates.find(
        (c) => c.tripId === priority.tripId && c.route === priority.route && c.nationality === priority.nationality,
      );
      // Return base fare + seat-class surcharges so the booking total matches the quoted fare
      if (match) return match.baseFareMinor + premiumMinor + insuranceMinor;
    }

    // 2. FareEngine — distance × rate-per-km from the booking's actual segment stations
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      select: { routeId: true, originStationId: true, destinationStationId: true },
    });

    if (schedule?.routeId) {
      try {
        const fare = await this.fareEngine.calculate({
          routeId: schedule.routeId,
          // Use the booking's boarding/alighting stations so the distance reflects the
          // passenger's actual segment, not the full schedule route.
          originStationId: originStationId ?? schedule.originStationId,
          destinationStationId: destinationStationId ?? schedule.destinationStationId,
          seatClassId,
          nationality,
        });
        // farePerPassengerMinor already includes base + premiumMinor + insuranceFeeMinor
        return fare.farePerPassengerMinor;
      } catch {
        // FareEngine throws if distanceKm is missing; fall through to error
      }
    }

    throw new BadRequestException(
      `No fare configured for this schedule and seat class. Please set up fare rules or route distances.`,
    );
  }
}
