import { Injectable, Logger } from "@nestjs/common";
import { InjectDataSource } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { PrismaService } from "../../common/prisma.service";
import { GenerateReportDto, ReportType } from "./reports.dto";

@Injectable()
export class ReportsService {
  private readonly logger = new Logger(ReportsService.name);
  constructor(
    private prisma: PrismaService,
    @InjectDataSource() private dataSource: DataSource,
  ) {}

  async generateReport(dto: GenerateReportDto) {
    const dateFrom = new Date(dto.dateFrom);
    dateFrom.setHours(0, 0, 0, 0);

    const dateTo = new Date(dto.dateTo);
    dateTo.setHours(23, 59, 59, 999);

    let data: any;
    switch (dto.reportType) {
      case ReportType.REVENUE:
        data = await this.generateRevenueReport(dateFrom, dateTo);
        break;
      case ReportType.OCCUPANCY:
        data = await this.generateOccupancyReport(dateFrom, dateTo);
        break;
      case ReportType.AGENT_SALES:
        data = await this.generateAgentSalesReport(
          dateFrom,
          dateTo,
          dto.agentId,
        );
        break;
      case ReportType.CANCELLATIONS:
        data = await this.generateCancellationsReport(dateFrom, dateTo);
        break;
      case ReportType.PAYMENT_METHODS:
        data = await this.generatePaymentMethodsReport(dateFrom, dateTo);
        break;
      default:
        data = {};
    }

    const report = await this.prisma.operationalReport.create({
      data: {
        reportType: dto.reportType,
        dateFrom,
        dateTo,
        data,
      },
    });

    return { reportId: report.id, reportType: dto.reportType, data };
  }

  private async generateRevenueReport(dateFrom: Date, dateTo: Date) {
    // Fetch all bookings in date range, regardless of status
    const bookings = await this.prisma.booking.findMany({
      where: {
        createdAt: { gte: dateFrom, lte: dateTo },
      },
      include: { paymentIntent: true },
    });

    const totalRevenue = bookings.reduce((sum, b) => sum + b.totalMinor, 0);
    const byPaymentMethod = bookings.reduce(
      (acc, b) => {
        const method = b.paymentIntent?.method ?? "UNKNOWN";
        acc[method] = (acc[method] || 0) + b.totalMinor;
        return acc;
      },
      {} as Record<string, number>,
    );

    // Group by date for charts
    const byDate = bookings.reduce(
      (acc, b) => {
        const date = b.createdAt.toISOString().split("T")[0];
        if (!acc[date]) {
          acc[date] = { totalMinor: 0, count: 0 };
        }
        acc[date].totalMinor += b.totalMinor;
        acc[date].count += 1;
        return acc;
      },
      {} as Record<string, any>,
    );

    return {
      totalBookings: bookings.length,
      totalRevenueMinor: totalRevenue,
      totalRevenue: totalRevenue / 100,
      currency: "ETB",
      byPaymentMethod,
      byDate,
      cancellationRate: 0,
    };
  }

  private async generateOccupancyReport(dateFrom: Date, dateTo: Date) {
    const schedules = await this.prisma.trainSchedule.findMany({
      where: { departureAt: { gte: dateFrom, lte: dateTo } },
      include: {
        coachAssignments: { include: { coach: { include: { seats: true } } } },
        bookings: {
          where: { status: { in: ["CONFIRMED", "BOARDED"] } },
          include: { seats: true },
        },
      },
    });

    const tripData = schedules.map((schedule) => {
      const totalSeats = schedule.coachAssignments.reduce(
        (sum, a) => sum + a.coach.seats.length,
        0,
      );
      const bookedSeats = schedule.bookings.reduce(
        (sum, b) =>
          sum + b.seats.filter((s: any) => s.scheduleId === schedule.id).length,
        0,
      );
      const occupancyRate =
        totalSeats > 0 ? (bookedSeats / totalSeats) * 100 : 0;
      return {
        scheduleId: schedule.id,
        departureAt: schedule.departureAt,
        totalSeats,
        bookedSeats,
        occupancyRate: +occupancyRate.toFixed(2),
      };
    });

    const avgOccupancy =
      tripData.length > 0
        ? tripData.reduce((sum, t) => sum + t.occupancyRate, 0) /
          tripData.length
        : 0;
    return {
      totalSchedules: schedules.length,
      averageOccupancyRate: +avgOccupancy.toFixed(2),
      schedules: tripData,
    };
  }

  private async generateAgentSalesReport(
    dateFrom: Date,
    dateTo: Date,
    agentId?: string,
  ) {
    const agentBookings = await this.prisma.agentBooking.findMany({
      where: {
        createdAt: { gte: dateFrom, lte: dateTo },
        ...(agentId ? { agentId } : {}),
      },
      include: {
        agent: { select: { id: true, iamUserId: true, agentCode: true } },
        booking: true,
      },
    });

    const iamUserIds = [
      ...new Set(
        agentBookings
          .map((ab) => ab.agent.iamUserId)
          .filter(Boolean) as string[],
      ),
    ];
    const iamRows =
      iamUserIds.length > 0
        ? await this.dataSource.query<
            { id: string; name: { en?: string; am?: string } | null }[]
          >(`SELECT id, name FROM iam.users WHERE id = ANY($1)`, [iamUserIds])
        : [];
    const iamMap = new Map(iamRows.map((r) => [r.id, r]));

    const byAgent = agentBookings.reduce(
      (acc, ab) => {
        const iam = ab.agent.iamUserId
          ? iamMap.get(ab.agent.iamUserId)
          : undefined;
        const agentName = iam?.name?.en ?? iam?.name?.am ?? ab.agent.agentCode;
        if (!acc[agentName]) {
          acc[agentName] = { bookings: 0, revenueMinor: 0, cashCollected: 0 };
        }
        acc[agentName].bookings += 1;
        acc[agentName].revenueMinor += ab.booking.totalMinor;
        acc[agentName].cashCollected += ab.cashReceived ?? 0;
        return acc;
      },
      {} as Record<string, any>,
    );

    return {
      totalAgentBookings: agentBookings.length,
      byAgent,
    };
  }

  private async generateCancellationsReport(dateFrom: Date, dateTo: Date) {
    const cancellations = await this.prisma.bookingCancellation.findMany({
      where: { createdAt: { gte: dateFrom, lte: dateTo } },
      include: { booking: true },
    });

    const totalRefunded = cancellations.reduce(
      (sum, c) => sum + c.refundAmount,
      0,
    );

    return {
      totalCancellations: cancellations.length,
      totalRefundedMinor: totalRefunded,
      totalRefunded: totalRefunded / 100,
      currency: "ETB",
    };
  }

  private async generatePaymentMethodsReport(dateFrom: Date, dateTo: Date) {
    const payments = await this.prisma.paymentIntent.findMany({
      where: {
        createdAt: { gte: dateFrom, lte: dateTo },
        status: "SUCCEEDED",
      },
    });

    const byMethod = payments.reduce(
      (acc, p) => {
        const method = p.method;
        if (!acc[method]) {
          acc[method] = { count: 0, totalMinor: 0 };
        }
        acc[method].count += 1;
        acc[method].totalMinor += p.amountMinor;
        return acc;
      },
      {} as Record<string, any>,
    );

    return {
      totalPayments: payments.length,
      byMethod,
    };
  }

  async getOccupancyBySchedule(scheduleId: string) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      include: {
        originStation: true,
        destinationStation: true,
        train: true,
        coachAssignments: {
          include: {
            coach: {
              include: {
                coachType: true,
                seats: { select: { id: true } },
              },
            },
          },
        },
        bookings: {
          where: { status: { in: ['CONFIRMED', 'BOARDED'] } },
          select: { id: true, originStationId: true, destinationStationId: true },
        },
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
      },
    });

    if (!schedule) return null;

    // Fetch booking seats for this schedule — covers:
    //   • outbound seats (leg=1, scheduleId=scheduleId)
    //   • return seats (leg=2, booking.returnScheduleId=scheduleId)
    //   • legacy rows where scheduleId is null but booking.scheduleId matches
    const allBookingSeats = await this.prisma.bookingSeat.findMany({
      where: {
        OR: [
          { scheduleId, booking: { status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { leg: 2, booking: { returnScheduleId: scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { scheduleId: null, leg: 1, booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
        ],
      },
      select: {
        bookingId: true,
        seat: { select: { coachId: true, coach: { select: { coachType: { select: { name: true } } } } } },
      },
    });

    const totalSeats = schedule.coachAssignments.reduce((s, a) => s + a.coach.seats.length, 0);
    const totalPassengers = allBookingSeats.length;
    const occupancyRate = totalSeats > 0 ? +((totalPassengers / totalSeats) * 100).toFixed(1) : 0;

    // Per-coach breakdown
    const coachMap = new Map<string, { coachNumber: string; coachType: string; totalSeats: number; booked: number }>();
    for (const assignment of schedule.coachAssignments) {
      const c = assignment.coach;
      coachMap.set(c.id, { coachNumber: c.number, coachType: (c as any).coachType?.name ?? 'Unknown', totalSeats: c.seats.length, booked: 0 });
    }
    for (const bs of allBookingSeats) {
      const coachId = bs.seat?.coachId;
      if (coachId && coachMap.has(coachId)) coachMap.get(coachId)!.booked++;
    }
    const byCoach = [...coachMap.values()].map(c => ({ ...c, occupancyRate: c.totalSeats > 0 ? +((c.booked / c.totalSeats) * 100).toFixed(1) : 0 }));

    // Per-class breakdown
    const classMap = new Map<string, { className: string; totalSeats: number; booked: number }>();
    for (const assignment of schedule.coachAssignments) {
      const typeName = (assignment.coach as any).coachType?.name ?? 'Unknown';
      if (!classMap.has(typeName)) classMap.set(typeName, { className: typeName, totalSeats: 0, booked: 0 });
      classMap.get(typeName)!.totalSeats += assignment.coach.seats.length;
    }
    for (const bs of allBookingSeats) {
      const typeName = bs.seat?.coach?.coachType?.name ?? 'Unknown';
      if (!classMap.has(typeName)) classMap.set(typeName, { className: typeName, totalSeats: 0, booked: 0 });
      classMap.get(typeName)!.booked++;
    }
    const byClass = [...classMap.values()].map(c => ({ ...c, occupancyRate: c.totalSeats > 0 ? +((c.booked / c.totalSeats) * 100).toFixed(1) : 0 }));

    // Per-origin / per-destination — count actual seats per booking from allBookingSeats
    const seatCountByBooking = allBookingSeats.reduce((acc, bs) => { acc[bs.bookingId] = (acc[bs.bookingId] ?? 0) + 1; return acc; }, {} as Record<string, number>);
    const originMap = new Map<string, { stationName: string; passengers: number }>();
    const destMap = new Map<string, { stationName: string; passengers: number }>();
    for (const booking of schedule.bookings) {
      const count = seatCountByBooking[booking.id] ?? 0;
      const oId = booking.originStationId ?? schedule.originStationId;
      const dId = booking.destinationStationId ?? schedule.destinationStationId;
      const oName = schedule.stopTimes.find(st => st.stationId === oId)?.station?.name ?? (schedule as any).originStation?.name ?? oId;
      const dName = schedule.stopTimes.find(st => st.stationId === dId)?.station?.name ?? (schedule as any).destinationStation?.name ?? dId;
      if (!originMap.has(oId)) originMap.set(oId, { stationName: oName, passengers: 0 });
      originMap.get(oId)!.passengers += count;
      if (!destMap.has(dId)) destMap.set(dId, { stationName: dName, passengers: 0 });
      destMap.get(dId)!.passengers += count;
    }
    const byOrigin = [...originMap.values()].sort((a, b) => b.passengers - a.passengers);
    const byDestination = [...destMap.values()].sort((a, b) => b.passengers - a.passengers);

    return {
      schedule: {
        id: schedule.id,
        trainName: (schedule as any).train?.name ?? (schedule as any).train?.number,
        origin: (schedule as any).originStation?.name,
        destination: (schedule as any).destinationStation?.name,
        departureAt: schedule.departureAt,
        arrivalAt: schedule.arrivalAt,
      },
      summary: { totalSeats, totalPassengers, occupancyRate },
      byCoach,
      byClass,
      byOrigin,
      byDestination,
    };
  }

  async listSchedulesForPicker(all = false) {
    const now = new Date();
    const schedules = await this.prisma.trainSchedule.findMany({
      where: all ? undefined : { departureAt: { gte: now } },
      select: {
        id: true,
        departureAt: true,
        isPackageOnly: true,
        train: { select: { number: true } },
        originStation: { select: { name: true } },
        destinationStation: { select: { name: true } },
      },
      orderBy: { departureAt: all ? 'desc' : 'asc' },
      take: 200,
    });
    return schedules.map((s) => ({
      id: s.id,
      departureAt: s.departureAt,
      isPackage: s.isPackageOnly,
      label: `${s.train.number} · ${s.originStation.name} → ${s.destinationStation.name} · ${new Date(s.departureAt).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' })}${
        s.isPackageOnly ? ' (package)' : ''
      }`,
    }));
  }

  async getPassengerList(scheduleId: string) {
    const seats = await this.prisma.bookingSeat.findMany({
      where: {
        OR: [
          { scheduleId, booking: { status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { leg: 2, booking: { returnScheduleId: scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { scheduleId: null, leg: 1, booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
        ],
      },
      include: {
        booking: {
          select: {
            bookingRef: true,
            status: true,
            originStationId: true,
            destinationStationId: true,
            totalMinor: true,
            currency: true,
            _count: { select: { seats: true } },
          },
        },
        seat: { select: { seatNumber: true, bedPosition: true, coach: { select: { number: true, coachType: { select: { name: true, seatClasses: { select: { name: true, bedPosition: true } } } } } } } },
      },
      orderBy: [{ seat: { coach: { number: "asc" } } }, { seat: { seatNumber: "asc" } }],
    });

    const stationIds = [...new Set(
      seats.flatMap(bs => [bs.booking.originStationId, bs.booking.destinationStationId]).filter(Boolean) as string[],
    )];
    const stations = stationIds.length > 0
      ? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } })
      : [];
    const stationName = new Map(stations.map(s => [s.id, s.name]));

    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      select: { departureAt: true },
    });

    return seats.map((bs) => ({
      bookingRef: bs.booking.bookingRef,
      passengerName: bs.passengerName,
      passengerCategory: bs.passengerCategory,
      idDocumentType: bs.idDocumentType,
      idDocumentNumber: bs.idDocumentNumber,
      passportNumber: bs.passportNumber,
      passportCountry: bs.passportCountry,
      seatLabel: bs.seatLabelSnapshot,
      seatNumber: bs.seat?.seatNumber ?? null,
      seatClassName: (() => {
        const classes = bs.seat?.coach?.coachType?.seatClasses ?? [];
        const matched = bs.seat?.bedPosition
          ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === bs.seat!.bedPosition!.toLowerCase())
          : null;
        return (matched ?? classes[0])?.name ?? bs.seat?.coach?.coachType?.name ?? null;
      })(),
      coachNumber: bs.seat?.coach?.number ?? null,
      coachType: bs.seat?.coach?.coachType?.name ?? null,
      nationality: bs.passportCountry
        ? (bs.passportCountry === 'Djibouti' ? 'Djiboutian' : bs.passportCountry)
        : bs.idDocumentType === 'NATIONAL_ID' ? 'Ethiopian' : null,
      origin: bs.booking.originStationId ? (stationName.get(bs.booking.originStationId) ?? null) : null,
      destination: bs.booking.destinationStationId ? (stationName.get(bs.booking.destinationStationId) ?? null) : null,
      amountPaidMinor: bs.booking.totalMinor,
      currency: bs.booking.currency ?? 'ETB',
      isGroupBooking: (bs.booking._count?.seats ?? 0) > 1,
      bookingStatus: bs.booking.status,
    }));
  }

  async getSeatStatusReport(scheduleId: string) {
    // Confirmed/boarded seats — exclude dining coaches
    const bookingSeats = await this.prisma.bookingSeat.findMany({
      where: {
        OR: [
          { scheduleId, booking: { status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } } },
          { leg: 2, booking: { returnScheduleId: scheduleId, status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } } },
          { scheduleId: null, leg: 1, booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED', 'PENDING_PAYMENT'] } } },
        ],
        seat: { coach: { coachType: { type: { not: 'dining' } } } },
      },
      include: {
        booking: {
          select: {
            bookingRef: true,
            status: true,
            totalMinor: true,
            currency: true,
            createdAt: true,
            paymentIntent: { select: { status: true } },
          },
        },
        seat: {
          select: {
            seatNumber: true,
            bedPosition: true,
            coach: {
              select: {
                number: true,
                coachType: { select: { name: true, type: true, seatClasses: { select: { name: true, bedPosition: true } } } },
              },
            },
          },
        },
      },
      orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }],
    });

    // Active seat holds for this schedule
    const activeHolds = await this.prisma.seatHold.findMany({
      where: { scheduleId },
      orderBy: { createdAt: 'desc' },
    });

    // Expired holds (last 24h) — held but never converted to a booking
    const since24h = new Date(Date.now() - 24 * 60 * 60 * 1000);
    const expiredHolds = await this.prisma.seatHold.findMany({
      where: {
        scheduleId,
        expiresAt: { lt: new Date(), gte: since24h },
      },
      orderBy: { expiresAt: 'desc' },
    });

    // Manually blocked seats — schedule-scoped blocks for this schedule OR global blocks (scheduleId null)
    // Exclude MAINTENANCE and booking-system-created blocks
    const blocks = await this.prisma.seatBlock.findMany({
      where: {
        OR: [
          { scheduleId },
          { scheduleId: null },
        ],
        NOT: [
          { reason: { startsWith: 'MAINTENANCE:' } },
          { reason: { startsWith: 'Booked in tickets' } },
        ],
      },
      include: {
        seat: {
          select: {
            seatNumber: true,
            bedPosition: true,
            coach: {
              select: {
                number: true,
                coachType: { select: { name: true, type: true, seatClasses: { select: { name: true, bedPosition: true } } } },
              },
            },
          },
        },
      },
      orderBy: { blockedAt: 'desc' },
    });

    const resolveSeatClass = (seat: any): string | null => {
      const classes = seat?.coach?.coachType?.seatClasses ?? [];
      const matched = seat?.bedPosition
        ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === seat.bedPosition.toLowerCase())
        : null;
      return (matched ?? classes[0])?.name ?? seat?.coach?.coachType?.name ?? null;
    };

    const paidSeats = bookingSeats.filter(bs =>
      bs.booking.status === 'CONFIRMED' || bs.booking.status === 'BOARDED'
    );
    const unpaidSeats = bookingSeats.filter(bs =>
      bs.booking.status === 'PENDING_PAYMENT'
    );

    const mapSeat = (bs: any) => ({
      bookingRef: bs.booking.bookingRef,
      passengerName: bs.passengerName,
      passengerCategory: bs.passengerCategory,
      coachNumber: bs.seat?.coach?.number ?? null,
      seatNumber: bs.seat?.seatNumber ?? null,
      seatClassName: resolveSeatClass(bs.seat),
      fareMinor: bs.fareMinor,
      currency: bs.booking.currency ?? 'ETB',
      bookingStatus: bs.booking.status,
      paymentStatus: bs.booking.paymentIntent?.status ?? 'PENDING',
      bookedAt: bs.booking.createdAt,
    });

    return {
      summary: {
        paidCount: paidSeats.length,
        unpaidCount: unpaidSeats.length,
        expiredHoldCount: expiredHolds.length,
        blockedCount: blocks.filter(b => b.seat?.coach?.coachType?.type !== 'dining').length,
      },
      paidSeats: paidSeats.map(mapSeat),
      unpaidSeats: unpaidSeats.map(mapSeat),
      expiredHolds: expiredHolds.map(h => ({
        holdId: h.id,
        seatIds: h.seatIds,
        expiresAt: h.expiresAt,
        createdAt: h.createdAt,
      })),
      blockedSeats: blocks
        .filter(b => b.seat?.coach?.coachType?.type !== 'dining')
        .map(b => ({
          id: b.id,
          coachNumber: b.seat?.coach?.number ?? null,
          seatNumber: b.seat?.seatNumber ?? null,
          seatClassName: resolveSeatClass(b.seat),
          reason: b.reason,
          blockedBy: b.blockedBy,
          blockedAt: b.blockedAt,
          unblockAt: b.unblockAt,
        })),
    };
  }

  async getPaymentDiscrepancyReport(params: {
    from?: string;
    to?: string;
    sortBy?: string;
    search?: string;
  }) {
    // Load all exchange rates once — we need conversions in both directions.
    const rateRows = await this.prisma.currencyExchangeRate.findMany({
      orderBy: { effectiveDate: 'desc' },
    });
    // Most-recent rate for each fromCurrency→toCurrency pair
    const rateMap = new Map<string, number>();
    for (const r of rateRows) {
      const key = `${r.fromCurrency}→${r.toCurrency}`;
      if (!rateMap.has(key)) rateMap.set(key, Number(r.rate));
    }

    // Convert minor amount from one currency to another.
    const convertMinor = (minor: number, from: string, to: string): number => {
      if (from === to) return minor;
      const direct = rateMap.get(`${from}→${to}`);
      if (direct) return Math.round(minor * direct);
      // Try via ETB as pivot
      const toEtb = rateMap.get(`${from}→ETB`);
      const fromEtb = rateMap.get(`ETB→${to}`);
      if (toEtb && fromEtb) return Math.round(minor * toEtb * fromEtb);
      return minor; // fallback: no rate on file
    };

    if (params.search?.trim()) {
      return this.getDiscrepancyForRef(params.search.trim(), convertMinor);
    }

    const dateFilter: Record<string, Date> = {};
    if (params.from) dateFilter.gte = new Date(params.from + 'T00:00:00.000Z');
    if (params.to) dateFilter.lte = new Date(params.to + 'T23:59:59.999Z');

    const seatSelect = {
      where: { leg: 1 },
      orderBy: [
        { seat: { coach: { number: 'asc' as const } } },
        { seat: { seatNumber: 'asc' as const } },
      ],
      select: {
        passengerName: true,
        passengerCategory: true,
        seatLabelSnapshot: true,
        fareMinor: true,
        displayFareMinor: true,
        displayCurrency: true,
        seat: {
          select: {
            seatNumber: true,
            coach: { select: { number: true, coachType: { select: { name: true } } } },
          },
        },
      },
    };

    const bookings = await this.prisma.booking.findMany({
      where: {
        status: { in: ['CONFIRMED', 'BOARDED', 'NO_SHOW'] as any },
        paymentIntent: { status: 'SUCCEEDED' },
        ...(Object.keys(dateFilter).length > 0 && { createdAt: dateFilter }),
      },
      include: {
        paymentIntent: { select: { amountMinor: true, currency: true, paidAt: true } },
        schedule: {
          include: {
            originStation: { select: { name: true, code: true, city: true } },
            destinationStation: { select: { name: true, code: true, city: true } },
          },
        },
        seats: seatSelect,
        passenger: {
          select: { user: { select: { phone: true, fullName: true } } },
        },
      },
      orderBy: { createdAt: 'desc' },
    });

    const rows = bookings
      .map(b => {
        const pi = b.paymentIntent!;

        // Display amounts shown to the passenger (may be in DJF/USD).
        const actualMinor = b.displayTotalMinor ?? b.totalMinor;
        const actualCurrency = (b.displayCurrency as string | null) ?? b.currency;

        const paidMinor = pi.amountMinor;
        const paidCurrency = pi.currency;

        // Balance in the booking's display currency:
        // convert paid (major units from gateway) to display currency minor, then subtract.
        const paidInDisplayMinor = convertMinor(paidMinor * 100, paidCurrency, actualCurrency);
        const balanceMinor = actualMinor - paidInDisplayMinor;
        const balanceCurrency = actualCurrency;

        const firstSeat = b.seats[0];
        const passengers = this.buildSeatPassengers(b.seats, actualCurrency);
        return {
          pnr: b.bookingRef,
          passengerName: firstSeat?.passengerName ?? b.passenger?.user?.fullName ?? '—',
          phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
          bookingDate: b.createdAt,
          origin: b.schedule.originStation,
          destination: b.schedule.destinationStation,
          departureAt: b.schedule.departureAt,
          seatType: firstSeat?.seatLabelSnapshot ?? firstSeat?.seat?.coach?.coachType?.name ?? '—',
          coachNumber: firstSeat?.seat?.coach?.number ?? null,
          actualMinor,
          actualCurrency,
          paidMinor,
          paidCurrency,
          balanceMinor,
          balanceCurrency,
          passengerCount: passengers.length,
          passengers,
        };
      })
      .filter(r => r.balanceMinor > 0);

    if (params.sortBy === 'departure') {
      rows.sort((a, b) => new Date(a.departureAt).getTime() - new Date(b.departureAt).getTime());
    } else {
      rows.sort((a, b) => b.balanceMinor - a.balanceMinor);
    }

    const totalBalanceEtbMinor = rows.reduce((sum, r) => sum + r.balanceMinor, 0);

    return { total: rows.length, totalBalanceEtbMinor, rows };
  }

  private async getDiscrepancyForRef(
    search: string,
    convertMinor: (minor: number, from: string, to: string) => number,
  ) {
    let bookingId: string | null = null;
    const byPnr = await this.prisma.booking.findUnique({
      where: { bookingRef: search.toUpperCase() },
      select: { id: true },
    });
    if (byPnr) {
      bookingId = byPnr.id;
    } else {
      const ticket = await this.prisma.ticket.findFirst({
        where: { barcodePayload: search },
        select: { bookingId: true },
      });
      bookingId = ticket?.bookingId ?? null;
    }

    if (!bookingId) {
      return { total: 0, totalBalanceEtbMinor: 0, rows: [], notFound: true };
    }

    const b = await this.prisma.booking.findUnique({
      where: { id: bookingId },
      include: {
        paymentIntent: { select: { amountMinor: true, currency: true, paidAt: true, status: true } },
        schedule: {
          include: {
            originStation: { select: { name: true, code: true, city: true } },
            destinationStation: { select: { name: true, code: true, city: true } },
          },
        },
        seats: {
          where: { leg: 1 },
          orderBy: [
            { seat: { coach: { number: 'asc' } } },
            { seat: { seatNumber: 'asc' } },
          ],
          select: {
            passengerName: true,
            passengerCategory: true,
            seatLabelSnapshot: true,
            fareMinor: true,
            displayFareMinor: true,
            displayCurrency: true,
            seat: {
              select: {
                seatNumber: true,
                coach: { select: { number: true, coachType: { select: { name: true } } } },
              },
            },
          },
        },
        passenger: {
          select: { user: { select: { phone: true, fullName: true } } },
        },
      },
    });

    if (!b) return { total: 0, totalBalanceEtbMinor: 0, rows: [], notFound: true };

    const pi = b.paymentIntent;
    const actualMinor = b.displayTotalMinor ?? b.totalMinor;
    const actualCurrency = (b.displayCurrency as string | null) ?? b.currency;
    const paidMinor = pi?.amountMinor ?? 0;
    const paidCurrency = pi?.currency ?? b.currency;

    const paidInDisplayMinor = convertMinor(paidMinor * 100, paidCurrency, actualCurrency);
    const balanceMinor = actualMinor - paidInDisplayMinor;
    const balanceCurrency = actualCurrency;

    const firstSeat = b.seats[0];
    const passengers = this.buildSeatPassengers(b.seats, actualCurrency);
    const row = {
      pnr: b.bookingRef,
      passengerName: firstSeat?.passengerName ?? b.passenger?.user?.fullName ?? '—',
      phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
      bookingDate: b.createdAt,
      origin: b.schedule.originStation,
      destination: b.schedule.destinationStation,
      departureAt: b.schedule.departureAt,
      seatType: firstSeat?.seatLabelSnapshot ?? firstSeat?.seat?.coach?.coachType?.name ?? '—',
      coachNumber: firstSeat?.seat?.coach?.number ?? null,
      actualMinor,
      actualCurrency,
      paidMinor,
      paidCurrency,
      balanceMinor,
      balanceCurrency,
      bookingStatus: b.status,
      paymentStatus: pi?.status ?? null,
      passengerCount: passengers.length,
      passengers,
    };

    return {
      total: balanceMinor > 0 ? 1 : 0,
      totalBalanceEtbMinor: balanceMinor > 0 ? balanceMinor : 0,
      rows: [row],
      notFound: false,
    };
  }

  private buildSeatPassengers(seats: any[], fallbackCurrency: string) {
    return seats.map(s => ({
      name: s.passengerName as string,
      category: s.passengerCategory as string,
      seatNumber: (s.seat?.seatNumber ?? null) as string | null,
      coachNumber: (s.seat?.coach?.number ?? null) as string | null,
      seatType: (s.seatLabelSnapshot ?? s.seat?.coach?.coachType?.name ?? null) as string | null,
      fareMinor: (s.displayFareMinor ?? s.fareMinor ?? null) as number | null,
      fareCurrency: ((s.displayCurrency as string | null) ?? fallbackCurrency),
    }));
  }

  async getPaymentsReport(scheduleId: string) {
    const rateRows = await this.prisma.currencyExchangeRate.findMany({
      where: { toCurrency: 'ETB' as any },
      orderBy: { effectiveDate: 'desc' },
    });
    const rateToEtb = new Map<string, number>();
    for (const r of rateRows) {
      if (!rateToEtb.has(r.fromCurrency)) rateToEtb.set(r.fromCurrency, Number(r.rate));
    }
    const toEtbMinor = (minor: number, currency: string): number => {
      if (currency === 'ETB') return minor;
      const rate = rateToEtb.get(currency);
      return rate ? Math.round(minor * rate) : minor;
    };

    const bookings = await this.prisma.booking.findMany({
      where: {
        scheduleId,
        status: { in: ['CONFIRMED', 'BOARDED', 'NO_SHOW'] as any },
        paymentIntent: { status: 'SUCCEEDED' },
      },
      include: {
        paymentIntent: { select: { amountMinor: true, currency: true, method: true, paidAt: true } },
        seats: {
          where: { leg: 1 },
          select: {
            passengerName: true,
            fareMinor: true,
            displayFareMinor: true,
            displayCurrency: true,
            passengerCategory: true,
            seatLabelSnapshot: true,
            seat: { select: { coach: { select: { number: true, coachType: { select: { name: true } } } } } },
          },
        },
        passenger: { select: { user: { select: { phone: true, fullName: true } } } },
      },
    });

    const rows = bookings.map(b => {
      const pi = b.paymentIntent!;
      const actualMinor = b.displayTotalMinor ?? b.totalMinor;
      const actualCurrency = (b.displayCurrency as string | null) ?? b.currency;
      // pi.amountMinor is stored in major units — convert to minor
      const paidMinor = Math.round(pi.amountMinor * 100);
      const paidCurrency = pi.currency;
      const varianceMinor = toEtbMinor(actualMinor, actualCurrency) - toEtbMinor(paidMinor, paidCurrency);
      return {
        bookingRef: b.bookingRef,
        passengerName: b.seats[0]?.passengerName ?? b.passenger?.user?.fullName ?? '—',
        phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
        method: pi.method,
        paidAt: pi.paidAt,
        actualMinor,
        actualCurrency,
        paidMinor,
        paidCurrency,
        varianceMinor,
        passengerCount: b.seats.length,
      };
    });

    const totalActualEtbMinor = rows.reduce((s, r) => s + toEtbMinor(r.actualMinor, r.actualCurrency), 0);
    const totalPaidEtbMinor = rows.reduce((s, r) => s + toEtbMinor(r.paidMinor, r.paidCurrency), 0);

    const byMethod = rows.reduce((acc, r) => {
      if (!acc[r.method]) acc[r.method] = { totalPaidEtbMinor: 0, currency: 'ETB' };
      acc[r.method].totalPaidEtbMinor += toEtbMinor(r.paidMinor, r.paidCurrency);
      return acc;
    }, {} as Record<string, { totalPaidEtbMinor: number; currency: string }>);

    return { totalActualEtbMinor, totalPaidEtbMinor, byMethod, rows };
  }

  async getPaymentDiscrepancyBySchedule(scheduleId: string, params: {
    search?: string;
    seatClass?: string;
    sort?: string;
  }) {
    const bookings = await this.prisma.booking.findMany({
      where: {
        scheduleId,
        status: { in: ['CONFIRMED', 'BOARDED', 'NO_SHOW'] as any },
        paymentIntent: { status: 'SUCCEEDED' },
      },
      include: {
        paymentIntent: { select: { amountMinor: true, currency: true } },
        schedule: { select: { id: true } },
        seats: {
          where: { leg: 1 },
          orderBy: [
            { seat: { coach: { number: 'asc' as const } } },
            { seat: { seatNumber: 'asc' as const } },
          ],
          select: {
            passengerName: true,
            passengerCategory: true,
            seatLabelSnapshot: true,
            fareMinor: true,
            seat: {
              select: {
                seatNumber: true,
                bedPosition: true,
                coach: { select: { number: true, coachType: { select: { name: true, seatClasses: { select: { name: true, bedPosition: true } } } } } },
              },
            },
          },
        },
        passenger: { select: { user: { select: { phone: true, fullName: true } } } },
      },
    });

    const stationIds = [...new Set(
      bookings.flatMap(b => [b.originStationId, b.destinationStationId]).filter(Boolean) as string[],
    )];
    const stations = stationIds.length > 0
      ? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } })
      : [];
    const stationName = new Map(stations.map(s => [s.id, s.name]));

    const resolveSeatClass = (seat: any): string => {
      const classes = seat?.coach?.coachType?.seatClasses ?? [];
      const matched = seat?.bedPosition
        ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === seat.bedPosition.toLowerCase())
        : null;
      return (matched ?? classes[0])?.name ?? seat?.coach?.coachType?.name ?? 'Unknown';
    };

    let rows = bookings.map(b => {
      const pi = b.paymentIntent!;
      const actualMinor = b.seats.reduce((s, seat) => s + (seat.fareMinor ?? 0), 0);
      // pi.amountMinor is a Float in full currency units — convert to cents once
      const paidMinorCents = Math.round(pi.amountMinor * 100);

      const isPackage = !!(b as any).packageId;
      const effectiveActualMinor = isPackage ? actualMinor * 2 : actualMinor;
      const effectiveVarianceMinor = effectiveActualMinor - paidMinorCents;

      const breakdown = b.seats.map(s => ({
        passengerName: s.passengerName ?? '—',
        seatClass: resolveSeatClass(s.seat),
        coachNumber: s.seat?.coach?.number ?? null,
        seatNumber: s.seat?.seatNumber ?? null,
        fareMinor: isPackage ? (s.fareMinor ?? 0) * 2 : (s.fareMinor ?? 0),
      }));

      const firstSeat = b.seats[0];
      return {
        bookingRef: b.bookingRef,
        isPackage,
        seatClass: firstSeat?.seatLabelSnapshot ?? firstSeat?.seat?.coach?.coachType?.name ?? '—',
        coachNumber: firstSeat?.seat?.coach?.number ?? null,
        seatNumber: firstSeat?.seat?.seatNumber ?? null,
        origin: b.originStationId ? (stationName.get(b.originStationId) ?? '—') : '—',
        destination: b.destinationStationId ? (stationName.get(b.destinationStationId) ?? '—') : '—',
        phone: b.passenger?.user?.phone ?? (b as any).contactPhone ?? '—',
        actualMinor: effectiveActualMinor,
        paidMinor: paidMinorCents,
        varianceMinor: effectiveVarianceMinor,
        breakdown,
      };
    }).filter(r => r.varianceMinor > 0);

    if (params.search?.trim()) {
      const q = params.search.trim().toUpperCase();
      rows = rows.filter(r => r.bookingRef.toUpperCase().includes(q));
    }
    if (params.seatClass?.trim()) {
      const sc = params.seatClass.trim().toLowerCase();
      rows = rows.filter(r => r.breakdown.some(bd => bd.seatClass.toLowerCase().includes(sc)));
    }
    if (params.sort === 'asc') {
      rows.sort((a, b) => a.varianceMinor - b.varianceMinor);
    } else {
      rows.sort((a, b) => b.varianceMinor - a.varianceMinor);
    }

    return { total: rows.length, rows };
  }

  async getBoardingReport(scheduleId: string) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      select: {
        id: true,
        departureAt: true,
        arrivalAt: true,
        train: { select: { number: true, name: true } },
        originStation: { select: { name: true } },
        destinationStation: { select: { name: true } },
      },
    });
    if (!schedule) return null;

    const tickets = await this.prisma.ticket.findMany({
      where: {
        OR: [
          { scheduleId, booking: { status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { leg: 2, booking: { returnScheduleId: scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
          { scheduleId: null, leg: 1, booking: { scheduleId, status: { in: ['CONFIRMED', 'BOARDED'] } } },
        ],
      },
      select: {
        id: true,
        bookingRef: true,
        passengerName: true,
        boardedAt: true,
        validatorId: true,
        status: true,
        booking: {
          select: {
            status: true,
            originStationId: true,
            destinationStationId: true,
          },
        },
        seat: {
          select: {
            seatNumber: true,
            bedPosition: true,
            coach: {
              select: {
                number: true,
                coachType: { select: { name: true, seatClasses: { select: { name: true, bedPosition: true } } } },
              },
            },
          },
        },
      },
      orderBy: [{ seat: { coach: { number: 'asc' } } }, { seat: { seatNumber: 'asc' } }],
    });

    const stationIds = [...new Set(
      tickets.flatMap(t => [t.booking.originStationId, t.booking.destinationStationId]).filter(Boolean) as string[],
    )];
    const stations = stationIds.length > 0
      ? await this.prisma.station.findMany({ where: { id: { in: stationIds } }, select: { id: true, name: true } })
      : [];
    const stationName = new Map(stations.map(s => [s.id, s.name]));

    const resolveSeatClass = (seat: any): string | null => {
      const classes = seat?.coach?.coachType?.seatClasses ?? [];
      const matched = seat?.bedPosition
        ? classes.find((sc: any) => sc.bedPosition?.toLowerCase() === seat.bedPosition.toLowerCase())
        : null;
      return (matched ?? classes[0])?.name ?? seat?.coach?.coachType?.name ?? null;
    };

    const rows = tickets.map(t => ({
      bookingRef: t.bookingRef,
      passengerName: t.passengerName,
      coachNumber: t.seat?.coach?.number ?? null,
      seatNumber: t.seat?.seatNumber ?? null,
      seatClassName: resolveSeatClass(t.seat),
      origin: t.booking.originStationId ? (stationName.get(t.booking.originStationId) ?? null) : null,
      destination: t.booking.destinationStationId ? (stationName.get(t.booking.destinationStationId) ?? null) : null,
      boarded: !!t.boardedAt,
      boardedAt: t.boardedAt ?? null,
      validatorId: t.validatorId ?? null,
      bookingStatus: t.booking.status,
    }));

    const boardedCount = rows.filter(r => r.boarded).length;
    const notBoardedCount = rows.length - boardedCount;

    const byCoach = new Map<string, { coachNumber: string; total: number; boarded: number }>();
    for (const r of rows) {
      const key = r.coachNumber ?? 'Unknown';
      if (!byCoach.has(key)) byCoach.set(key, { coachNumber: key, total: 0, boarded: 0 });
      byCoach.get(key)!.total++;
      if (r.boarded) byCoach.get(key)!.boarded++;
    }

    return {
      schedule: {
        id: schedule.id,
        trainName: (schedule.train as any)?.name ?? (schedule.train as any)?.number,
        origin: (schedule.originStation as any)?.name,
        destination: (schedule.destinationStation as any)?.name,
        departureAt: schedule.departureAt,
        arrivalAt: schedule.arrivalAt,
      },
      summary: {
        total: rows.length,
        boardedCount,
        notBoardedCount,
        boardingRate: rows.length > 0 ? +((boardedCount / rows.length) * 100).toFixed(1) : 0,
      },
      byCoach: [...byCoach.values()].sort((a, b) => a.coachNumber.localeCompare(b.coachNumber)),
      rows,
    };
  }

  async getReport(reportId: string) {
    return this.prisma.operationalReport.findUnique({
      where: { id: reportId },
    });
  }

  async listReports(reportType?: string) {
    return this.prisma.operationalReport.findMany({
      where: reportType ? { reportType } : {},
      orderBy: { createdAt: "desc" },
      take: 50,
    });
  }
}
