import { Injectable, NotFoundException, BadRequestException, ConflictException, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { PrismaService } from '../../common/prisma.service';
import { NotificationsService } from '../notifications/notifications.service';
import { SystemConfigService, CONFIG_KEYS } from '../system-config/system-config.service';
import { AuditService } from '../../common/audit.service';
import { resolveBookingSegment } from '../../common/utils/segment-resolver.utils';
import * as QRCode from 'qrcode';

interface OfflineValidation {
  bookingRef: string;
  validatorId: string;
  gateId?: string;
  validatedAt: string;
  leg?: string;
}

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

  constructor(
    private readonly prisma: PrismaService,
    private readonly notifications: NotificationsService,
    private readonly systemConfig: SystemConfigService,
    private readonly auditService: AuditService,
    @InjectDataSource() private readonly dataSource: DataSource,
  ) {}

  async listTickets(filters: { search?: string; status?: string; originStationId?: string; destinationStationId?: string; departureDate?: string; arrivalDate?: string; dateFrom?: string; dateTo?: string; coachId?: string; skip: number; take: number }) {
    const where: any = {};
    if (filters.search) {
      where.OR = [
        { bookingRef: { contains: filters.search, mode: 'insensitive' } },
        { barcodePayload: { contains: filters.search, mode: 'insensitive' } },
        { passengerName: { contains: filters.search, mode: 'insensitive' } },
        { booking: { bookingRef: { contains: filters.search, mode: 'insensitive' } } },
      ];
    }
    if (filters.status) {
      where.status = filters.status;
    }
    if (filters.originStationId) {
      where.booking = { ...where.booking, originStationId: filters.originStationId };
    }
    if (filters.destinationStationId) {
      where.booking = { ...where.booking, destinationStationId: filters.destinationStationId };
    }
    if (filters.departureDate) {
      const start = new Date(filters.departureDate);
      const end = new Date(filters.departureDate);
      end.setDate(end.getDate() + 1);
      where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, departureAt: { gte: start, lt: end } } };
    }
    if (filters.arrivalDate) {
      const start = new Date(filters.arrivalDate);
      const end = new Date(filters.arrivalDate);
      end.setDate(end.getDate() + 1);
      where.booking = { ...where.booking, schedule: { ...where.booking?.schedule, arrivalAt: { gte: start, lt: end } } };
    }
    if (filters.dateFrom || filters.dateTo) {
      where.issuedAt = {
        ...(filters.dateFrom ? { gte: new Date(filters.dateFrom) } : {}),
        ...(filters.dateTo ? { lte: new Date(new Date(filters.dateTo).setHours(23, 59, 59, 999)) } : {}),
      };
    }
    if (filters.coachId) {
      where.seat = {
        coachId: filters.coachId
      };
    }
    
    const [tickets, total] = await Promise.all([
      this.prisma.ticket.findMany({
        where,
        include: {
          booking: {
            include: {
              schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
              returnSchedule: { include: { originStation: true, destinationStation: true } },
              passenger: { include: { travelerProfiles: true } },
              seats: { include: { seat: { include: { coach: true } } } },
            },
          },
          seat: { include: { coach: { include: { coachType: true } } } },
        } as any,
        skip: filters.skip,
        take: filters.take,
        orderBy: { issuedAt: 'desc' },
      }) as any,
      this.prisma.ticket.count({ where }),
    ]);

    const iamUserIds = tickets.map((t: any) => t.booking?.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]));

    return {
      items: tickets.map((t: any) => {
        const iam = t.booking?.passenger?.iamUserId ? iamMap.get(t.booking.passenger.iamUserId) : undefined;
        
        // Extract phone from TravelerProfile notes JSON
        let guestPhone = null;
        let guestEmail = null;
        const matchingProfile = t.booking?.passenger?.travelerProfiles?.find((tp: any) => tp.fullName === t.passengerName);
        
        if (matchingProfile?.notes) {
          try {
            const notesData = JSON.parse(matchingProfile.notes);
            guestPhone = notesData.phone || null;
            guestEmail = notesData.email || null;
          } catch (err) {
            this.logger.error(`Failed to parse notes JSON: ${err}`);
          }
        }
        
        // Fallback to booking contact info if no match in TravelerProfile
        if (!guestPhone) guestPhone = t.booking?.contactPhone;
        if (!guestEmail) guestEmail = t.booking?.contactEmail;
        
        
        const passengerInfo = iam
          ? { fullName: iam.name?.en ?? iam.name?.am ?? null, email: iam.email, phone: iam.phone_number }
          : { fullName: 'Guest', email: guestEmail, phone: guestPhone };
        
        
        const segment = resolveBookingSegment(t.booking?.schedule, t.booking?.originStationId, t.booking?.destinationStationId);
        return {
          id: t.id,
          ticketNumber: t.barcodePayload,
          bookingRef: t.bookingRef,
          passengerName: t.passengerName,
          leg: t.leg,
          booking: {
            id: t.booking?.id,
            bookingRef: t.booking?.bookingRef,
            status: t.booking?.status,
            bookingType: t.booking?.bookingType,
            returnLegStatus: t.booking?.returnLegStatus ?? null,
            outboundBoardedAt: t.booking?.outboundBoardedAt ?? null,
            returnBoardedAt: t.booking?.returnBoardedAt ?? null,
            totalMinor: t.booking?.totalMinor,
            currency: t.booking?.currency,
            displayCurrency: t.booking?.displayCurrency,
            displayTotalMinor: t.booking?.displayTotalMinor,
            passenger: passengerInfo,
            contactEmail: t.booking?.contactEmail,
            contactPhone: t.booking?.contactPhone,
            returnSchedule: t.booking?.returnSchedule ?? null,
            seats: t.booking?.seats ?? [],
            originStation: segment.origin,
            destinationStation: segment.destination,
          },
          schedule: t.booking?.schedule ? {
            ...t.booking.schedule,
            departureAt: segment.departureAt,
            arrivalAt: segment.arrivalAt,
          } : null,
          seat: t.seat ? {
            id: t.seat.id,
            seatNumber: t.seat.seatNumber,
            coach: t.seat.coach ? {
              id: t.seat.coach.id,
              number: t.seat.coach.number,
              coachType: t.seat.coach.coachType ? {
                id: t.seat.coach.coachType.id,
                name: t.seat.coach.coachType.name,
                type: t.seat.coach.coachType.type,
              } : null,
            } : null,
          } : null,
          status: t.status,
          validatedAt: t.validatedAt,
          boardedAt: t.validatedAt,
          qrCode: t.qrPayload ?? null,
          createdAt: t.issuedAt,
        };
      }),
      total,
      skip: filters.skip,
      take: filters.take,
    };
  }

  // Smart seat assignment for conflict resolution:
  // 1. If the original seat is still free → keep it and generate
  // 2. If the original seat is taken → find a truly available seat in the same coach type
  //    (excludes: confirmed/boarded bookings, active holds, seat blocks, BOOKED/HELD/REMOVED status)
  // 3. If no seats of that class remain → throw so the agent is notified
  async smartAssignAndGenerate(bookingId: string) {
    const booking = await this.prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        seats: {
          include: {
            seat: { include: { coach: { include: { coachType: true } } } },
          },
        },
      },
    });
    if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);

    // Seats held by any active SeatHold (not yet expired)
    const heldSeatIds = await this.prisma.seatHold.findMany({
      where: { expiresAt: { gt: new Date() } },
      select: { seatIds: true },
    }).then(rows => new Set(rows.flatMap(r => r.seatIds)));

    // Seats with an active SeatBlock
    const blockedSeatIds = await this.prisma.seatBlock.findMany({
      select: { seatId: true },
    }).then(rows => new Set(rows.map(r => r.seatId)));

    const ownSeatIds = new Set((booking as any).seats.map((bs: any) => bs.seatId as string));
    const reassigned: { seatNumber: string; newSeatNumber: string }[] = [];

    // Track newly assigned seats so the same seat isn't given to two passengers
    const unavailableIds = new Set([
      ...[...heldSeatIds],
      ...[...blockedSeatIds],
    ]);

    for (const bs of (booking as any).seats) {
      const originalSeatId: string = bs.seatId;
      // Use the per-seat scheduleId — for ROUND_TRIP leg 2 this is the return schedule,
      // not booking.scheduleId (the outbound schedule).
      const legScheduleId: string = bs.scheduleId ?? booking.scheduleId;

      // Seats taken by other confirmed/boarded bookings on THIS leg's schedule
      const takenByOthersOnLeg = await this.prisma.bookingSeat.findMany({
        where: {
          booking: { id: { not: bookingId }, status: { in: ['CONFIRMED', 'BOARDED'] } },
          seat: { coach: { assignments: { some: { scheduleId: legScheduleId } } } },
        },
        select: { seatId: true },
      }).then(rows => new Set(rows.map(r => r.seatId)));

      // Case 1: original seat is still free on this leg — nothing to do
      if (
        !takenByOthersOnLeg.has(originalSeatId) &&
        !heldSeatIds.has(originalSeatId) &&
        !blockedSeatIds.has(originalSeatId)
      ) continue;

      // Case 2: original seat is unavailable — find a free seat of the same coach type on this leg's schedule
      const coachTypeId: string | undefined = bs.seat?.coach?.coachTypeId;

      const allUnavailable = new Set([
        ...[...takenByOthersOnLeg].filter(id => !ownSeatIds.has(id)),
        ...[...unavailableIds],
      ]);

      const candidate = await this.prisma.seat.findFirst({
        where: {
          status: 'AVAILABLE',
          seatNumber: { not: '' },
          NOT: [
            { seatNumber: { startsWith: '-' } },
            { id: { in: [...allUnavailable] } },
          ],
          coach: {
            assignments: { some: { scheduleId: legScheduleId } },
            ...(coachTypeId ? { coachTypeId } : {}),
          },
        },
        orderBy: [{ coach: { number: 'asc' } }, { row: 'asc' }, { col: 'asc' }],
      });

      // Case 3: no seats left in that class on this leg
      if (!candidate) {
        const className = bs.seat?.coach?.coachType?.name ?? 'the same class';
        throw new ConflictException(
          `No available seats remaining in ${className} on this schedule. Please contact the passenger to arrange an alternative.`,
        );
      }

      await this.prisma.bookingSeat.update({
        where: { id: bs.id },
        data: { seatId: candidate.id },
      });

      unavailableIds.add(candidate.id);
      reassigned.push({ seatNumber: bs.seat.seatNumber, newSeatNumber: candidate.seatNumber });
    }

    await this.auditService.log({
      action: 'UPDATE',
      entityType: 'Booking',
      entityId: bookingId,
      newData: { smartReassigned: true, changes: reassigned },
    });

    return this.generate(bookingId);
  }

  async generate(bookingId: string) {
    if (!bookingId) throw new BadRequestException('Booking ID is required');

    const booking = await this.prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        schedule: { include: { originStation: true, destinationStation: true, train: true } },
        seats: { include: { seat: { include: { coach: true } } } },
        paymentIntent: true,
      },
    });
    if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);

    // No payment intent record at all
    if (!(booking as any).paymentIntent) {
      throw new HttpException(
        { status: 'error', message: 'Payment not completed', code: 400 },
        HttpStatus.BAD_REQUEST,
      );
    }

    // Payment intent exists but not yet succeeded
    if ((booking as any).paymentIntent.status !== 'SUCCEEDED') {
      throw new HttpException(
        {
          status: 'error',
          message: 'Payment not completed',
          code: 400,
          detail: `Payment status: ${(booking as any).paymentIntent.status}`,
        },
        HttpStatus.BAD_REQUEST,
      );
    }

    // Booking not in CONFIRMED state — could be a webhook delivery failure.
    if (booking.status !== 'CONFIRMED') {
      if (booking.status === 'PENDING_PAYMENT') {
        this.logger.warn(
          `Booking ${bookingId} is PENDING_PAYMENT but payment intent SUCCEEDED — webhook likely missed. Auto-confirming before ticket generation.`,
        );
        await this.prisma.booking.update({
          where: { id: bookingId },
          data: { status: 'CONFIRMED' },
        });
      } else {
        throw new HttpException(
          {
            status: 'error',
            message: 'Payment not completed',
            code: 400,
            detail: `Booking status: ${booking.status}`,
          },
          HttpStatus.BAD_REQUEST,
        );
      }
    }

    await this.prisma.ticket.deleteMany({ where: { bookingId } });

    // Remove any SeatBlock rows left over from a previous generate() run for this
    // booking — they reference the old ticket IDs which are now deleted, and would
    // otherwise cause the conflict check below to see this booking's own seats as
    // blocked by another booking.
    const seatIds = (booking as any).seats.map((bs: any) => bs.seatId);
    await this.prisma.seatBlock.deleteMany({
      where: { seatId: { in: seatIds }, blockedBy: 'SYSTEM' },
    });

    // Check for seat conflicts — only seats confirmed/boarded by a *different* booking
    // on the SAME schedule AND with OVERLAPPING segments are a real conflict.
    // Segment overlap: two bookings conflict on a seat when their stop-sequence ranges
    // overlap: A.originSeq < B.destSeq AND B.originSeq < A.destSeq.
    // We resolve sequences via TripStopTime using each booking's originStationId /
    // destinationStationId. Bookings with no station IDs (full-route) are treated as
    // seq 0 → ∞ and always overlap.
    const thisBookingSeats = (booking as any).seats as Array<{ seatId: string; scheduleId: string | null }>;

    // Resolve this booking's stop sequences per leg schedule
    const thisSeqMap = new Map<string, { originSeq: number; destSeq: number }>();
    const legScheduleIds = [...new Set(thisBookingSeats.map(bs => bs.scheduleId ?? booking.scheduleId))];
    for (const schedId of legScheduleIds) {
      const originId = (booking as any).originStationId;
      const destId   = (booking as any).destinationStationId;
      if (!originId || !destId) {
        thisSeqMap.set(schedId, { originSeq: 0, destSeq: Number.MAX_SAFE_INTEGER });
        continue;
      }
      const stops = await this.prisma.tripStopTime.findMany({
        where: { scheduleId: schedId, stationId: { in: [originId, destId] } },
        select: { stationId: true, sequence: true },
      });
      const oStop = stops.find(s => s.stationId === originId);
      const dStop = stops.find(s => s.stationId === destId);
      thisSeqMap.set(schedId, {
        originSeq: oStop?.sequence ?? 0,
        destSeq:   dStop?.sequence ?? Number.MAX_SAFE_INTEGER,
      });
    }

    // Find other confirmed/boarded bookings that share any (seatId, scheduleId) pair
    const candidateConflicts = await this.prisma.bookingSeat.findMany({
      where: {
        OR: thisBookingSeats.map(bs => ({
          seatId:     bs.seatId,
          scheduleId: bs.scheduleId ?? booking.scheduleId,
          booking: { id: { not: bookingId }, status: { in: ['CONFIRMED', 'BOARDED'] } },
        })),
      },
      include: {
        seat:    true,
        booking: { select: { id: true, originStationId: true, destinationStationId: true } },
      },
    });

    const trueConflicts: string[] = [];
    for (const other of candidateConflicts) {
      const legScheduleId = other.scheduleId ?? booking.scheduleId;
      const thisSeq = thisSeqMap.get(legScheduleId) ?? { originSeq: 0, destSeq: Number.MAX_SAFE_INTEGER };

      const otherOriginId = (other.booking as any).originStationId;
      const otherDestId   = (other.booking as any).destinationStationId;
      let otherOriginSeq = 0;
      let otherDestSeq   = Number.MAX_SAFE_INTEGER;
      if (otherOriginId && otherDestId) {
        const stops = await this.prisma.tripStopTime.findMany({
          where: { scheduleId: legScheduleId, stationId: { in: [otherOriginId, otherDestId] } },
          select: { stationId: true, sequence: true },
        });
        otherOriginSeq = stops.find(s => s.stationId === otherOriginId)?.sequence ?? 0;
        otherDestSeq   = stops.find(s => s.stationId === otherDestId)?.sequence   ?? Number.MAX_SAFE_INTEGER;
      }

      // Segments overlap when: thisOrigin < otherDest AND otherOrigin < thisDest
      if (thisSeq.originSeq < otherDestSeq && otherOriginSeq < thisSeq.destSeq) {
        trueConflicts.push((other as any).seat.seatNumber);
      }
    }

    if (trueConflicts.length > 0) {
      const labels = [...new Set(trueConflicts)].join(', ');
      throw new ConflictException(
        `Seat(s) ${labels} are already confirmed for another booking on the same schedule and overlapping segment.`,
      );
    }

    // Generate one ticket per passenger per leg.
    // Round-trip / transit bookings have seats on multiple legs — each leg needs its own
    // ticket so the voucher can match by (passengerName, leg) and gate scanners can
    // validate each leg independently.
    const tickets = [];

    // Group seats by (passengerName, leg)
    const passengerLegSeatsMap = new Map<string, any[]>();
    for (const bookingSeat of (booking as any).seats) {
      const key = `${bookingSeat.passengerName}|${bookingSeat.leg ?? 1}`;
      if (!passengerLegSeatsMap.has(key)) {
        passengerLegSeatsMap.set(key, []);
      }
      passengerLegSeatsMap.get(key)!.push(bookingSeat);
    }

    // Create one ticket per (passenger, leg)
    for (const [key, legSeats] of passengerLegSeatsMap.entries()) {
      const [passengerName] = key.split('|');
      const primarySeat = legSeats[0];
      const leg = primarySeat.leg ?? 1;

      const barcodePayload = `${booking.bookingRef}${primarySeat.seatId.substring(0, 8).toUpperCase()}L${leg}`;

      const qrDataWithTicket = JSON.stringify({
        ref: booking.bookingRef,
        ticketNumber: barcodePayload,
        type: booking.bookingType,
        passenger: passengerName,
        leg,
        seats: legSeats.map(ps => ({
          seat: ps.seat?.seatNumber,
          coach: ps.seat?.coach?.number,
          leg: ps.leg || 1,
          scheduleId: ps.scheduleId || booking.scheduleId,
        })),
      });
      const qrPayloadFinal = await QRCode.toDataURL(qrDataWithTicket);

      const ticket = await this.prisma.ticket.create({
        data: {
          bookingId,
          bookingRef: booking.bookingRef,
          passengerName,
          seatId: primarySeat.seatId,
          leg,
          scheduleId: primarySeat.scheduleId || booking.scheduleId,
          qrPayload: qrPayloadFinal,
          barcodePayload,
        } as any,
      });
      tickets.push(ticket);
    }

    // Block all seats across all legs
    for (const seatId of seatIds) {
      await this.prisma.seat.update({ where: { id: seatId }, data: { status: 'BOOKED' } });
      await this.prisma.seatBlock.create({
        data: { seatId, reason: `Booked in tickets ${tickets.map(t => t.id).join(', ')}`, blockedBy: 'SYSTEM', approvedBy: 'SYSTEM' },
      }).catch(() => null);
    }

    await this.auditService.log({ action: 'CREATE', entityType: 'Ticket', entityId: booking.id, newData: { bookingRef: booking.bookingRef, totalTickets: tickets.length } });
    return { tickets, totalTickets: tickets.length };
  }

  async updateSeats(bookingId: string, seatIds: string[]) {
    const booking = await this.prisma.booking.findUnique({
      where: { id: bookingId },
      include: { tickets: true, seats: true } as any
    });
    if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);

    // Remove old seat blocks
    for (const ticket of (booking as any).tickets) {
      await this.prisma.seatBlock.deleteMany({
        where: { reason: { contains: ticket.id } }
      });
    }

    // Delete existing tickets
    await this.prisma.ticket.deleteMany({ where: { bookingId } });

    // Update booking seats
    await this.prisma.bookingSeat.deleteMany({ where: { bookingId } });
    
    // Create new seat assignments (simplified)
    for (let i = 0; i < seatIds.length; i++) {
      await this.prisma.bookingSeat.create({
        data: {
          bookingId,
          seatId: seatIds[i],
          passengerName: `Passenger ${i + 1}`,
          leg: 1
        } as any
      });
    }

    // Generate new tickets
    return this.generate(bookingId);
  }

  async getByMerchantOrderId(merchantOrderId: string) {
    const paymentIntent = await this.prisma.paymentIntent.findUnique({
      where: { merchantOrderId },
      include: { booking: { include: { tickets: true } as any } } as any
    });
    if (!paymentIntent) throw new NotFoundException('Payment not found');
    
    const booking = (paymentIntent as any).booking;
    if (!booking) throw new NotFoundException('Booking not found');
    
    return this.getByRef(booking.bookingRef);
  }

  async getByRef(ref: string) {
    const booking = await this.prisma.booking.findUnique({
      where: { bookingRef: ref },
      include: {
        tickets: true,
        schedule: { include: { originStation: true, destinationStation: true } },
        returnSchedule: { include: { originStation: true, destinationStation: true } },
        seats: { include: { seat: { include: { coach: true } } } }
      } as any
    });
    if (!booking) throw new NotFoundException('Booking not found');
    
    return {
      booking: {
        id: booking.id,
        bookingRef: booking.bookingRef,
        status: booking.status,
        bookingType: booking.bookingType,
        totalMinor: booking.totalMinor,
        currency: booking.currency
      },
      tickets: (booking as any).tickets,
      schedule: (booking as any).schedule,
      returnSchedule: (booking as any).returnSchedule,
      seats: (booking as any).seats
    };
  }

  async scanAndBoard(qrCodeOrRef: string, validatorId: string, gateId?: string) {
    try {
      // Extract booking reference from QR code if it's JSON
      let bookingRef = qrCodeOrRef;
      try {
        const qrData = JSON.parse(qrCodeOrRef);
        if (qrData.ref) bookingRef = qrData.ref;
      } catch {
        // Not JSON, treat as booking reference or ticket number
      }

      // Get booking and ticket info
      let booking = await this.prisma.booking.findUnique({
        where: { bookingRef },
        include: {
          schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
          returnSchedule: { include: { originStation: true, destinationStation: true } },
          tickets: true,
          seats: { include: { seat: { include: { coach: true } } } },
        },
      });

      if (!booking) {
        // Input may be a ticket number (barcodePayload) — look it up
        const ticket = await this.prisma.ticket.findFirst({ where: { barcodePayload: bookingRef } });
        if (ticket) {
          booking = await this.prisma.booking.findUnique({
            where: { bookingRef: ticket.bookingRef },
            include: {
              schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
              returnSchedule: { include: { originStation: true, destinationStation: true } },
              tickets: true,
              seats: { include: { seat: { include: { coach: true } } } },
            },
          });
          if (booking) bookingRef = (booking as any).bookingRef;
        }
      }

      if (!booking) {
        throw new NotFoundException('Ticket not found');
      }

      if (booking.status !== 'CONFIRMED') {
        throw new BadRequestException('Ticket is not confirmed');
      }

      const ticket = (booking as any).tickets[0];
      if (!ticket) {
        throw new NotFoundException('No ticket found for this booking');
      }

      // Check if ticket date matches today. Boarding window is relative to the
      // passenger's actual boarding stop, not the train's origin — for a mid-route
      // boarding these differ.
      const today = new Date();
      const boardingSegment = resolveBookingSegment((booking as any).schedule, (booking as any).originStationId, (booking as any).destinationStationId);

      if (boardingSegment.departureAt) {
        const departureTime = new Date(boardingSegment.departureAt);
        const boardingWindowHours = await this.systemConfig.getNumber(CONFIG_KEYS.BOARDING_WINDOW_HOURS_BEFORE_DEPARTURE);
        const boardingOpenTime = new Date(departureTime.getTime() - boardingWindowHours * 60 * 60 * 1000);

        if (today < boardingOpenTime) {
          throw new BadRequestException(
            `Boarding opens ${boardingWindowHours} hour(s) before departure at ${boardingOpenTime.toISOString()}`,
          );
        }

        if (today >= departureTime) {
          throw new BadRequestException('Boarding is closed — departure time has passed');
        }
      }

      // Use existing validation logic to handle round trips properly
      const result = await this.validate(bookingRef, validatorId, gateId);

      // Get seat information
      const seatInfo = (booking as any).seats[0];
      const seatNumber = seatInfo?.seat?.seatNumber || 'N/A';
      const coachNumber = seatInfo?.seat?.coach?.number || 'N/A';

      // Send notifications after successful boarding
      await this.sendBoardingNotifications(booking, ticket, result.leg || 'OUTBOUND');

      return {
        success: true,
        message: `Passenger boarded successfully (${result.leg || 'OUTBOUND'} leg)`,
        boarding: {
          ticketId: ticket.id,
          ticketNumber: ticket.barcodePayload,
          bookingRef: booking.bookingRef,
          passengerName: seatInfo?.passengerName || ticket.passengerName || 'N/A',
          route: `${boardingSegment.origin?.name || 'N/A'} → ${boardingSegment.destination?.name || 'N/A'}`,
          seat: seatNumber,
          coach: coachNumber,
          trainName: (booking as any).schedule?.train?.name || (booking as any).schedule?.train?.number || 'N/A',
          departureTime: boardingSegment.departureAt,
          boardedAt: result.validatedAt,
          leg: result.leg || 'OUTBOUND',
          bookingType: booking.bookingType,
          isRoundTrip: booking.bookingType === 'ROUND_TRIP' || booking.bookingType === 'ROUND_TRIP_TRANSIT',
        },
      };
    } catch (error) {
      // Return structured error for the UI
      const errorMessage = error instanceof Error ? error.message : 'Boarding failed';
      const errorCode = error instanceof BadRequestException ? 'VALIDATION_ERROR' 
        : error instanceof NotFoundException ? 'NOT_FOUND' 
        : 'SYSTEM_ERROR';
      
      return {
        success: false,
        error: errorMessage,
        errorCode,
      };
    }
  }

  private async sendBoardingNotifications(booking: any, ticket: any, leg: string) {
    try {
      const passengerName = booking.seats?.[0]?.passengerName || ticket.passengerName || 'Passenger';
      const contactEmail = booking.contactEmail;
      const contactPhone = booking.contactPhone;
      
      if (!contactEmail && !contactPhone) {
        this.logger.warn(`No contact details found for booking ${booking.bookingRef}`);
        return;
      }

      const routeInfo = `${booking.schedule?.originStation?.name} → ${booking.schedule?.destinationStation?.name}`;
      const trainName = booking.schedule?.train?.name || booking.schedule?.train?.number;
      const departureTime = booking.schedule?.departureAt ? new Date(booking.schedule.departureAt).toLocaleString() : 'N/A';
      const legText = leg === 'RETURN' ? 'Return' : 'Outbound';
      
      // Use the existing sendBoardingPassNotification method
      await this.notifications.sendBoardingPassNotification({
        passengerId: booking.passengerId || null,
        contactEmail,
        contactPhone,
        bookingRef: booking.bookingRef,
        leg,
        booking,
        ticket,
      });

    } catch (error: any) {
      this.logger.error('Error sending boarding notifications:', error);
    }
  }

  async validate(ticketIdOrRef: string, validatorId: string, gateId?: string, leg?: string) {
    // Accept either a ticket UUID or a bookingRef
    let bookingRef = ticketIdOrRef;
    const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(ticketIdOrRef);
    if (isUuid) {
      const ticket = await this.prisma.ticket.findUnique({ where: { id: ticketIdOrRef }, select: { bookingRef: true } });
      if (!ticket) throw new NotFoundException('Ticket not found');
      bookingRef = ticket.bookingRef;
    }
    const resolvedValidatorId = validatorId || 'BACKOFFICE';
    const booking = await this.prisma.booking.findUnique({ where: { bookingRef } });
    if (!booking) throw new NotFoundException('Booking not found');
    const ticket = await this.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
    if (!ticket) throw new NotFoundException('Ticket not found');

    const type = booking.bookingType;
    const now = new Date();

    // ── ONE_WAY / TRANSIT (single scan) ───────────────────────────────────
    if (type === 'ONE_WAY') {
      if (ticket.validatedAt) {
        return { validated: true, ticketId: ticket.id, validatedAt: ticket.validatedAt, alreadyValidated: true };
      }
      await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
      await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, status: 'APPROVED' } });
      this.fireBoardingPassNotification(booking, ticket, null);
      await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: 'ONE_WAY' } });
      return { validated: true, ticketId: ticket.id, validatedAt: now };
    }

    // ── TRANSIT — leg=LEG1 or leg=LEG2 ──────────────────────────────────
    if (type === 'TRANSIT') {
      const resolvedLeg = (leg ?? 'LEG1').toUpperCase();
      if (resolvedLeg !== 'LEG1' && resolvedLeg !== 'LEG2') {
        throw new BadRequestException('For TRANSIT bookings supply leg=LEG1 or leg=LEG2');
      }
      const logs = await this.prisma.gateValidationLog.findMany({ where: { ticketId: ticket.id, status: 'APPROVED' } }) as any[];
      const alreadyValidated = logs.some(l => l.leg === resolvedLeg);
      if (alreadyValidated) {
        await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: `${resolvedLeg}_ALREADY_USED` } as any });
        throw new BadRequestException(`${resolvedLeg} already validated`);
      }
      if (!ticket.validatedAt) await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
      await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
      this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
      await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: resolvedLeg } });
      return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now };
    }

    // ── ROUND_TRIP — leg=OUTBOUND or leg=RETURN ────────────────────────
    if (type === 'ROUND_TRIP') {
      let resolvedLeg = (leg ?? '').toUpperCase();
      // Auto-detect next unused leg when called from backoffice without a leg param
      if (!resolvedLeg) {
        resolvedLeg = !(booking as any).outboundBoardedAt ? 'OUTBOUND' : 'RETURN';
      }
      const bookingData: Record<string, any> = {};
      if (resolvedLeg === 'OUTBOUND') {
        if ((booking as any).outboundBoardedAt) {
          await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: 'OUTBOUND_ALREADY_USED' } as any });
          throw new BadRequestException('Outbound leg already validated');
        }
        bookingData.outboundBoardedAt = now;
      } else if (resolvedLeg === 'RETURN') {
        if ((booking as any).returnBoardedAt) {
          await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'REJECTED', reason: 'RETURN_ALREADY_USED' } as any });
          throw new BadRequestException('Return leg already validated');
        }
        bookingData.returnBoardedAt = now;
      } else {
        throw new BadRequestException('For ROUND_TRIP bookings supply leg=OUTBOUND or leg=RETURN');
      }
      
      await this.prisma.booking.update({ where: { id: booking.id }, data: bookingData });
      if (!ticket.validatedAt) {
        await this.prisma.ticket.update({ where: { id: ticket.id }, data: { validatedAt: now, boardedAt: now, validatorId: resolvedValidatorId } });
      }
      await this.prisma.gateValidationLog.create({ data: { ticketId: ticket.id, validatorId: resolvedValidatorId, gateId, leg: resolvedLeg, status: 'APPROVED' } as any });
      this.fireBoardingPassNotification(booking, ticket, resolvedLeg);
      await this.auditService.log({ action: 'VERIFY', entityType: 'Ticket', entityId: ticket.id, newData: { bookingRef, validatorId: resolvedValidatorId, leg: resolvedLeg } });
      return { validated: true, ticketId: ticket.id, leg: resolvedLeg, validatedAt: now };
    }

    throw new BadRequestException(`Unsupported booking type: ${type}`);
  }

  private async fireBoardingPassNotification(booking: any, ticket: any, leg: string | null) {
    this.logger.log(`Boarding pass notification for booking ${booking.bookingRef}, leg: ${leg}`);
  }

  async getValidationLogs(ticketId: string) {
    return this.prisma.gateValidationLog.findMany({
      where: { ticketId },
      orderBy: { validatedAt: 'desc' }
    });
  }

  async exportOfflineData(tripId: string) {
    const bookings = await this.prisma.booking.findMany({
      where: { scheduleId: tripId, status: 'CONFIRMED' },
      include: {
        tickets: true,
        seats: { include: { seat: { include: { coach: true } } } },
        passenger: { select: { id: true, iamUserId: true } },
      } as any,
    });

    return bookings.map((b) => ({
      bookingRef: b.bookingRef,
      ticketId: (b as any).tickets?.[0]?.id,
      passengerName: (b as any).seats[0]?.passengerName,
      seatLabel: (b as any).seats[0]?.seat.seatNumber,
      coachLabel: (b as any).seats[0]?.seat.coach.number,
      qrPayload: (b as any).tickets?.[0]?.qrPayload,
      status: b.status,
      bookingType: b.bookingType,
      returnLegStatus: (b as any).returnLegStatus ?? null,
      validatedAt: (b as any).tickets?.[0]?.validatedAt,
    }));
  }

  async validateOfflineBatch(validations: OfflineValidation[]) {
    const results = [];
    
    for (const validation of validations) {
      try {
        const result = await this.validate(
          validation.bookingRef,
          validation.validatorId,
          validation.gateId,
          validation.leg
        );
        results.push({ 
          bookingRef: validation.bookingRef, 
          success: true, 
          result 
        });
      } catch (error) {
        results.push({ 
          bookingRef: validation.bookingRef, 
          success: false, 
          error: error instanceof Error ? error.message : 'Validation failed'
        });
      }
    }
    
    return {
      processed: results.length,
      successful: results.filter(r => r.success).length,
      failed: results.filter(r => !r.success).length,
      results
    };
  }

  async generateMissing(limit = 10): Promise<{ processed: number; generated: number; failed: number; remaining: number; details: any[] }> {
    const missingWhere = {
      status: 'CONFIRMED' as const,
      tickets: { none: {} },
      paymentIntent: { status: 'SUCCEEDED' as const },
    };

    const [confirmedWithNoTickets, totalRemaining] = await Promise.all([
      this.prisma.booking.findMany({
        where: missingWhere,
        select: { id: true, bookingRef: true },
        take: limit,
      }),
      this.prisma.booking.count({ where: missingWhere }),
    ]);

    const details: any[] = [];
    let generated = 0;
    let failed = 0;

    for (const booking of confirmedWithNoTickets) {
      try {
        await this.smartAssignAndGenerate(booking.id);
        generated++;
        details.push({ bookingId: booking.id, bookingRef: booking.bookingRef, status: 'generated' });
      } catch (err) {
        failed++;
        details.push({ bookingId: booking.id, bookingRef: booking.bookingRef, status: 'failed', error: err instanceof Error ? err.message : String(err) });
      }
    }

    return {
      processed: confirmedWithNoTickets.length,
      generated,
      failed,
      remaining: Math.max(0, totalRemaining - confirmedWithNoTickets.length),
      details,
    };
  }

  async delete(id: string) {
    const ticket = await this.prisma.ticket.findUnique({ where: { id } });
    if (!ticket) throw new NotFoundException('Ticket not found');
    
    await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: id } });
    
    // Remove seat blocks associated with this ticket
    await this.prisma.seatBlock.deleteMany({
      where: { 
        reason: { contains: id }
      }
    });
    
    await this.prisma.ticket.delete({ where: { id } });
    
    return { deleted: true, ticketId: id };
  }

  async restore(id: string) {
    const ticket = await this.prisma.ticket.findUnique({ where: { id } });
    if (!ticket) throw new NotFoundException('Ticket not found');
    return this.prisma.ticket.update({ where: { id }, data: { status: 'ACTIVE' } });
  }
}
