import { Injectable, NotFoundException, BadRequestException, 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 { TicketsService } from '../tickets/tickets.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { CreateBookingDto, ModifyBookingDto } from './bookings.dto';
import { Cron, CronExpression } from '@nestjs/schedule';
import { VerifaydaService } from '../verifayda/verifayda.service';
import { CurrencyService } from '../currency/currency.service';
import { FareEngineService } from '../fare-engine/fare-engine.service';
import { Currency, PassengerCategory, IdDocumentType } from '@prisma/client';
import { resolveCurrencyFromNationality } from '../fare-engine/fare-engine.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { AuditService } from '../../common/audit.service';
import { resolveBookingSegment } from '../../common/utils/segment-resolver.utils';

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


/**
 * For package round-trip bookings, totalMinor in the DB may have been stored as a
 * single-leg amount before the server fix. Recompute from the tier price when needed.
 * tierPriceMinor is the per-leg per-adult price from PackagePackagePriceTier.
 */
function resolvePackageRoundTripTotal(
  booking: { totalMinor: number; bookingType: string; packageId?: string | null },
  tierPriceMinor: number | null | undefined,
  adultCount: number,
  childCount: number,
): number {
  if (!booking.packageId || booking.bookingType !== 'ROUND_TRIP' || !tierPriceMinor) {
    return booking.totalMinor;
  }
  // First child per adult is free; additional children pay full adult fare
  const adultFareMinor = tierPriceMinor * 2;
  const paidChildren = Math.max(0, childCount - adultCount);
  return adultCount * adultFareMinor + paidChildren * adultFareMinor;
}

/**
 * Returns all plausible normalised variants of a raw phone string so that the
 * DB query matches regardless of how the number was stored (local 09… vs international +251…).
 * Returns an empty array when the input is clearly invalid (< 7 digits).
 */
function normalizePhoneVariants(raw: string): string[] {
  // Strip whitespace, dashes, dots, parentheses — keep digits and a leading +
  const stripped = raw.replace(/[^\d+]/g, '');
  const digits = stripped.replace(/^\+/, '');
  if (digits.length < 7) return [];

  const variants = new Set<string>([stripped]);

  if (stripped.startsWith('+251') && digits.length === 12) {
    // +251 9XXXXXXXX → 251 9XXXXXXXX (no +) → 09XXXXXXXX
    variants.add(digits);                   // 251XXXXXXXXX
    variants.add('0' + digits.slice(3));    // 09XXXXXXXXX
  } else if (stripped.startsWith('251') && digits.length === 12) {
    // 251 9XXXXXXXX → +251 9XXXXXXXX → 09XXXXXXXX
    variants.add('+' + stripped);           // +251XXXXXXXXX
    variants.add('0' + digits.slice(3));    // 09XXXXXXXXX
  } else if (stripped.startsWith('0') && digits.length === 10) {
    // 09XXXXXXXX → +251 9XXXXXXXX → 251 9XXXXXXXX (no +)
    variants.add('+251' + digits.slice(1)); // +251XXXXXXXXX
    variants.add('251' + digits.slice(1));  // 251XXXXXXXXX
  } else if (!stripped.startsWith('+') && digits.length >= 9) {
    // bare international digits without +
    variants.add('+' + digits);
  }

  return [...variants];
}

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;
}

interface BookingFilters {
  search?: string;
  status?: string;
  returnLegStatus?: string;
  bookingType?: string;
  paymentStatus?: string;
  dateFrom?: string;
  dateTo?: string;
  page?: number;
  pageSize?: number;
}

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

  constructor(
    private readonly prisma: PrismaService,
    @InjectDataSource() private readonly dataSource: DataSource,
    private readonly seatsService: SeatsService,
    private readonly ticketsService: TicketsService,
    private readonly eventEmitter: EventEmitter2,
    private readonly verifaydaService: VerifaydaService,
    private readonly currencyService: CurrencyService,
    private readonly fareEngine: FareEngineService,
    private readonly auditService: AuditService,
  ) {}

  async findByIamUserId(iamUserId: string, filters: BookingFilters = {}) {
    const passenger = await this.prisma.passenger.findUniqueOrThrow({ where: { iamUserId }, select: { id: true } });
    return this.findByPassengerId(passenger.id, filters);
  }

  async findByPassengerId(passengerId: string, filters: BookingFilters = {}) {
    const { search, status, page = 1, pageSize = 20 } = filters;
    const skip = (page - 1) * pageSize;
    
    const where: any = { passengerId };
    
    if (search) {
      where.OR = [
        { bookingRef: { contains: search, mode: 'insensitive' } },
        { schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } },
        { schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } },
      ];
    }
    
    if (status) {
      where.status = status;
    }
    
    const [items, total] = await Promise.all([
      this.prisma.booking.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          paymentIntent: true,
          seats: { include: { seat: true } },
          priceTier: { select: { priceMinor: true } },
        },
      }),
      this.prisma.booking.count({ where }),
    ]);

    return {
      items: items.map(booking => {
        const segment = resolveBookingSegment((booking as any).schedule, (booking as any).originStationId, (booking as any).destinationStationId);
        return {
          id: booking.id,
          bookingRef: booking.bookingRef,
          status: booking.status,
          totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
          currency: booking.displayCurrency,
          displayCurrency: booking.displayCurrency,
          displayTotalMinor: booking.displayTotalMinor,
          adultCount: booking.adultCount,
          childCount: booking.childCount,
          bookingType: booking.bookingType,
          returnLegStatus: (booking as any).returnLegStatus ?? null,
          createdAt: booking.createdAt,
          schedule: {
            train: booking.schedule.train,
            originStation: segment.origin,
            destinationStation: segment.destination,
            departureAt: segment.departureAt,
            arrivalAt: segment.arrivalAt,
          },
          paymentIntent: booking.paymentIntent,
          seatCount: booking.seats.length,
        };
      }),
      meta: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  async findByPhone(rawPhone: string, filters: BookingFilters = {}) {
    const variants = normalizePhoneVariants(rawPhone);
    if (variants.length === 0) return { items: [], meta: { page: 1, pageSize: 20, total: 0, totalPages: 0 } };

    const { status, page = 1, pageSize = 20 } = filters;
    const skip = (page - 1) * pageSize;

    // Authenticated-user bookings don't store contactPhone — their phone lives in
    // iam.users.phone_number linked via passenger.iamUserId. Mirror the same lookup
    // that findAll uses for the search field.
    const iamRows = await this.dataSource
      .query<{ id: string }[]>(
        `SELECT u.id FROM iam.users u WHERE u.phone_number = ANY($1::text[])`,
        [variants],
      )
      .catch((err: unknown) => {
        this.logger.warn(`IAM phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
        return [] as { id: string }[];
      });

    const iamPassengerIds = iamRows.length > 0
      ? (await this.prisma.passenger.findMany({
          where: { iamUserId: { in: iamRows.map(r => r.id) } },
          select: { id: true },
        })).map(p => p.id)
      : [];

    // Guest bookings store phone in TravelerProfile.notes JSON (created for every guest booking).
    // This catches cases where contactPhone was null but the phone was still recorded in the profile.
    const travelerRows = await this.dataSource
      .query<{ passengerId: string }[]>(
        `SELECT DISTINCT passenger_id AS "passengerId"
         FROM passenger.traveler_profiles
         WHERE notes IS NOT NULL
           AND (notes::jsonb->>'phone') = ANY($1::text[])`,
        [variants],
      )
      .catch((err: unknown) => {
        this.logger.warn(`TravelerProfile phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
        return [] as { passengerId: string }[];
      });
    const travelerPassengerIds = travelerRows.map(r => r.passengerId);

    // Guests who saved their profile (savePassengerDetails:true) have a SavedPassengerProfile
    // row with phone + deviceId. Guest bookings store the deviceId in Booking.userAgent.
    const savedProfileRows = await this.dataSource
      .query<{ deviceId: string }[]>(
        `SELECT DISTINCT device_id AS "deviceId"
         FROM passenger.saved_passenger_profiles
         WHERE phone = ANY($1::text[]) AND device_id IS NOT NULL`,
        [variants],
      )
      .catch((err: unknown) => {
        this.logger.warn(`SavedPassengerProfile phone lookup failed: ${err instanceof Error ? err.message : String(err)}`);
        return [] as { deviceId: string }[];
      });
    const guestDeviceIds = savedProfileRows.map(r => r.deviceId);

    // Merge all passenger IDs from every source
    const allPassengerIds = [...new Set([...iamPassengerIds, ...travelerPassengerIds])];

    const where: any = {
      OR: [
        { contactPhone: { in: variants } },
        { passenger: { user: { phone: { in: variants } } } },
        ...(allPassengerIds.length > 0 ? [{ passengerId: { in: allPassengerIds } }] : []),
        ...(guestDeviceIds.length > 0 ? [{ userAgent: { in: guestDeviceIds } }] : []),
      ],
    };
    if (status) where.status = status;

    // PackageBooking is a separate table with its own contactPhone field —
    // must be queried independently or guest package bookings are invisible.
    const pkgWhere: any = {
      OR: [
        { contactPhone: { in: variants } },
        ...(allPassengerIds.length > 0 ? [{ passengerId: { in: allPassengerIds } }] : []),
      ],
    };
    if (status) pkgWhere.status = status;

    const [items, total, pkgItems, pkgTotal] = await Promise.all([
      this.prisma.booking.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          paymentIntent: { select: { method: true, status: true, amountMinor: true, currency: true } },
          seats: { select: { id: true } },
          priceTier: { select: { priceMinor: true } },
        },
      }),
      this.prisma.booking.count({ where }),
      this.prisma.packageBooking.findMany({
        where: pkgWhere,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          package: {
            include: {
              outboundSchedule: { include: { originStation: true, destinationStation: true } },
            },
          },
          paymentIntent: { select: { method: true, status: true, amountMinor: true, currency: true } },
        },
      }),
      this.prisma.packageBooking.count({ where: pkgWhere }),
    ]);

    const mappedBookings = items.map(booking => {
      const segment = resolveBookingSegment((booking as any).schedule, (booking as any).originStationId, (booking as any).destinationStationId);
      return {
      id: booking.id,
      bookingRef: booking.bookingRef,
      status: booking.status,
      totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
      currency: booking.displayCurrency,
      displayCurrency: booking.displayCurrency,
      displayTotalMinor: booking.displayTotalMinor,
      adultCount: booking.adultCount,
      childCount: booking.childCount,
      bookingType: booking.bookingType,
      returnLegStatus: (booking as any).returnLegStatus ?? null,
      createdAt: booking.createdAt,
      schedule: {
        train: booking.schedule.train,
        originStation: segment.origin,
        destinationStation: segment.destination,
        departureAt: segment.departureAt,
        arrivalAt: segment.arrivalAt,
      },
      payment: booking.paymentIntent ?? undefined,
      seatCount: booking.seats.length,
      };
    });

    const mappedPkg = pkgItems.map((b: any) => ({
      id: b.id,
      bookingRef: b.bookingRef,
      status: b.status,
      totalMinor: b.displayTotalMinor ?? b.totalMinor,
      currency: b.displayCurrency ?? b.currency ?? null,
      displayCurrency: b.displayCurrency ?? null,
      displayTotalMinor: b.displayTotalMinor ?? null,
      adultCount: b.adultCount,
      childCount: b.childCount,
      bookingType: 'PACKAGE',
      returnLegStatus: null,
      createdAt: b.createdAt,
      schedule: b.package?.outboundSchedule
        ? {
            train: null,
            originStation: b.package.outboundSchedule.originStation,
            destinationStation: b.package.outboundSchedule.destinationStation,
            departureAt: b.package.outboundSchedule.departureAt,
            arrivalAt: b.package.outboundSchedule.arrivalAt,
          }
        : null,
      payment: b.paymentIntent ?? undefined,
      seatCount: b.passengerCount,
    }));

    const allItems = [...mappedBookings, ...mappedPkg]
      .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
      .slice(0, pageSize);

    return {
      items: allItems,
      meta: {
        page,
        pageSize,
        total: total + pkgTotal,
        totalPages: Math.ceil((total + pkgTotal) / pageSize),
      },
    };
  }

  async findByDeviceId(deviceId: string, filters: BookingFilters = {}) {
    const { search, status, page = 1, pageSize = 20 } = filters;
    const skip = (page - 1) * pageSize;
    
    // Find passenger linked to this device via iamUserId
    const device = await this.prisma.device.findUnique({ where: { id: deviceId } }).catch(() => null);
    const passenger = device?.iamUserId
      ? await this.prisma.passenger.findUnique({ where: { iamUserId: device.iamUserId } }).catch(() => null)
      : null;

    const searchConditions = search ? [
      { bookingRef: { contains: search, mode: 'insensitive' } },
      { schedule: { originStation: { name: { contains: search, mode: 'insensitive' } } } },
      { schedule: { destinationStation: { name: { contains: search, mode: 'insensitive' } } } },
    ] : [];

    const where: any = {
      OR: [
        { userAgent: deviceId },
        ...(passenger ? [{ passengerId: passenger.id }] : []),
      ],
    };
    
    if (search) {
      where.AND = [{ OR: searchConditions }];
    }
    
    if (status) {
      where.status = status;
    }
    
    const [items, total] = await Promise.all([
      this.prisma.booking.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          paymentIntent: true,
          seats: { include: { seat: true } },
          priceTier: { select: { priceMinor: true } },
        },
      }),
      this.prisma.booking.count({ where }),
    ]);

    return {
      items: items.map(booking => {
        const segment = resolveBookingSegment((booking as any).schedule, (booking as any).originStationId, (booking as any).destinationStationId);
        return {
        id: booking.id,
        bookingRef: booking.bookingRef,
        status: booking.status,
        totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
        currency: booking.displayCurrency,
        displayCurrency: booking.displayCurrency,
        displayTotalMinor: booking.displayTotalMinor,
        adultCount: booking.adultCount,
        childCount: booking.childCount,
        bookingType: booking.bookingType,
        returnLegStatus: (booking as any).returnLegStatus ?? null,
        createdAt: booking.createdAt,
        schedule: {
          train: booking.schedule.train,
          originStation: segment.origin,
          destinationStation: segment.destination,
          departureAt: segment.departureAt,
          arrivalAt: segment.arrivalAt,
        },
        paymentIntent: booking.paymentIntent,
        seatCount: booking.seats.length,
        };
      }),
      meta: {
        page,
        pageSize,
        total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  async findAll(filters: BookingFilters = {}) {
    const { search, status, returnLegStatus, bookingType, paymentStatus, dateFrom, dateTo, page = 1, pageSize = 20 } = filters;
    const skip = (page - 1) * pageSize;

    const onlyPackages = bookingType === 'PACKAGE';
    const includePackageBookings = !returnLegStatus && bookingType !== 'ONE_WAY' && bookingType !== 'ROUND_TRIP' && bookingType !== 'TRANSIT' && bookingType !== 'ROUND_TRIP_TRANSIT';

    const where: any = {};

    if (search) {
      const iamRows = await this.dataSource.query<{ id: string }[]>(
        `SELECT u.id FROM iam.users u
         WHERE (u.name->>'en') ILIKE $1 OR (u.name->>'am') ILIKE $1
            OR u.email ILIKE $1 OR u.phone_number ILIKE $1`,
        [`%${search}%`],
      );
      const matchedPassengers = iamRows.length > 0
        ? await this.prisma.passenger.findMany({
            where: { iamUserId: { in: iamRows.map(r => r.id) } },
            select: { id: true },
          })
        : [];

      where.OR = [
        { bookingRef: { contains: search, mode: 'insensitive' } },
        { contactEmail: { contains: search, mode: 'insensitive' } },
        { contactPhone: { contains: search, mode: 'insensitive' } },
        ...(matchedPassengers.length > 0
          ? [{ passengerId: { in: matchedPassengers.map(p => p.id) } }]
          : []),
        { seats: { some: { passengerName: { contains: search, mode: 'insensitive' } } } },
      ];
    }

    if (status) where.status = status;
    if (returnLegStatus) (where as any).returnLegStatus = returnLegStatus;
    if (bookingType && !onlyPackages) where.bookingType = bookingType;
    if (dateFrom || dateTo) {
      where.createdAt = {
        ...(dateFrom ? { gte: new Date(dateFrom) } : {}),
        ...(dateTo ? { lte: new Date(new Date(dateTo).setHours(23, 59, 59, 999)) } : {}),
      };
    }
    if (paymentStatus) {
      const statusMap: Record<string, string> = { PAID: 'SUCCEEDED', PENDING: 'REQUIRES_ACTION', FAILED: 'FAILED', REFUNDED: 'REFUNDED' };
      const mapped = statusMap[paymentStatus] ?? paymentStatus;
      where.paymentIntent = { is: { status: mapped } };
    }

    const pkgWhere: any = {};
    if (search) {
      pkgWhere.OR = [
        { bookingRef: { contains: search, mode: 'insensitive' } },
        { contactEmail: { contains: search, mode: 'insensitive' } },
        { contactPhone: { contains: search, mode: 'insensitive' } },
        { passengers: { some: { passengerName: { contains: search, mode: 'insensitive' } } } },
      ];
    }
    if (status) pkgWhere.status = status;
    if (dateFrom || dateTo) pkgWhere.createdAt = where.createdAt;
    if (paymentStatus) pkgWhere.paymentIntent = { is: { status: (where.paymentIntent as any)?.is?.status } };

    if (onlyPackages) {
      // Package bookings live in two places:
      // 1. PackageBooking table (dedicated package bookings)
      // 2. Booking table with packageId != null (round-trip bookings linked to a package)
      const bookingPkgWhere: any = { packageId: { not: null } };
      if (status) bookingPkgWhere.status = status;
      if (dateFrom || dateTo) bookingPkgWhere.createdAt = where.createdAt;
      if (paymentStatus) bookingPkgWhere.paymentIntent = where.paymentIntent;
      if (search) bookingPkgWhere.OR = where.OR;

      const [pkgItems, pkgTotal, regPkgItems, regPkgTotal] = await Promise.all([
        this.prisma.packageBooking.findMany({
          where: pkgWhere,
          skip,
          take: pageSize,
          orderBy: { createdAt: 'desc' },
          include: {
            package: { select: { id: true, name: true, code: true } },
            priceTier: { select: { id: true, label: true, seatType: true } },
            passengers: true,
            paymentIntent: true,
          },
        }),
        this.prisma.packageBooking.count({ where: pkgWhere }),
        this.prisma.booking.findMany({
          where: bookingPkgWhere,
          skip,
          take: pageSize,
          orderBy: { createdAt: 'desc' },
          include: {
            passenger: { select: { id: true, iamUserId: true } },
            schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
            paymentIntent: true,
            seats: { include: { seat: true } },
            priceTier: { select: { priceMinor: true } },
          },
        }),
        this.prisma.booking.count({ where: bookingPkgWhere }),
      ]);

      const iamUserIds = regPkgItems.map((b: any) => b.passenger?.iamUserId).filter(Boolean) as string[];
      const iamRows = iamUserIds.length > 0
        ? await this.dataSource.query<{ id: string; email: string; name: any; phone_number: string | null }[]>(
            `SELECT id, email, name, phone_number FROM iam.users WHERE id = ANY($1)`,
            [iamUserIds],
          )
        : [];
      const iamMap = new Map(iamRows.map(r => [r.id, r]));

      const mappedRegPkg = regPkgItems.map((booking: any) => {
        const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined;
        const passengerDetails = booking.seats.map((s: any) => ({ name: s.passengerName, category: s.passengerCategory }));
        const uniquePassengers = Array.from(new Map(passengerDetails.map((p: any) => [p.name, p])).values());
        const segment = booking.schedule ? resolveBookingSegment(booking.schedule, booking.originStationId, booking.destinationStationId) : null;
        return {
          id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
          totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, booking.priceTier?.priceMinor, booking.adultCount, booking.childCount),
          currency: booking.displayCurrency,
          displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor,
          contactEmail: booking.contactEmail, contactPhone: booking.contactPhone,
          bookingType: booking.bookingType, packageId: booking.packageId, isPackageBooking: true,
          returnLegStatus: (booking as any).returnLegStatus ?? null,
          adultCount: booking.adultCount, childCount: booking.childCount,
          createdAt: booking.createdAt,
          passenger: iam ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number } : null,
          passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))],
          passengers: uniquePassengers,
          schedule: segment ? {
            train: booking.schedule.train,
            originStation: segment.origin,
            destinationStation: segment.destination,
            departureAt: segment.departureAt,
          } : null,
          paymentIntent: booking.paymentIntent,
          seatCount: booking.seats.length,
        };
      });

      const mappedPkg = pkgItems.map((b: any) => ({
        id: b.id, bookingRef: b.bookingRef, status: b.status,
        totalMinor: b.displayTotalMinor ?? b.totalMinor, currency: b.displayCurrency || b.currency,
        displayCurrency: b.displayCurrency, displayTotalMinor: b.displayTotalMinor,
        contactEmail: b.contactEmail, contactPhone: b.contactPhone,
        bookingType: 'PACKAGE', packageId: b.packageId, priceTierId: b.priceTierId,
        isPackageBooking: true, packageName: b.package?.name, packageCode: b.package?.code,
        tierLabel: b.priceTier?.label ?? null,
        returnLegStatus: null, adultCount: b.passengerCount, childCount: 0,
        createdAt: b.createdAt, passenger: null,
        passengerNames: b.passengers?.map((p: any) => p.passengerName) ?? [],
        passengers: b.passengers?.map((p: any) => ({ name: p.passengerName, category: 'ADULT' })) ?? [],
        schedule: null, paymentIntent: b.paymentIntent, seatCount: b.passengerCount,
      }));

      const total = pkgTotal + regPkgTotal;
      const allItems = [...mappedPkg, ...mappedRegPkg]
        .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
        .slice(0, pageSize);

      return {
        items: allItems,
        meta: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) },
      };
    }

    const [regularItems, regularTotal, pkgItems, pkgTotal] = await Promise.all([
      this.prisma.booking.findMany({
        where,
        skip,
        take: pageSize,
        orderBy: { createdAt: 'desc' },
        include: {
          passenger: { select: { id: true, iamUserId: true } },
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          paymentIntent: true,
          seats: { include: { seat: { include: { coach: true } } } },
          package: { select: { id: true, name: true, code: true } },
          priceTier: { select: { id: true, label: true, priceMinor: true } },
        },
      }),
      this.prisma.booking.count({ where }),
      includePackageBookings
        ? this.prisma.packageBooking.findMany({
            where: pkgWhere,
            orderBy: { createdAt: 'desc' },
            include: {
              package: { select: { id: true, name: true, code: true } },
              priceTier: { select: { id: true, label: true, seatType: true } },
              passengers: true,
              paymentIntent: true,
            },
          })
        : Promise.resolve([] as any[]),
      includePackageBookings ? this.prisma.packageBooking.count({ where: pkgWhere }) : Promise.resolve(0),
    ]);

    const iamUserIds = regularItems.map((b: any) => b.passenger?.iamUserId).filter(Boolean) as string[];
    const iamRows = iamUserIds.length > 0
      ? await this.dataSource.query<{ id: string; email: string; name: any; phone_number: string | null }[]>(
          `SELECT id, email, name, phone_number FROM iam.users WHERE id = ANY($1)`,
          [iamUserIds],
        ).catch(() => [] as { id: string; email: string; name: any; phone_number: string | null }[])
      : [];
    const iamMap = new Map(iamRows.map(r => [r.id, r]));

    // For bookings that have no contactEmail/contactPhone and no IAM match,
    // fall back to TravelerProfile.notes JSON.
    // Covers: (1) legacy authenticated bookings where IAM returns nothing,
    // (2) guest bookings where passenger.iamUserId is null.
    const passengerIdsNeedingFallback = regularItems
      .filter((b: any) => !b.contactEmail && !b.contactPhone && (!b.passenger?.iamUserId || !iamMap.has(b.passenger.iamUserId)))
      .map((b: any) => b.passengerId)
      .filter(Boolean) as string[];

    const travelerProfileMap = new Map<string, { phone: string | null; email: string | null }>();
    if (passengerIdsNeedingFallback.length > 0) {
      const profiles = await this.prisma.travelerProfile.findMany({
        where: { passengerId: { in: passengerIdsNeedingFallback } },
        select: { passengerId: true, notes: true },
        orderBy: { createdAt: 'asc' },
      });
      for (const profile of profiles) {
        if (travelerProfileMap.has(profile.passengerId)) continue;
        try {
          const notes = profile.notes ? (typeof profile.notes === 'string' ? JSON.parse(profile.notes) : profile.notes) : null;
          if (notes?.phone || notes?.email) {
            travelerProfileMap.set(profile.passengerId, { phone: notes.phone ?? null, email: notes.email ?? null });
          }
        } catch { /* ignore */ }
      }
    }

    const mappedRegular = regularItems.map((booking: any) => {
      const iam = booking.passenger?.iamUserId ? iamMap.get(booking.passenger.iamUserId) : undefined;
      const passengerDetails = booking.seats.map((s: any) => ({ name: s.passengerName, category: s.passengerCategory, seat: s.seat ? { seatNumber: s.seat.seatNumber, coach: s.seat.coach?.number ?? null } : null }));
      const uniquePassengers = Array.from(new Map(passengerDetails.map((p: any) => [p.name, p])).values());

      // Resolve contact: DB row → IAM → TravelerProfile notes → seat name fallback
      const fallback = booking.passengerId ? travelerProfileMap.get(booking.passengerId) : null;
      const resolvedEmail = booking.contactEmail ?? iam?.email ?? fallback?.email ?? null;
      const resolvedPhone = booking.contactPhone ?? iam?.phone_number ?? fallback?.phone ?? null;

      return {
        id: booking.id,
        bookingRef: booking.bookingRef,
        status: booking.status,
        totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
        currency: booking.displayCurrency,
        displayCurrency: booking.displayCurrency,
        displayTotalMinor: booking.displayTotalMinor,
        contactEmail: resolvedEmail,
        contactPhone: resolvedPhone,
        bookingType: booking.bookingType,
        packageId: booking.packageId ?? null,
        priceTierId: (booking as any).priceTierId ?? null,
        packageName: (booking as any).package?.name ?? null,
        packageCode: (booking as any).package?.code ?? null,
        tierLabel: (booking as any).priceTier?.label ?? null,
        isPackageBooking: !!booking.packageId,
        returnLegStatus: (booking as any).returnLegStatus ?? null,
        adultCount: booking.adultCount,
        childCount: booking.childCount,
        createdAt: booking.createdAt,
        originStationId: (booking as any).originStationId ?? null,
        passenger: iam ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number } : null,
        passengerNames: [...new Set(booking.seats.map((s: any) => s.passengerName))],
        passengers: uniquePassengers,
        seats: booking.seats.map((s: any) => ({
          passengerName: s.passengerName,
          passengerCategory: s.passengerCategory,
          leg: s.leg ?? 1,
          fareMinor: s.fareMinor,
          idDocumentType: s.idDocumentType,
          verifaydaVerified: s.verifaydaVerified,
          seat: s.seat ? { seatNumber: s.seat.seatNumber, coach: { number: s.seat.coach?.number ?? null } } : null,
        })),
        schedule: (() => {
          const segment = resolveBookingSegment(booking.schedule, (booking as any).originStationId, (booking as any).destinationStationId);
          return {
            train: booking.schedule.train,
            originStation: segment.origin,
            destinationStation: segment.destination,
            departureAt: segment.departureAt,
          };
        })(),
        paymentIntent: booking.paymentIntent,
        seatCount: booking.seats.length,
      };
    });

    const mappedPkg = pkgItems.map((b: any) => ({
      id: b.id,
      bookingRef: b.bookingRef,
      status: b.status,
      totalMinor: b.displayTotalMinor ?? b.totalMinor,
      currency: b.displayCurrency || b.currency,
      displayCurrency: b.displayCurrency,
      displayTotalMinor: b.displayTotalMinor,
      contactEmail: b.contactEmail,
      contactPhone: b.contactPhone,
      bookingType: 'PACKAGE',
      packageId: b.packageId,
      priceTierId: b.priceTierId,
      isPackageBooking: true,
      packageName: b.package?.name,
      packageCode: b.package?.code,
      tierLabel: b.priceTier?.label ?? null,
      returnLegStatus: null,
      adultCount: b.passengerCount,
      childCount: 0,
      createdAt: b.createdAt,
      passenger: null,
      passengerNames: b.passengers?.map((p: any) => p.passengerName) ?? [],
      passengers: b.passengers?.map((p: any) => ({ name: p.passengerName, category: 'ADULT' })) ?? [],
      schedule: null,
      paymentIntent: b.paymentIntent,
      seatCount: b.passengerCount,
    }));

    const total = regularTotal + pkgTotal;
    const allItems = [...mappedRegular, ...mappedPkg]
      .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
      .slice(0, pageSize);

    return {
      items: allItems,
      meta: { page, pageSize, total, totalPages: Math.ceil(total / pageSize) },
    };
  }

  async create(dto: CreateBookingDto) {
    // Resolve passengerId from iamUserId when the caller is authenticated
    if (dto.passengerId && !dto.passengerId.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i)) {
      // passengerId is actually an iamUserId — resolve the passenger record
      const passenger = await this.prisma.passenger.findUnique({ where: { iamUserId: dto.passengerId }, select: { id: true } });
      if (!passenger) throw new NotFoundException('Passenger profile not found for this account');
      dto = { ...dto, passengerId: passenger.id };
    }
    if (dto.bookingType === 'ROUND_TRIP')         return this.createRoundTripBooking(dto);
    if (dto.bookingType === 'TRANSIT')            return this.createTransitBooking(dto);
    if (dto.bookingType === 'ROUND_TRIP_TRANSIT') return this.createRoundTripTransitBooking(dto);
    return this.createOneWayBooking(dto);
  }

  private validateSeatIdsAgainstHold(holdId: string, holdSeatIds: string[], requestedSeatIds: string[]) {
    for (const seatId of requestedSeatIds) {
      if (!holdSeatIds.includes(seatId)) {
        throw new BadRequestException(
          `Seat ${seatId} is not part of hold ${holdId}. Use seat IDs returned from POST /seats/hold.`,
        );
      }
    }
  }

  /** Resolves contactEmail/contactPhone for an IAM-authenticated passenger booking. */
  private async resolveIamContact(passengerId?: string): Promise<{ contactEmail: string | null; contactPhone: string | null }> {
    if (!passengerId) return { contactEmail: null, contactPhone: null };
    const passenger = await this.prisma.passenger.findUnique({ where: { id: passengerId }, select: { iamUserId: true } });
    if (!passenger?.iamUserId) return { contactEmail: null, contactPhone: null };
    const rows = await this.dataSource.query<{ email: string; phone_number: string | null }[]>(
      `SELECT email, phone_number FROM iam.users WHERE id = $1 LIMIT 1`,
      [passenger.iamUserId],
    );
    return { contactEmail: rows[0]?.email ?? null, contactPhone: rows[0]?.phone_number ?? null };
  }

  private async createOneWayBooking(dto: CreateBookingDto) {
    const hold = await this.prisma.seatHold.findUnique({ where: { id: dto.holdId } });
    if (!hold || hold.expiresAt < new Date()) throw new BadRequestException('Seat hold expired');

    const requestedSeatIds = (dto.passengers as any[]).map(p => p.seatId);
    this.validateSeatIdsAgainstHold(dto.holdId, hold.seatIds, requestedSeatIds);

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

    const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId);
    const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
    if (!originStop || !destStop) throw new NotFoundException('Origin or destination not found');

    const [passengersData, iamContact] = await Promise.all([
      this.processPassengers(dto.passengers as any[]),
      this.resolveIamContact(dto.passengerId),
    ]);
    const { adultCount, childCount } = this.countPassengers(passengersData);
    const fareCalculation = dto.packageId && dto.priceTierId
      ? await this.calculatePackageFare(dto.priceTierId, adultCount, childCount)
      : await this.calculateFare(dto.scheduleId, dto.seatClassId, originStop, destStop, passengersData[0]?.nationality, adultCount, childCount, dto.promoCode, dto.loyaltyRedemptionPoints);
    
    const displayCurrency = dto.displayCurrency || resolveCurrencyFromNationality(passengersData[0]?.nationality);

    // Track per-seat fare. Use the client-supplied seatFareMinor when present (berth-specific
    // pricing for Upper/Middle/Lower beds). Fall back to the fare engine's baseFareMinor.
    const passengersWithFares = passengersData.map(p => {
      let fareMinor: number;
      if (p.category === PassengerCategory.ADULT) {
        fareMinor = p.seatFareMinor ?? fareCalculation.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 ?? fareCalculation.baseFareMinor) : 0;
      }
      return { ...p, fareMinor };
    });

    // Determine the authoritative total.
    // Priority (one-way, non-package):
    //   1. Server-computed sum of per-seat fares when every seated passenger supplied
    //      seatFareMinor — this captures berth-specific pricing (Upper/Middle/Lower)
    //      exactly as shown to the user and cannot be corrupted by a frontend race
    //      condition that sends reviewedTotalMinor before all fares are resolved.
    //   2. reviewedTotalMinor from the frontend — fallback when the server doesn't
    //      have complete per-seat data (e.g. auto-assign with no seat map loaded).
    //   3. Fare engine total — last resort when neither is available.
    // For package bookings reviewedTotalMinor always wins because the tier price
    // may include berth-specific adjustments the server cannot derive alone.
    // Free children have no seatId and no seatFareMinor — exclude from the check.
    const seatedPassengers = passengersData.filter(p => p.seatId);
    const allFaresProvided = seatedPassengers.length > 0 && seatedPassengers.every(p => p.seatFareMinor != null);
    let resolvedTotalMinor: number;
    let displayTotalMinor: number;
    // True when the total came from a client-summed subtotal (per-seat sum or reviewedTotalMinor),
    // which the portal computes UNDISCOUNTED — the promo must still be applied to it (H-13).
    let usedClientSubtotal = false;

    if (allFaresProvided && !dto.packageId) {
      // Server has every passenger's berth fare — sum is the authoritative display total.
      displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.fareMinor, 0);
      resolvedTotalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
        : displayTotalMinor;
      usedClientSubtotal = true;
      if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor !== displayTotalMinor) {
        this.logger.warn(`createOneWayBooking: reviewedTotalMinor=${dto.reviewedTotalMinor} ignored — using server-computed sum=${displayTotalMinor}`);
      }
    } else if (dto.packageId && dto.reviewedTotalMinor != null) {
      // Package booking: client-supplied tier-adjusted total.
      displayTotalMinor = dto.reviewedTotalMinor;
      resolvedTotalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB)
        : 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) {
      // Partial data on server: use client's total as best available.
      displayTotalMinor = dto.reviewedTotalMinor;
      resolvedTotalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB)
        : dto.reviewedTotalMinor;
      usedClientSubtotal = true;
    } else {
      resolvedTotalMinor = fareCalculation.totalMinor;
      displayTotalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(resolvedTotalMinor, Currency.ETB, displayCurrency)
        : resolvedTotalMinor;
    }

    // H-13 fix: the portal sums UNDISCOUNTED per-passenger fares into the total it sends, silently
    // dropping the promo the fare engine recognized (the discount lives only in the fare-breakdown).
    // When the total came from that client subtotal, apply the authoritative promo discount so the
    // customer is charged the discounted price. No-op when no promo applies (discountMinor === 0).
    // The fallback branch above already books fareCalculation.totalMinor (discount included), so it is
    // excluded via usedClientSubtotal to avoid double-subtracting.
    if (usedClientSubtotal && fareCalculation.discountMinor > 0) {
      resolvedTotalMinor = Math.max(0, resolvedTotalMinor - fareCalculation.discountMinor);
      const discountDisplayMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(fareCalculation.discountMinor, Currency.ETB, displayCurrency)
        : fareCalculation.discountMinor;
      displayTotalMinor = Math.max(0, displayTotalMinor - discountDisplayMinor);
    }
    this.logger.log(`createOneWayBooking: resolvedTotalMinor=${resolvedTotalMinor} displayTotalMinor=${displayTotalMinor} displayCurrency=${displayCurrency} (reviewedTotalMinor=${dto.reviewedTotalMinor} allFaresProvided=${allFaresProvided} discountMinor=${fareCalculation.discountMinor} fareEngine=${fareCalculation.totalMinor})`);

    // C-1 guard: never charge less than the server-recomputed authoritative fare. resolvedTotalMinor
    // is the ETB charge basis; fareCalculation.totalMinor is the authoritative ETB fare (already net
    // of promo/loyalty/free-child). A client that forges seatFareMinor / reviewedTotalMinor below it
    // is rejected. Floor (not equality) so legitimate berth surcharges — which only raise the total —
    // still pass; the tolerance absorbs FX-conversion rounding.
    this.assertTotalNotUnderAuthoritative(resolvedTotalMinor, fareCalculation.totalMinor, 'createOneWayBooking');

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: dto.passengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.destinationStationId,
        status: 'PENDING_PAYMENT',
        bookingType: 'ONE_WAY',
        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,
        contactEmail: iamContact.contactEmail,
        contactPhone: iamContact.contactPhone,
        ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
        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,
            fareMinor: p.fareMinor,
            displayCurrency
          }))
        }
      },
      include: { seats: { include: { seat: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } } }
    });

    await this.seatsService.confirmSeats(passengersData.map(p => p.seatId));
    if (dto.packageId && dto.priceTierId) {
      await this.prisma.packagePriceTier.update({
        where: { id: dto.priceTierId },
        data: { bookedSeats: { increment: passengersData.length } },
      });
    }
    this.eventEmitter.emit('booking.created', { booking });
    await this.auditService.log({ userId: dto.passengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ONE_WAY', totalMinor: resolvedTotalMinor } });
    return { ...booking, fareBreakdown: fareCalculation };
  }

  private async createRoundTripBooking(dto: CreateBookingDto) {
    if (!dto.returnScheduleId || !dto.returnHoldId || !dto.returnOriginStationId || !dto.returnDestinationStationId) {
      throw new BadRequestException('Return trip details required for round-trip booking');
    }

    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');
    if (!returnHold || returnHold.expiresAt < new Date()) throw new BadRequestException('Return seat hold expired');

    const holdObSeatIds  = (dto.passengers as any[]).map((p: any) => p.seatId ?? p.outboundSeatId).filter(Boolean);
    const holdRetSeatIds = (dto.passengers as any[]).map((p: any) => p.returnSeatId).filter(Boolean);
    if (holdObSeatIds.length)  this.validateSeatIdsAgainstHold(dto.holdId,       outboundHold.seatIds, holdObSeatIds);
    if (holdRetSeatIds.length) this.validateSeatIdsAgainstHold(dto.returnHoldId!, returnHold.seatIds,   holdRetSeatIds);

    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' } } }
      }),
      this.prisma.trainSchedule.findUnique({
        where: { id: dto.returnScheduleId },
        include: { originStation: true, destinationStation: true, stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } } }
      })
    ]);

    if (!outboundSchedule || !returnSchedule) throw new NotFoundException('Schedule not found');

    const outboundOriginStop = outboundSchedule.stopTimes.find(s => s.stationId === dto.originStationId);
    const outboundDestStop = outboundSchedule.stopTimes.find(s => s.stationId === dto.destinationStationId);
    const returnOriginStop = returnSchedule.stopTimes.find(s => s.stationId === dto.returnOriginStationId);
    const returnDestStop = returnSchedule.stopTimes.find(s => s.stationId === dto.returnDestinationStationId);

    if (!outboundOriginStop || !outboundDestStop || !returnOriginStop || !returnDestStop) {
      throw new NotFoundException('Origin or destination stops not found');
    }

    const [passengersData, iamContact] = await Promise.all([
      this.processRoundTripPassengers(dto.passengers as any[]),
      this.resolveIamContact(dto.passengerId),
    ]);
    const { adultCount, childCount } = this.countPassengers(passengersData);

    // Package bookings use fixed tier price split equally across both legs
    let outboundFare: Awaited<ReturnType<typeof this.calculateFare>>;
    let returnFare:   Awaited<ReturnType<typeof this.calculateFare>>;
    let combinedBaseFareMinor: number;
    let discountMinor = 0;
    let loyaltyMinor = 0;
    let totalMinor: number;

    if (dto.packageId && dto.priceTierId) {
      const pkgFare = await this.calculatePackageFare(dto.priceTierId, adultCount, childCount);
      // pkgFare covers one leg; round-trip = both legs combined
      const roundTripTotal = pkgFare.totalMinor * 2;
      // Split evenly across both legs for per-seat fare recording
      const halfMinor = Math.round(pkgFare.baseFareMinor / 2);
      outboundFare = { ...pkgFare, baseFareMinor: halfMinor, totalBaseFareMinor: Math.round(pkgFare.totalBaseFareMinor / 2) };
      returnFare   = { ...pkgFare, baseFareMinor: pkgFare.baseFareMinor - halfMinor, totalBaseFareMinor: pkgFare.totalBaseFareMinor - Math.round(pkgFare.totalBaseFareMinor / 2) };
      combinedBaseFareMinor = pkgFare.totalBaseFareMinor * 2;
      totalMinor = roundTripTotal;
    } else {
      [outboundFare, returnFare] = await Promise.all([
        this.calculateFare(dto.scheduleId, dto.seatClassId, outboundOriginStop, outboundDestStop, passengersData[0]?.nationality, adultCount, childCount),
        this.calculateFare(dto.returnScheduleId, dto.returnSeatClassId || dto.seatClassId, returnOriginStop, returnDestStop, passengersData[0]?.nationality, adultCount, childCount)
      ]);
      combinedBaseFareMinor = outboundFare.totalBaseFareMinor + returnFare.totalBaseFareMinor;
      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);
        }
      }
      loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * 10;
      totalMinor = Math.max(0, combinedBaseFareMinor - discountMinor - loyaltyMinor);
    }
    // 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 taxesMinor = 0;

    const displayCurrency = dto.displayCurrency || resolveCurrencyFromNationality(passengersData[0]?.nationality);
    // displayTotalMinor will be overridden below when reviewedTotalMinor is provided.
    let displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    // Track per-seat fare. Use client-supplied seatFareMinor/returnSeatFareMinor when
    // present (berth-specific pricing). Fall back to fare engine values.
    const passengersWithFares = passengersData.map(p => {
      let outboundFareMinor: number;
      let returnFareMinor: number;

      if (p.category === PassengerCategory.ADULT) {
        outboundFareMinor = p.seatFareMinor ?? outboundFare.baseFareMinor;
        returnFareMinor   = p.returnSeatFareMinor ?? returnFare.baseFareMinor;
      } else {
        // Free children have no outbound seat (frontend excludes them).
        // Guard by outboundSeatId: unseated = free (0), seated = paid child.
        // Applies to both package and regular bookings.
        outboundFareMinor = p.outboundSeatId ? (p.seatFareMinor ?? outboundFare.baseFareMinor) : 0;
        returnFareMinor   = p.outboundSeatId ? (p.returnSeatFareMinor ?? returnFare.baseFareMinor) : 0;
      }

      return { ...p, outboundFareMinor, returnFareMinor };
    });

    // Override totalMinor with the sum of actual per-seat fares when all seated passengers
    // supplied their fares — free children (no seatId) are excluded from the check.
    // Same priority logic as one-way: server-computed sum wins when all per-seat fares
    // are present; reviewedTotalMinor is used only as fallback to avoid a frontend
    // race condition from under-counting passengers.
    const rtSeatedPassengers = passengersData.filter(p => p.outboundSeatId);
    const allRTFaresProvided = rtSeatedPassengers.length > 0 &&
      rtSeatedPassengers.every(p => p.seatFareMinor != null && p.returnSeatFareMinor != null);

    if (allRTFaresProvided && !dto.packageId) {
      displayTotalMinor = passengersWithFares.reduce((sum, p) => sum + p.outboundFareMinor + p.returnFareMinor, 0);
      totalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(displayTotalMinor, displayCurrency, Currency.ETB)
        : displayTotalMinor;
      if (dto.reviewedTotalMinor != null && dto.reviewedTotalMinor !== displayTotalMinor) {
        this.logger.warn(`createRoundTripBooking: reviewedTotalMinor=${dto.reviewedTotalMinor} ignored — using server-computed sum=${displayTotalMinor}`);
      }
    } else if (dto.packageId && dto.reviewedTotalMinor != null) {
      displayTotalMinor = dto.reviewedTotalMinor;
      totalMinor = displayCurrency !== Currency.ETB
        ? await this.currencyService.convertAmount(dto.reviewedTotalMinor, displayCurrency, Currency.ETB)
        : dto.reviewedTotalMinor;
      const seatedCount = rtSeatedPassengers.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(dto.reviewedTotalMinor, displayCurrency, Currency.ETB)
        : dto.reviewedTotalMinor;
    }

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

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: dto.passengerId,
        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: dto.returnSeatClassId,
        returnLegStatus: 'NEITHER_USED',
        contactEmail: iamContact.contactEmail,
        contactPhone: iamContact.contactPhone,
        ...(dto.packageId ? { packageId: dto.packageId, priceTierId: dto.priceTierId } : {}),
        seats: {
          create: [
            ...passengersWithFares.map(p => ({
              seat: { connect: { id: p.outboundSeatId } },
              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,
              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,
              fareMinor: p.returnFareMinor,
              displayCurrency,
            })),
          ],
        },
      } as any,
      include: { seats: { include: { seat: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } } }
    });

    const outboundSeatIds = passengersData.map(p => p.outboundSeatId);
    const returnSeatIds = passengersData.map(p => p.returnSeatId);
    await Promise.all([
      this.seatsService.confirmSeats(outboundSeatIds),
      this.seatsService.confirmSeats(returnSeatIds)
    ]);

    if (dto.packageId && dto.priceTierId) {
      await this.prisma.packagePriceTier.update({
        where: { id: dto.priceTierId },
        data: { bookedSeats: { increment: passengersData.length } },
      });
    }

    this.eventEmitter.emit('booking.created', { booking });
    await this.auditService.log({ userId: dto.passengerId, action: 'CREATE', entityType: 'Booking', entityId: booking.id, newData: { bookingRef: booking.bookingRef, bookingType: 'ROUND_TRIP', totalMinor } });

    return {
      ...booking,
      fareBreakdown: {
        outboundFare: outboundFare.baseFareMinor,
        returnFare: returnFare.baseFareMinor,
        combinedBaseFareMinor,
        discountMinor,
        loyaltyRedemptionMinor: loyaltyMinor,
        taxesFeesMinor: taxesMinor,
        totalMinor,
        currency: booking.displayCurrency,
        displayCurrency,
        displayTotalMinor
      }
    };
  }

  private async createTransitBooking(dto: CreateBookingDto) {
    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');
    if (!leg2Hold || leg2Hold.expiresAt < new Date()) throw new BadRequestException('Leg-2 seat hold expired');

    const leg1SeatIds = (dto.passengers as any[]).map(p => p.seatId);
    const leg2SeatIds = (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId);
    this.validateSeatIdsAgainstHold(dto.holdId, leg1Hold.seatIds, leg1SeatIds);
    this.validateSeatIdsAgainstHold(dto.leg2HoldId!, leg2Hold.seatIds, leg2SeatIds);

    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' } } },
      }),
      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 station not found on leg-2 schedule');

    const [passengersData, iamContact] = await Promise.all([
      this.processPassengers(dto.passengers as any[]),
      this.resolveIamContact(dto.passengerId),
    ]);
    const { adultCount, childCount } = this.countPassengers(passengersData);

    const leg2SeatClassId = dto.leg2SeatClassId ?? dto.seatClassId;
    const [leg1Fare, leg2Fare] = await Promise.all([
      this.calculateFare(dto.scheduleId, dto.seatClassId, leg1OriginStop, leg1DestStop, passengersData[0]?.nationality, adultCount, childCount),
      this.calculateFare(dto.leg2ScheduleId, leg2SeatClassId, leg2OriginStop, leg2DestStop, passengersData[0]?.nationality, adultCount, childCount),
    ]);

    const combinedBase = leg1Fare.totalBaseFareMinor + leg2Fare.totalBaseFareMinor;
    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 loyaltyMinor    = (dto.loyaltyRedemptionPoints ?? 0) * 10;
    const taxesMinor      = 0;
    const totalMinor      = Math.max(0, combinedBase - discountMinor - loyaltyMinor);
    const displayCurrency = dto.displayCurrency || resolveCurrencyFromNationality(passengersData[0]?.nationality);
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    // Track which child gets free fare for leg1 and leg2
    let leg1FreeChildUsed = false;
    let leg2FreeChildUsed = false;
    const passengersWithFares = passengersData.map(p => {
      let leg1FareMinor: number;
      let leg2FareMinor: number;
      
      if (p.category === PassengerCategory.ADULT) {
        leg1FareMinor = leg1Fare.baseFareMinor;
        leg2FareMinor = leg2Fare.baseFareMinor;
      } else {
        // Child fare for leg1
        if (!leg1FreeChildUsed) {
          leg1FareMinor = 0;
          leg1FreeChildUsed = true;
        } else {
          leg1FareMinor = leg1Fare.baseFareMinor;
        }
        
        // Child fare for leg2
        if (!leg2FreeChildUsed) {
          leg2FareMinor = 0;
          leg2FreeChildUsed = true;
        } else {
          leg2FareMinor = leg2Fare.baseFareMinor;
        }
      }
      
      return { ...p, leg1FareMinor, leg2FareMinor };
    });

    // 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: dto.passengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.transitStationId,
        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,
        contactEmail: iamContact.contactEmail,
        contactPhone: iamContact.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,
              fareMinor: p.leg1FareMinor,
              displayCurrency,
            })),
            ...passengersWithFares.map(p => ({
              seat: { connect: { id: p.leg2SeatId ?? p.seatId } },
              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,
              fareMinor: p.leg2FareMinor,
              displayCurrency,
            })),
          ],
        },
      } as any,
      include: { seats: { include: { seat: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } } },
    });

    await Promise.all([
      this.seatsService.confirmSeats(passengersData.map(p => p.seatId)),
      this.seatsService.confirmSeats(passengersData.map(p => p.leg2SeatId ?? p.seatId)),
    ]);
    this.eventEmitter.emit('booking.created', { booking });

    return {
      ...booking,
      fareBreakdown: {
        leg1BaseFareMinor: leg1Fare.baseFareMinor,
        leg2BaseFareMinor: leg2Fare.baseFareMinor,
        adultCount, childCount,
        freeChildrenCount: Math.min(childCount, 1),
        paidChildrenCount: leg1Fare.paidChildrenCount,
        combinedBaseFareMinor: combinedBase,
        discountMinor, loyaltyRedemptionMinor: loyaltyMinor,
        taxesFeesMinor: taxesMinor, totalMinor,
        currency: displayCurrency, displayTotalMinor,
      },
    };
  }

  private async createRoundTripTransitBooking(dto: CreateBookingDto) {
    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 outbound transit fields (leg2ScheduleId, leg2HoldId, transitStationId, leg2DestinationStationId) ' +
        'AND return transit fields (returnScheduleId, returnHoldId, returnOriginStationId, returnDestinationStationId, ' +
        'returnLeg2ScheduleId, returnLeg2HoldId, returnTransitStationId, returnLeg2DestinationStationId)',
      );
    }

    // Validate all 4 holds
    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 } }),
    ]);
    const now = new Date();
    if (!obL1Hold  || obL1Hold.expiresAt  < now) throw new BadRequestException('Outbound leg-1 seat hold expired');
    if (!obL2Hold  || obL2Hold.expiresAt  < now) throw new BadRequestException('Outbound leg-2 seat hold expired');
    if (!retL1Hold || retL1Hold.expiresAt < now) throw new BadRequestException('Return leg-1 seat hold expired');
    if (!retL2Hold || retL2Hold.expiresAt < now) throw new BadRequestException('Return leg-2 seat hold expired');

    this.validateSeatIdsAgainstHold(dto.holdId,              obL1Hold.seatIds,  (dto.passengers as any[]).map(p => p.seatId));
    this.validateSeatIdsAgainstHold(dto.leg2HoldId!,         obL2Hold.seatIds,  (dto.passengers as any[]).map(p => p.leg2SeatId ?? p.seatId));
    this.validateSeatIdsAgainstHold(dto.returnHoldId!,       retL1Hold.seatIds, (dto.passengers as any[]).map(p => p.returnSeatId));
    this.validateSeatIdsAgainstHold(dto.returnLeg2HoldId!,   retL2Hold.seatIds, (dto.passengers as any[]).map(p => p.returnLeg2SeatId ?? p.returnSeatId));

    // Load all 4 schedules
    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' } } } }),
      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 station not found');
    if (!obL2Origin || !obL2Dest)   throw new NotFoundException('Outbound leg-2: transit or destination not found');
    if (!retL1Origin || !retL1Dest) throw new NotFoundException('Return leg-1: origin or transit station not found');
    if (!retL2Origin || !retL2Dest) throw new NotFoundException('Return leg-2: transit or destination not found');

    const [passengersData, iamContact] = await Promise.all([
      this.processRoundTripPassengers(dto.passengers as any[]),
      this.resolveIamContact(dto.passengerId),
    ]);
    const { adultCount, childCount } = this.countPassengers(passengersData);
    const nat = passengersData[0]?.nationality;

    const obL2SeatClassId  = dto.leg2SeatClassId        ?? dto.seatClassId;
    const retL1SeatClassId = dto.returnSeatClassId       ?? dto.seatClassId;
    const retL2SeatClassId = dto.returnLeg2SeatClassId   ?? dto.seatClassId;

    const [obL1Fare, obL2Fare, retL1Fare, retL2Fare] = await Promise.all([
      this.calculateFare(dto.scheduleId,          dto.seatClassId,   obL1Origin,  obL1Dest,  nat, adultCount, childCount),
      this.calculateFare(dto.leg2ScheduleId,       obL2SeatClassId,  obL2Origin,  obL2Dest,  nat, adultCount, childCount),
      this.calculateFare(dto.returnScheduleId,     retL1SeatClassId, retL1Origin, retL1Dest, nat, adultCount, childCount),
      this.calculateFare(dto.returnLeg2ScheduleId, retL2SeatClassId, retL2Origin, retL2Dest, nat, adultCount, childCount),
    ]);

    const combinedBase = obL1Fare.totalBaseFareMinor + obL2Fare.totalBaseFareMinor +
                         retL1Fare.totalBaseFareMinor + retL2Fare.totalBaseFareMinor;
    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 loyaltyMinor    = (dto.loyaltyRedemptionPoints ?? 0) * 10;
    const taxesMinor      = 0;
    const totalMinor      = Math.max(0, combinedBase - discountMinor - loyaltyMinor);
    const displayCurrency = dto.displayCurrency || resolveCurrencyFromNationality(nat);
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    // Track which child gets free fare for all 4 legs
    let obL1FreeChildUsed = false;
    let obL2FreeChildUsed = false;
    let retL1FreeChildUsed = false;
    let retL2FreeChildUsed = false;
    
    const passengersWithFares = passengersData.map(p => {
      let obL1FareMinor: number, obL2FareMinor: number, retL1FareMinor: number, retL2FareMinor: number;
      
      if (p.category === PassengerCategory.ADULT) {
        obL1FareMinor = obL1Fare.baseFareMinor;
        obL2FareMinor = obL2Fare.baseFareMinor;
        retL1FareMinor = retL1Fare.baseFareMinor;
        retL2FareMinor = retL2Fare.baseFareMinor;
      } else {
        // Child fares for each leg
        obL1FareMinor = !obL1FreeChildUsed ? (obL1FreeChildUsed = true, 0) : obL1Fare.baseFareMinor;
        obL2FareMinor = !obL2FreeChildUsed ? (obL2FreeChildUsed = true, 0) : obL2Fare.baseFareMinor;
        retL1FareMinor = !retL1FreeChildUsed ? (retL1FreeChildUsed = true, 0) : retL1Fare.baseFareMinor;
        retL2FareMinor = !retL2FreeChildUsed ? (retL2FreeChildUsed = true, 0) : retL2Fare.baseFareMinor;
      }
      
      return { ...p, obL1FareMinor, obL2FareMinor, retL1FareMinor, retL2FareMinor };
    });

    const makeSeat = (p: any, seatId: string, leg: number, scheduleId: string, fareMinor: 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,
      fareMinor,
      displayCurrency,
    });

    const booking = await this.prisma.booking.create({
      data: {
        bookingRef: generateRef(),
        passengerId: dto.passengerId,
        scheduleId: dto.scheduleId,
        originStationId: dto.originStationId,
        destinationStationId: dto.leg2DestinationStationId,
        status: 'PENDING_PAYMENT',
        bookingType: 'ROUND_TRIP_TRANSIT',
        totalMinor, currency: Currency.ETB, adultCount, childCount, displayCurrency, displayTotalMinor, // ETB charge basis; passenger currency in displayCurrency/displayTotalMinor
        // Outbound transit leg-2
        leg2ScheduleId: dto.leg2ScheduleId,
        leg2OriginStationId: dto.transitStationId,
        leg2DestinationStationId: dto.leg2DestinationStationId,
        leg2SeatClassId: obL2SeatClassId,
        // Return transit
        returnScheduleId: dto.returnScheduleId,
        returnOriginStationId: dto.returnOriginStationId,
        returnDestinationStationId: dto.returnDestinationStationId,
        returnSeatClassId: retL1SeatClassId,
        returnLeg2ScheduleId: dto.returnLeg2ScheduleId,
        returnLeg2OriginStationId: dto.returnTransitStationId,
        returnLeg2DestStationId: dto.returnLeg2DestinationStationId,
        returnLeg2SeatClassId: retL2SeatClassId,
        returnLegStatus: 'NEITHER_USED',
        contactEmail: iamContact.contactEmail,
        contactPhone: iamContact.contactPhone,
        seats: {
          create: [
            // Outbound leg-1 (sequence 1)
            ...passengersWithFares.map(p => makeSeat(p, p.outboundSeatId, 1, dto.scheduleId, p.obL1FareMinor)),
            // Outbound leg-2 (sequence 2)
            ...passengersWithFares.map(p => makeSeat(p, p.outboundLeg2SeatId ?? p.outboundSeatId, 2, dto.leg2ScheduleId!, p.obL2FareMinor)),
            // Return leg-1 (sequence 3)
            ...passengersWithFares.map(p => makeSeat(p, p.returnSeatId, 3, dto.returnScheduleId!, p.retL1FareMinor)),
            // Return leg-2 (sequence 4)
            ...passengersWithFares.map(p => makeSeat(p, p.returnLeg2SeatId ?? p.returnSeatId, 4, dto.returnLeg2ScheduleId!, p.retL2FareMinor)),
          ],
        },
      } as any,
      include: { seats: { include: { seat: true } }, schedule: { include: { originStation: true, destinationStation: true, train: true } } },
    });

    await Promise.all([
      this.seatsService.confirmSeats(passengersData.map(p => p.outboundSeatId)),
      this.seatsService.confirmSeats(passengersData.map(p => p.outboundLeg2SeatId ?? p.outboundSeatId)),
      this.seatsService.confirmSeats(passengersData.map(p => p.returnSeatId)),
      this.seatsService.confirmSeats(passengersData.map(p => p.returnLeg2SeatId ?? p.returnSeatId)),
    ]);
    this.eventEmitter.emit('booking.created', { booking });

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

  private async processPassengers(passengers: any[]) {
    const processedPassengers = [];
    for (const passenger of passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const age = calculateAge(dateOfBirth);
      const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;

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

      if (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 = nationality || 'Ethiopian';
      } else if (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');
      }

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

  private async processRoundTripPassengers(passengers: any[]) {
    const processedPassengers = [];
    for (const passenger of passengers) {
      const dateOfBirth = new Date(passenger.dateOfBirth);
      const age = calculateAge(dateOfBirth);
      const category: PassengerCategory = age < 5 ? PassengerCategory.CHILD : PassengerCategory.ADULT;

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

      if (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 = nationality || 'Ethiopian';
      } else if (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');
      }

      processedPassengers.push({
        ...passenger,
        passengerName,
        dateOfBirth,
        category,
        verifaydaVerified,
        verifaydaData,
        nationality,
        // Normalise: PassengerInputDto uses seatId/returnSeatId; RoundTripPassengerDto uses
        // outboundSeatId/returnSeatId. Accept either form so both DTOs work.
        outboundSeatId:    passenger.outboundSeatId    ?? passenger.seatId,
        outboundLeg2SeatId: passenger.outboundLeg2SeatId ?? passenger.leg2SeatId,
        returnSeatId:      passenger.returnSeatId,
        returnLeg2SeatId:  passenger.returnLeg2SeatId,
      });
    }
    return processedPassengers;
  }

  private countPassengers(passengersData: any[]) {
    let adultCount = 0, childCount = 0;
    for (const passenger of passengersData) {
      if (passenger.category === PassengerCategory.ADULT) adultCount++;
      else childCount++;
    }
    return { adultCount, childCount };
  }

  private async calculatePackageFare(
    priceTierId: string,
    adultCount: number,
    childCount: number,
  ) {
    const tier = await this.prisma.packagePriceTier.findUniqueOrThrow({ where: { id: priceTierId } });
    // First child per adult travels free (no seat); additional children pay full adult fare.
    const freeChildrenCount = Math.min(childCount, adultCount);
    const paidChildrenCount = Math.max(0, childCount - adultCount);
    const adultFareMinor = tier.priceMinor * adultCount;
    const childTotalMinor = tier.priceMinor * paidChildrenCount;
    const totalBaseFareMinor = adultFareMinor + childTotalMinor;
    return {
      baseFareMinor: tier.priceMinor,
      adultCount,
      adultFareMinor,
      childCount,
      freeChildrenCount,
      paidChildrenCount,
      childFareMinor: childTotalMinor,
      totalBaseFareMinor,
      discountMinor: 0,
      loyaltyRedemptionMinor: 0,
      taxesFeesMinor: 0,
      totalMinor: totalBaseFareMinor,
    };
  }

  /**
   * C-1 protection: 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 1 or 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');
    }
  }

  private async calculateFare(
    scheduleId: string,
    seatClassId: string,
    originStop: any,
    destStop: any,
    nationality?: string,
    adultCount = 1,
    childCount = 0,
    promoCode?: string,
    loyaltyRedemptionPoints?: number
  ) {
    const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
    const baseFareMinor = await this.getBaseFare(scheduleId, seatClassId, segmentRoute, undefined, nationality, originStop.sequence, destStop.sequence, originStop.stationId, destStop.stationId);
    
    const adultFareMinor = baseFareMinor * adultCount;
    const paidChildrenCount = Math.max(0, childCount - 1);
    const childFareMinor = baseFareMinor * paidChildrenCount;
    const totalBaseFareMinor = adultFareMinor + childFareMinor;

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

    const loyaltyMinor = (loyaltyRedemptionPoints ?? 0) * 10;
    const taxesMinor = 0;
    const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor - loyaltyMinor);

    return {
      baseFareMinor,
      adultCount,
      adultFareMinor,
      childCount,
      freeChildrenCount: Math.min(childCount, 1),
      paidChildrenCount,
      childFareMinor,
      totalBaseFareMinor,
      discountMinor,
      loyaltyRedemptionMinor: loyaltyMinor,
      taxesFeesMinor: taxesMinor,
      totalMinor
    };
  }

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

    // 1. SegmentFareRule — most specific explicit price
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      select: { routeId: true, originStationId: true, destinationStationId: true },
    });

    if (schedule?.routeId && originStopSeq !== undefined && destStopSeq !== undefined) {
      const segmentFare = await this.prisma.segmentFareRule.findFirst({
        where: {
          routeId: schedule.routeId,
          originStopSequence: originStopSeq,
          destinationStopSequence: destStopSeq,
          seatClassId,
          nationality: nationality ?? null,
          validFrom: { lte: now },
          OR: [{ validUntil: null }, { validUntil: { gte: now } }],
        },
      }) ?? (nationality ? await this.prisma.segmentFareRule.findFirst({
        where: {
          routeId: schedule.routeId,
          originStopSequence: originStopSeq,
          destinationStopSequence: destStopSeq,
          seatClassId,
          nationality: null,
          validFrom: { lte: now },
          OR: [{ validUntil: null }, { validUntil: { gte: now } }],
        },
      }) : null);

      if (segmentFare) return segmentFare.baseFareMinor;
    }

    // 2. FareRule table — explicit override rules
    const candidates = await this.prisma.fareRule.findMany({
      where: {
        seatClassId,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
      },
    });
    const bestMatch = this.selectBestFareRule(candidates, scheduleId, segmentRoute, fullRoute, nationality);
    if (bestMatch) return bestMatch.baseFareMinor;

    // 3. FareEngine — distance × rate-per-km from the booking's actual segment stations
    if (schedule?.routeId) {
      try {
        const fare = await this.fareEngine.calculate({
          routeId: schedule.routeId,
          originStationId: originStationId ?? schedule.originStationId,
          destinationStationId: destinationStationId ?? schedule.destinationStationId,
          seatClassId,
          nationality,
        });
        return fare.baseFarePerPassengerMinor;
      } 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.`,
    );
  }

  /**
   * Resolves a reservation booking by its standalone payToken — the public entry point for
   * the portal's pay-by-link page (a traveler who never had a portal session, texted a link
   * by a reservation-issuing staff member). Payment itself still goes through the already-
   * public /payments/* endpoints (initiate/methods/booking-amount/status), keyed by the
   * booking id this returns — no change needed there.
   */
  async getByPayToken(token: string) {
    const booking = await this.prisma.booking.findUnique({
      where: { payToken: token },
      include: {
        schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
        seats: { include: { seat: { include: { coach: true } } } },
      },
    });
    if (!booking) throw new NotFoundException('Payment link not found');
    if (booking.status !== 'PENDING_PAYMENT') throw new BadRequestException('This booking is no longer awaiting payment');
    if ((booking as any).payTokenExpiresAt && (booking as any).payTokenExpiresAt < new Date()) {
      throw new BadRequestException('This payment link has expired');
    }

    const segment = resolveBookingSegment((booking as any).schedule, (booking as any).originStationId, (booking as any).destinationStationId);
    return {
      id: booking.id,
      bookingRef: booking.bookingRef,
      status: booking.status,
      totalMinor: booking.totalMinor,
      currency: booking.currency,
      displayCurrency: booking.displayCurrency,
      displayTotalMinor: booking.displayTotalMinor,
      schedule: {
        trainNumber: (booking as any).schedule.train.number,
        trainName: (booking as any).schedule.train.name,
        origin: { id: segment.origin.id, name: segment.origin.name, code: segment.origin.code },
        destination: { id: segment.destination.id, name: segment.destination.name, code: segment.destination.code },
        departureAt: segment.departureAt,
        arrivalAt: segment.arrivalAt,
      },
      seats: (booking as any).seats.map((s: any) => ({
        passengerName: s.passengerName,
        seatNumber: s.seat?.seatNumber ?? null,
        coach: s.seat?.coach?.number ?? null,
      })),
    };
  }

  async getByRef(bookingRefOrId: string) {
    const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(bookingRefOrId);
    const booking = await this.prisma.booking.findUnique({
      where: isUuid ? { id: bookingRefOrId } : { bookingRef: bookingRefOrId },
      include: {
        schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
        returnSchedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
        seats: { include: { seat: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } } } },
        paymentIntent: true, tickets: true,
        priceTier: { select: { priceMinor: true } },
      },
    });

    if (!booking) {
      // Fall back to PackageBooking
      const pkgBooking = await this.prisma.packageBooking.findUnique({
        where: isUuid ? { id: bookingRefOrId } : { bookingRef: bookingRefOrId },
        include: {
          package: { include: { outboundSchedule: { include: { originStation: true, destinationStation: true, train: true } }, returnSchedule: { include: { originStation: true, destinationStation: true } } } },
          priceTier: true,
          passengers: true,
          paymentIntent: true,
        },
      });
      if (!pkgBooking) throw new NotFoundException('Booking not found');
      return {
        id: pkgBooking.id,
        bookingRef: pkgBooking.bookingRef,
        status: pkgBooking.status,
        totalMinor: pkgBooking.displayTotalMinor ?? pkgBooking.totalMinor,
        currency: pkgBooking.displayCurrency || pkgBooking.currency,
        adultCount: pkgBooking.passengerCount,
        childCount: 0,
        displayCurrency: pkgBooking.displayCurrency,
        displayTotalMinor: pkgBooking.displayTotalMinor ?? undefined,
        bookingType: 'PACKAGE',
        packageId: pkgBooking.packageId,
        priceTierId: pkgBooking.priceTierId,
        packageName: (pkgBooking as any).package?.name,
        packageCode: (pkgBooking as any).package?.code,
        tierLabel: (pkgBooking as any).priceTier?.label,
        isPackageBooking: true,
        returnLegStatus: null,
        contactEmail: pkgBooking.contactEmail,
        contactPhone: pkgBooking.contactPhone,
        createdAt: pkgBooking.createdAt,
        schedule: (pkgBooking as any).package?.outboundSchedule ? {
          id: (pkgBooking as any).package.outboundSchedule.id,
          trainNumber: (pkgBooking as any).package.outboundSchedule.train?.number,
          trainName: (pkgBooking as any).package.outboundSchedule.train?.name,
          origin: (pkgBooking as any).package.outboundSchedule.originStation,
          destination: (pkgBooking as any).package.outboundSchedule.destinationStation,
          departureAt: (pkgBooking as any).package.outboundSchedule.departureAt,
          arrivalAt: (pkgBooking as any).package.outboundSchedule.arrivalAt,
        } : null,
        passengers: (pkgBooking as any).passengers?.map((p: any) => ({
          fullName: p.passengerName,
          category: 'ADULT',
          leg: 1,
          fareMinor: Math.round((pkgBooking.displayTotalMinor ?? pkgBooking.totalMinor) / pkgBooking.passengerCount),
          verifaydaVerified: false,
          seat: null,
        })),
        payment: (pkgBooking as any).paymentIntent
          ? {
              method: (pkgBooking as any).paymentIntent.method,
              status: (pkgBooking as any).paymentIntent.status,
              amountMinor: (pkgBooking as any).paymentIntent.amountMinor,
              currency: (pkgBooking as any).paymentIntent.currency,
            }
          : undefined,
        tickets: [],
      };
    }

    // Auto-heal: if booking is CONFIRMED, payment SUCCEEDED, but tickets are missing
    // (ticket generation failed silently after payment — see finalizePaymentSuccess in
    // payments.service.ts), attempt to generate them now so the confirmation page
    // doesn't show "Not yet issued".
    if (
      booking.status === 'CONFIRMED' &&
      (booking as any).tickets?.length === 0 &&
      (booking as any).paymentIntent?.status === 'SUCCEEDED'
    ) {
      try {
        await this.ticketsService.generate(booking.id);
      } catch (err) {
        this.logger.warn(`getByRef: generate failed for booking ${booking.id}: ${err instanceof Error ? err.message : String(err)}. Trying smart assign.`);
        try {
          await this.ticketsService.smartAssignAndGenerate(booking.id);
        } catch (retryErr) {
          this.logger.error(`getByRef: smart assign also failed for booking ${booking.id}: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`);
        }
      }
      // Re-fetch to include any newly created tickets
      const refreshed = await this.prisma.booking.findUnique({
        where: { id: booking.id },
        include: {
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          returnSchedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          seats: { include: { seat: { include: { coach: { include: { coachType: { include: { seatClasses: true } } } } } } } },
          paymentIntent: true, tickets: true,
          priceTier: { select: { priceMinor: true } },
        },
      });
      if (refreshed) Object.assign(booking, refreshed);
    }

    const outboundSegment = resolveBookingSegment(
      (booking as any).schedule,
      (booking as any).originStationId,
      (booking as any).destinationStationId,
    );
    const returnSegment = (booking as any).returnSchedule
      ? resolveBookingSegment(
          (booking as any).returnSchedule,
          (booking as any).returnOriginStationId,
          (booking as any).returnDestinationStationId,
        )
      : null;

    return {
      id: booking.id, bookingRef: booking.bookingRef, status: booking.status,
      totalMinor: booking.displayTotalMinor ?? resolvePackageRoundTripTotal(booking, (booking as any).priceTier?.priceMinor, booking.adultCount, booking.childCount),
      currency: booking.displayCurrency,
      adultCount: booking.adultCount, childCount: booking.childCount,
      displayCurrency: booking.displayCurrency, displayTotalMinor: booking.displayTotalMinor ?? undefined,
      bookingType: booking.bookingType,
      packageId: (booking as any).packageId ?? null,
      isPackageBooking: !!(booking as any).packageId,
      returnLegStatus: (booking as any).returnLegStatus ?? null,
      outboundBoardedAt: (booking as any).outboundBoardedAt ?? null,
      returnBoardedAt: (booking as any).returnBoardedAt ?? null,
      contactEmail: booking.contactEmail,
      contactPhone: booking.contactPhone,
      createdAt: booking.createdAt,
      schedule: {
        id: (booking as any).schedule.id,
        trainNumber: (booking as any).schedule.train.number,
        trainName: (booking as any).schedule.train.name,
        origin: { id: outboundSegment.origin.id, name: outboundSegment.origin.name, code: outboundSegment.origin.code, city: outboundSegment.origin.city },
        destination: { id: outboundSegment.destination.id, name: outboundSegment.destination.name, code: outboundSegment.destination.code, city: outboundSegment.destination.city },
        departureAt: outboundSegment.departureAt, arrivalAt: outboundSegment.arrivalAt,
      },
      returnSchedule: (booking as any).returnSchedule
        ? {
            id: (booking as any).returnSchedule.id,
            trainNumber: (booking as any).returnSchedule.train.number,
            trainName: (booking as any).returnSchedule.train.name,
            origin: { id: returnSegment!.origin.id, name: returnSegment!.origin.name, code: returnSegment!.origin.code, city: returnSegment!.origin.city },
            destination: { id: returnSegment!.destination.id, name: returnSegment!.destination.name, code: returnSegment!.destination.code, city: returnSegment!.destination.city },
            departureAt: returnSegment!.departureAt, arrivalAt: returnSegment!.arrivalAt,
          }
        : null,
      passengers: (booking as any).seats?.map((bs: any) => {
        // A coach type can have several seat classes (e.g. a VIP Bed coach has separate
        // Upper/Lower classes) — seatClasses[0] is whichever was seeded first, so it always
        // showed the SAME class for every seat in the coach regardless of that seat's own
        // bed position. Match against the seat's actual bedPosition instead (Seat.bedPosition
        // is lowercase, SeatClass.bedPosition is uppercase — compare case-insensitively).
        // Falls back to [0] for non-bed seats (bedPosition is null, single class per coach).
        const classes = bs.seat.coach.coachType?.seatClasses ?? [];
        const matchedClass = bs.seat.bedPosition
          ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === bs.seat.bedPosition.toLowerCase())
          : null;
        return {
          fullName: bs.passengerName,
          category: bs.passengerCategory,
          leg: bs.leg ?? 1,
          fareMinor: bs.fareMinor,
          verifaydaVerified: bs.verifaydaVerified,
          seat: {
            id: bs.seat.id,
            number: bs.seat.seatNumber,
            coach: bs.seat.coach.number,
            coachId: bs.seat.coach.id,
            seatClass: (matchedClass ?? classes[0])?.name ?? null,
          },
        };
      }),
      payment: (booking as any).paymentIntent
        ? {
            method: (booking as any).paymentIntent.method,
            status: (booking as any).paymentIntent.status,
            amountMinor: (booking as any).paymentIntent.amountMinor,
            currency: (booking as any).paymentIntent.currency,
          }
        : undefined,
      // One ticket per passenger per leg (round trips have a separate ticket — and
      // barcode — for the return leg) — matched on the frontend by passengerName +
      // leg, not array position, since tickets are grouped/created independently of
      // the passengers array.
      tickets: (booking as any).tickets?.map((t: any) => ({
        id: t.id,
        passengerName: t.passengerName,
        leg: t.leg ?? 1,
        qrPayload: t.qrPayload,
        barcodePayload: t.barcodePayload,
        status: t.status,
      })) ?? [],
    };
  }

  async modify(dto: ModifyBookingDto, iamUserId?: string) {
    const booking = await this.prisma.booking.findUnique({ where: { bookingRef: dto.bookingRef }, include: { seats: true, schedule: true } });
    if (!booking) throw new NotFoundException('Booking not found');
    if (booking.status !== 'CONFIRMED') throw new BadRequestException('Only confirmed bookings can be modified');
    if (booking.schedule.departureAt < new Date()) throw new BadRequestException('Cannot modify past bookings');

    const oldSeats = booking.seats.map(s => s.seatId);
    await this.prisma.bookingModification.create({
      data: { bookingId: booking.id, modifiedBy: booking.passengerId, modificationType: 'SEAT_CHANGE', oldData: { scheduleId: booking.scheduleId, seatIds: oldSeats }, newData: { scheduleId: dto.newScheduleId, seatIds: dto.newSeatIds }, fareAdjustment: 0, reason: dto.reason },
    });
    await this.seatsService.releaseSeats(booking.id);
    await this.seatsService.confirmSeats(dto.newSeatIds);
    await this.auditService.log({ userId: iamUserId ?? booking.passengerId, action: 'UPDATE', entityType: 'Booking', entityId: booking.id, oldData: { seatIds: oldSeats }, newData: { seatIds: dto.newSeatIds, reason: dto.reason } });
    return { modified: true, bookingRef: dto.bookingRef };
  }

  async cancel(bookingRef: string, reason?: string, iamUserId?: string) {
    const booking = await this.prisma.booking.findUnique({ where: { bookingRef }, include: { seats: true, paymentIntent: true } });
    if (!booking) throw new NotFoundException('Booking not found');
    if (booking.status === 'CANCELLED') throw new BadRequestException('Booking already cancelled');
    const refundAmount = booking.status === 'CONFIRMED' ? Math.floor(booking.totalMinor * 0.8) : 0;
    await this.prisma.bookingCancellation.create({ data: { bookingId: booking.id, cancelledBy: booking.passengerId, reason, refundAmount, refundMethod: booking.paymentIntent?.method ?? 'ORIGINAL', refundStatus: 'PENDING' } });
    await this.seatsService.releaseSeats(booking.id);
    await this.prisma.booking.update({ where: { bookingRef }, data: { status: 'CANCELLED' } });
    this.eventEmitter.emit('booking.cancelled', { booking, refundAmount });
    await this.auditService.log({ userId: iamUserId ?? booking.passengerId, action: 'DELETE', entityType: 'Booking', entityId: booking.id, oldData: { bookingRef, status: booking.status }, newData: { status: 'CANCELLED', reason, refundAmount } });
    return { cancelled: true, refundAmount: refundAmount / 100, currency: booking.displayCurrency};
  }

  async update(id: string, dto: any) {
    const booking = await this.prisma.booking.findUnique({ where: { id } });
    if (!booking) throw new NotFoundException('Booking not found');
    return this.prisma.booking.update({
      where: { id },
      data: {
        status: dto.status || booking.status,
        totalMinor: dto.totalMinor !== undefined ? dto.totalMinor : booking.totalMinor,
        displayCurrency: dto.displayCurrency || booking.displayCurrency,
        displayTotalMinor: dto.displayTotalMinor !== undefined ? dto.displayTotalMinor : booking.displayTotalMinor,
      },
      include: {
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
        paymentIntent: true,
        seats: { include: { seat: true } },
      },
    });
  }

  async delete(id: string, cascade = false) {
    const booking = await this.prisma.booking.findUnique({ where: { id }, include: { seats: true } });
    if (!booking) throw new NotFoundException('Booking not found');

    if (!cascade) {
      const usage = await this.checkBookingUsage(id);
      if (usage.isInUse && usage.constraints) {
        throw new DeleteOperationException('Booking', booking.bookingRef, usage.constraints);
      }
    }

    await this.seatsService.releaseSeats(booking.id);

    if (cascade) {
      // Delete all child records that reference this booking (no onDelete: Cascade in schema)
      const paymentIntent = await this.prisma.paymentIntent.findUnique({ where: { bookingId: id } });
      if (paymentIntent) {
        await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: paymentIntent.id } });
        await this.prisma.paymentIntent.delete({ where: { bookingId: id } });
      }
      const tickets = await this.prisma.ticket.findMany({ where: { bookingId: id }, select: { id: true } });
      for (const t of tickets) {
        await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: t.id } });
      }
      await this.prisma.ticket.deleteMany({ where: { bookingId: id } });
      await this.prisma.bookingModification.deleteMany({ where: { bookingId: id } });
      await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: id } });
      await this.prisma.agentBooking.deleteMany({ where: { bookingId: id } });
      const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: id }, select: { id: true } });
      for (const fo of foodOrders) {
        await this.prisma.foodOrderItem.deleteMany({ where: { orderId: fo.id } });
      }
      await this.prisma.foodOrder.deleteMany({ where: { bookingId: id } });
      await this.prisma.baggageBooking.deleteMany({ where: { bookingId: id } });
      await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: id } });
      const journey = await this.prisma.journey.findUnique({ where: { bookingId: id } });
      if (journey) {
        await this.prisma.journeySegment.deleteMany({ where: { journeyId: journey.id } });
        await this.prisma.journey.delete({ where: { bookingId: id } });
      }
    }

    await this.prisma.bookingSeat.deleteMany({ where: { bookingId: id } });
    await this.prisma.booking.delete({ where: { id } });
    await this.auditService.log({ action: 'DELETE', entityType: 'Booking', entityId: id, oldData: { bookingRef: booking.bookingRef } });

    return { deleted: true, bookingRef: booking.bookingRef };
  }

  async checkBookingUsage(id: string) {
    const booking = await this.prisma.booking.findUnique({ where: { id } });
    if (!booking) throw new NotFoundException('Booking not found');

    const [ticketCount, paymentIntentCount, modificationsCount, cancellationCount] = await Promise.all([
      this.prisma.ticket.count({ where: { bookingId: id } }),
      this.prisma.paymentIntent.count({ where: { bookingId: id } }),
      this.prisma.bookingModification.count({ where: { bookingId: id } }),
      this.prisma.bookingCancellation.count({ where: { bookingId: id } }),
    ]);

    const constraints = [];
    if (ticketCount > 0) constraints.push({ entityName: 'ticket', count: ticketCount, action: 'complete' as const });
    if (paymentIntentCount > 0) constraints.push({ entityName: 'payment record', count: paymentIntentCount, action: 'complete' as const });
    if (modificationsCount > 0) constraints.push({ entityName: 'modification record', count: modificationsCount, action: 'complete' as const });
    if (cancellationCount > 0) constraints.push({ entityName: 'cancellation record', count: cancellationCount, action: 'complete' as const });

    return {
      isInUse: constraints.length > 0,
      affectedModules: constraints.map(c => `${c.count} ${c.entityName}${c.count > 1 ? 's' : ''}`),
      constraints
    };
  }

  @Cron(CronExpression.EVERY_MINUTE)
  async expirePendingBookings() {
    const cutoff = new Date(Date.now() - 20 * 60 * 1000);
    const expired = await this.prisma.booking.findMany({ where: { status: 'PENDING_PAYMENT', createdAt: { lt: cutoff } }, include: { seats: true } });
    for (const b of expired) {
      await this.seatsService.releaseSeats(b.id);
      await this.prisma.booking.update({ where: { id: b.id }, data: { status: 'CANCELLED' } });
    }
  }

  // Mark round-trip bookings where the return train has departed but the return leg
  // was never scanned. Runs every minute; only acts on CONFIRMED bookings whose
  // returnSchedule.departureAt is in the past and returnBoardedAt is still null.
  @Cron(CronExpression.EVERY_MINUTE)
  async markReturnLegNoShows() {
    const now = new Date();
    const graceCutoff = new Date(now.getTime() - 30 * 60 * 1000);

    const candidates = await this.prisma.booking.findMany({
      where: {
        bookingType: 'ROUND_TRIP',
        status: 'CONFIRMED',
        returnLegStatus: 'NEITHER_USED' as any,
        outboundBoardedAt: { not: null },
        returnBoardedAt: null,
        returnScheduleId: { not: null },
      },
      include: { returnSchedule: { select: { departureAt: true } } },
    } as any);

    for (const b of candidates) {
      const returnDep: Date | undefined = (b as any).returnSchedule?.departureAt;
      if (returnDep && returnDep < graceCutoff) {
        await this.prisma.booking.update({
          where: { id: b.id },
          data: { returnLegStatus: 'OUTBOUND_ONLY' } as any,
        });
      }
    }
  }

  private selectBestFareRule(
    candidates: any[],
    scheduleId: string,
    segmentRoute?: string,
    fullRoute?: string,
    nationality?: string,
  ): any | null {
    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,
      );
      if (match) return match;
    }

    return null;
  }
}
