import { Injectable, Logger, NotFoundException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { RoutesService } from './routes.service';
import { FareEngineService } from '../fare-engine/fare-engine.service';
import { CreateScheduleDto, UpdateScheduleDto, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto, BulkCreateSchedulesDto } from './schedules.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { parseEthiopianTime, startOfDayEAT, startOfNextDayEAT } from '../../common/utils/timezone.utils';
import { AuditService } from '../../common/audit.service';
import { computePlannedStopTimes } from '../../common/utils/schedule-times.utils';

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

  constructor(
    private prisma: PrismaService,
    private routesService: RoutesService,
    private fareEngine: FareEngineService,
    private auditService: AuditService,
  ) { }

  async bulkGenerateSchedules(dto: BulkCreateSchedulesDto) {
    const startDate = parseEthiopianTime(dto.startDateTime);
    const endDate = new Date(startDate.getTime() + dto.forNextDays * 24 * 60 * 60 * 1000);
    const errors: string[] = [];
    const scheduleIds: string[] = [];

    const route = await this.prisma.route.findUnique({
      where: { id: dto.routeId },
      include: { stops: { orderBy: { sequence: 'asc' } } },
    });
    if (!route) throw new NotFoundException('Route not found');
    if (!route.active) throw new BadRequestException('Route is not active');

    let currentDate = new Date(startDate);
    let scheduleCount = 0;

    while (currentDate < endDate) {
      try {
        const departureAt = new Date(currentDate);
        const arrivalAt = new Date(departureAt.getTime() + dto.durationHours * 60 * 60 * 1000);

        const createDto: CreateScheduleDto = {
          trainId: dto.trainId,
          routeId: dto.routeId,
          departureAt: departureAt.toISOString(),
          arrivalAt: arrivalAt.toISOString(),
          plannedTimes: dto.plannedTimes || [],
          coachIds: dto.coachIds,
        };

        // createSchedule applies coachIds if given, else auto-applies the route coach template,
        // and rejects the day outright (caught below) if it would end up with zero coaches.
        const schedule = await this.createSchedule(createDto);
        scheduleIds.push(schedule.id);

        scheduleCount++;
      } catch (error) {
        errors.push(`Failed to create schedule for ${currentDate.toISOString()}: ${error instanceof Error ? error.message : String(error)}`);
      }

      currentDate = new Date(currentDate.getTime() + dto.repeatEveryDays * 24 * 60 * 60 * 1000);
    }

    return { schedulesCreated: scheduleCount, errors, scheduleIds };
  }

  async listSchedules(dto: ListSchedulesDto) {
    const where: any = {};

    if (dto.date) {
      const date = parseEthiopianTime(dto.date);
      const nextDay = startOfNextDayEAT(date);
      where.departureAt = { gte: date, lt: nextDay };
    }
    if (dto.routeId) where.routeId = dto.routeId;
    if (dto.trainId) where.trainId = dto.trainId;
    if (dto.status) where.status = dto.status;

    return this.prisma.trainSchedule.findMany({
      where,
      include: {
        train: true,
        route: true,
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        coachAssignments: {
          include: { coach: true },
          orderBy: { positionNumber: 'asc' },
        },
        _count: { select: { coachAssignments: true, bookings: true } },
      },
      orderBy: { departureAt: 'asc' },
    });
  }

  async createSchedule(dto: CreateScheduleDto) {
    // Parse dates in local Ethiopian time (EAT - UTC+3)
    const dep = parseEthiopianTime(dto.departureAt);
    const arr = parseEthiopianTime(dto.arrivalAt);
    if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');
    // M-4: a new schedule cannot depart in the past — the backoffice form does not enforce this.
    if (dep.getTime() < Date.now()) throw new BadRequestException('departureAt must be in the future');

    const [train, route] = await Promise.all([
      this.prisma.train.findUnique({ where: { id: dto.trainId } }),
      this.prisma.route.findUnique({
        where: { id: dto.routeId },
        include: { stops: { orderBy: { sequence: 'asc' } } },
      }),
    ]);
    if (!train) throw new NotFoundException('Train not found');
    if (!train.isActive) throw new BadRequestException('Train is not active');
    if (!route) throw new NotFoundException('Route not found');
    if (!route.active) throw new BadRequestException('Route is not active');
    if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');

    // Check for existing schedule on the same day (local Ethiopian time)
    const depDate = startOfDayEAT(dep);
    const nextDay = startOfNextDayEAT(dep);

    const existingSchedule = await this.prisma.trainSchedule.findFirst({
      where: { trainId: dto.trainId, routeId: dto.routeId, departureAt: { gte: depDate, lt: nextDay } },
    });

    if (existingSchedule) {
      throw new BadRequestException(
        `A schedule for this train, route, and date already exists. Departure: ${new Date(existingSchedule.departureAt).toLocaleString()}`,
      );
    }

    let plannedTimes = dto.plannedTimes;
    if (!plannedTimes || plannedTimes.length === 0) {
      plannedTimes = computePlannedStopTimes(route, dep, arr);
    }

    const providedSeqs = new Set((plannedTimes ?? []).map(t => t.sequence));
    const missingSeqs = route.stops.map(s => s.sequence).filter(seq => !providedSeqs.has(seq));
    if (missingSeqs.length > 0) {
      throw new BadRequestException(`Missing planned times for stop sequences: ${missingSeqs.join(', ')}`);
    }

    const firstStop = route.stops[0];
    const lastStop = route.stops[route.stops.length - 1];

    const schedule = await this.prisma.trainSchedule.create({
      data: {
        trainId: dto.trainId,
        routeId: dto.routeId,
        originStationId: firstStop.stationId,
        destinationStationId: lastStop.stationId,
        departureAt: dep,
        arrivalAt: arr,
        durationMinutes: Math.round((arr.getTime() - dep.getTime()) / 60_000),
        stopsCount: Math.max(0, route.stops.length - 2),
      },
      include: { train: true, originStation: true, destinationStation: true },
    });

    const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
    await this.routesService.applyRouteToSchedule(dto.routeId, schedule.id, plannedTimesMap);

    // Explicit coachIds (from the schedule form's Coaches step) override the route's coach
    // template; otherwise auto-apply the template if one is defined.
    if (dto.coachIds && dto.coachIds.length > 0) {
      await this.assignCoaches(
        schedule.id,
        dto.coachIds.map((coachId, idx) => ({ coachId, positionNumber: idx + 1 })),
      );
    } else {
      const coachTemplates = await this.prisma.routeCoachTemplate.findMany({
        where: { routeId: dto.routeId },
        orderBy: { positionNumber: 'asc' },
      });
      if (coachTemplates.length > 0) {
        await this.assignCoaches(
          schedule.id,
          coachTemplates.map(t => ({ coachId: t.coachId, positionNumber: t.positionNumber })),
        );
      }
    }

    // A schedule with zero coaches has zero seats and is silently invisible to search (and
    // unbookable) with no indication why — block creation instead of leaving a dead schedule.
    const assignedCoachCount = await this.prisma.coachAssignment.count({ where: { scheduleId: schedule.id } });
    if (assignedCoachCount === 0) {
      await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: schedule.id } });
      await this.prisma.trainSchedule.delete({ where: { id: schedule.id } });
      throw new BadRequestException(
        'A schedule must have at least one coach assigned to be bookable. Add coaches in the Coaches step, or set a Route Coach Template on this route so new schedules auto-assign coaches.',
      );
    }

    const result = await this.getSchedule(schedule.id);
    await this.auditService.log({ action: 'CREATE', entityType: 'Schedule', entityId: schedule.id, newData: { trainId: dto.trainId, routeId: dto.routeId, departureAt: dep } });
    return result;
  }

  async getSchedule(id: string) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id },
      include: {
        train: true,
        originStation: true,
        destinationStation: true,
        coachAssignments: {
          include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } } },
          orderBy: { positionNumber: 'asc' },
        },
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const allSeatIds = schedule.coachAssignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id));
    const effectiveStatuses = await this.resolveEffectiveStatuses(id, allSeatIds);

    return {
      ...schedule,
      coachAssignments: schedule.coachAssignments.map((a: any) => ({
        ...a,
        coach: {
          ...a.coach,
          seats: a.coach.seats.map((s: any) => ({
            ...s,
            status: effectiveStatuses.get(s.id) ?? s.status,
          })),
        },
      })),
    };
  }

  private async resolveEffectiveStatuses(scheduleId: string, seatIds: string[]): Promise<Map<string, string>> {
    const statusMap = new Map<string, string>();
    if (seatIds.length === 0) return statusMap;

    const [activeHolds, bookedSegments] = await Promise.all([
      this.prisma.seatHold.findMany({
        where: { scheduleId, expiresAt: { gt: new Date() }, seatIds: { hasSome: seatIds } },
        select: { seatIds: true },
      }),
      this.prisma.journeySegment.findMany({
        where: {
          scheduleId,
          seatId: { in: seatIds },
          journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
        },
        select: { seatId: true },
      }),
    ]);

    for (const hold of activeHolds)
      for (const seatId of hold.seatIds)
        if (seatIds.includes(seatId)) statusMap.set(seatId, 'HELD');

    for (const seg of bookedSegments)
      if (seg.seatId) statusMap.set(seg.seatId, 'BOOKED');

    return statusMap;
  }

  async updateSchedule(id: string, dto: CreateScheduleDto) {
    const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
    if (!schedule) throw new NotFoundException('Schedule not found');

    // Parse dates in local Ethiopian time (EAT - UTC+3)
    const dep = parseEthiopianTime(dto.departureAt);
    const arr = parseEthiopianTime(dto.arrivalAt);
    if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');

    const [train, route] = await Promise.all([
      this.prisma.train.findUnique({ where: { id: dto.trainId } }),
      this.prisma.route.findUnique({
        where: { id: dto.routeId },
        include: { stops: { orderBy: { sequence: 'asc' } } },
      }),
    ]);
    if (!train) throw new NotFoundException('Train not found');
    if (!train.isActive) throw new BadRequestException('Train is not active');
    if (!route) throw new NotFoundException('Route not found');
    if (!route.active) throw new BadRequestException('Route is not active');
    if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');

    const firstStop = route.stops[0];
    const lastStop = route.stops[route.stops.length - 1];

    await this.prisma.trainSchedule.update({
      where: { id },
      data: {
        trainId: dto.trainId,
        routeId: dto.routeId,
        originStationId: firstStop.stationId,
        destinationStationId: lastStop.stationId,
        departureAt: dep,
        arrivalAt: arr,
        durationMinutes: Math.round((arr.getTime() - dep.getTime()) / 60_000),
        stopsCount: Math.max(0, route.stops.length - 2),
      },
    });

    await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });

    let plannedTimes = dto.plannedTimes;
    if (!plannedTimes || plannedTimes.length === 0) {
      plannedTimes = computePlannedStopTimes(route, dep, arr);
    }

    const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
    await this.routesService.applyRouteToSchedule(dto.routeId, id, plannedTimesMap);

    const result = await this.getSchedule(id);
    await this.auditService.log({ action: 'UPDATE', entityType: 'Schedule', entityId: id, newData: { trainId: dto.trainId, departureAt: dep } });
    return result;
  }

  async updateScheduleStatus(id: string, dto: UpdateScheduleStatusDto) {
    return this.prisma.trainSchedule.update({ where: { id }, data: { status: dto.status } });
  }

  async deleteSchedule(id: string, cascade = false) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id },
      include: { 
        _count: { select: { bookings: true } },
        train: true,
        originStation: true,
        destinationStation: true
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');
    
    if (!cascade) {
      const constraints = [];
      if ((schedule as any)._count.bookings > 0) {
        constraints.push({
          entityName: 'booking',
          count: (schedule as any)._count.bookings,
          action: 'cancel' as const
        });
      }
      
      if (constraints.length > 0) {
        const scheduleName = `${schedule.train.number} (${schedule.originStation.name} → ${schedule.destinationStation.name})`;
        throw new DeleteOperationException('Schedule', scheduleName, constraints);
      }
    }
    
    await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
    await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
    await this.prisma.tripLiveStatus.deleteMany({ where: { scheduleId: id } });
    await this.prisma.menuItem.deleteMany({ where: { scheduleId: id } });
    await this.prisma.journeySegment.deleteMany({ where: { scheduleId: id } });

    // Delete bookings and all their children
    const bookings = await this.prisma.booking.findMany({
      where: { OR: [{ scheduleId: id }, { returnScheduleId: id }] },
      select: { id: true },
    });
    if (bookings.length > 0) {
      const bookingIds = bookings.map(b => b.id);
      // Leaf tables first
      const tickets = await this.prisma.ticket.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
      if (tickets.length > 0) {
        await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: { in: tickets.map(t => t.id) } } });
      }
      await this.prisma.ticket.deleteMany({ where: { bookingId: { in: bookingIds } } });
      const foodOrders = await this.prisma.foodOrder.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
      if (foodOrders.length > 0) {
        await this.prisma.foodOrderItem.deleteMany({ where: { orderId: { in: foodOrders.map(o => o.id) } } });
      }
      await this.prisma.foodOrder.deleteMany({ where: { bookingId: { in: bookingIds } } });
      const paymentIntents = await this.prisma.paymentIntent.findMany({ where: { bookingId: { in: bookingIds } }, select: { id: true } });
      if (paymentIntents.length > 0) {
        await this.prisma.paymentRefund.deleteMany({ where: { paymentIntentId: { in: paymentIntents.map(p => p.id) } } });
      }
      await this.prisma.paymentIntent.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.bookingSeat.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.agentBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.bookingModification.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.bookingCancellation.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.baggageBooking.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.excessBaggageCharge.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.journey.deleteMany({ where: { bookingId: { in: bookingIds } } });
      await this.prisma.booking.deleteMany({ where: { id: { in: bookingIds } } });
    }

    // Delete travel packages that reference this schedule
    const packagesToDelete = await this.prisma.travelPackage.findMany({
      where: { OR: [{ outboundScheduleId: id }, { returnScheduleId: id }] },
      select: { id: true },
    });
    if (packagesToDelete.length > 0) {
      const packageIds = packagesToDelete.map(p => p.id);
      await this.prisma.packagePriceTier.deleteMany({ where: { packageId: { in: packageIds } } });
      await this.prisma.travelPackage.deleteMany({ where: { id: { in: packageIds } } });
    }
    await this.prisma.trainSchedule.delete({ where: { id } });
    await this.auditService.log({ action: 'DELETE', entityType: 'Schedule', entityId: id });
    return { deleted: true, id };
  }

  getStops(scheduleId: string) {
    return this.prisma.tripStopTime.findMany({
      where: { scheduleId },
      include: { station: true },
      orderBy: { sequence: 'asc' },
    });
  }

  async updateStop(scheduleId: string, sequence: number, dto: UpdateStopTimeDto) {
    const stop = await this.prisma.tripStopTime.findUnique({
      where: { scheduleId_sequence: { scheduleId, sequence } },
    });
    if (!stop) throw new NotFoundException(`Stop at sequence ${sequence} not found on schedule`);

    return this.prisma.tripStopTime.update({
      where: { scheduleId_sequence: { scheduleId, sequence } },
      data: {
        plannedArrivalAt: dto.plannedArrivalAt ? parseEthiopianTime(dto.plannedArrivalAt) : undefined,
        plannedDepartureAt: dto.plannedDepartureAt ? parseEthiopianTime(dto.plannedDepartureAt) : undefined,
        status: dto.status,
      },
      include: { station: true },
    });
  }

  async upsertScheduleFare(
    scheduleId: string,
    seatClassId: string,
    dto: { baseFareMinor: number; validFrom?: string; validUntil?: string },
  ) {
    const [schedule, seatClass] = await Promise.all([
      this.prisma.trainSchedule.findUnique({ where: { id: scheduleId } }),
      this.prisma.seatClass.findUnique({ where: { id: seatClassId } }),
    ]);
    if (!schedule) throw new NotFoundException('Schedule not found');
    if (!seatClass) throw new NotFoundException('Seat class not found');

    const now = new Date();
    const validFrom = dto.validFrom ? parseEthiopianTime(dto.validFrom) : now;
    const validUntil = dto.validUntil ? parseEthiopianTime(dto.validUntil) : null;

    return this.prisma.$transaction(async (tx) => {
      await tx.fareRule.updateMany({
        where: { tripId: scheduleId, seatClassId, validUntil: null },
        data: { validUntil: now },
      });
      return tx.fareRule.create({
        data: { tripId: scheduleId, seatClassId, baseFareMinor: dto.baseFareMinor, currency: 'ETB', validFrom, validUntil },
        include: { seatClass: true },
      });
    });
  }

  createFareRule(dto: CreateFareRuleDto) {
    const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto;
    const result = this.prisma.fareRule.create({
      data: {
        ...rest,
        tripId: scheduleId,
        nationality,
        validFrom: parseEthiopianTime(validFrom),
        validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
      },
      include: { seatClass: true },
    });
    result.then(r => this.auditService.log({ action: 'CREATE', entityType: 'FareRule', entityId: r.id, newData: { seatClassId: r.seatClassId, baseFareMinor: r.baseFareMinor } }));
    return result;
  }

  async updateFareRule(id: string, dto: Partial<CreateFareRuleDto>) {
    const existing = await this.prisma.fareRule.findUnique({ where: { id } });
    if (!existing) throw new NotFoundException('Fare rule not found');

    const { validFrom, validUntil, scheduleId, nationality, passengerCategory, ...rest } = dto;
    return this.prisma.fareRule.update({
      where: { id },
      data: {
        ...rest,
        ...(scheduleId !== undefined && { tripId: scheduleId }),
        ...(nationality !== undefined && { nationality }),
        ...(validFrom && { validFrom: parseEthiopianTime(validFrom) }),
        ...(validUntil !== undefined && { validUntil: validUntil ? parseEthiopianTime(validUntil) : null }),
      },
      include: { seatClass: true },
    });
  }

  async deleteFareRule(id: string) {
    const existing = await this.prisma.fareRule.findUnique({ where: { id } });
    if (!existing) throw new NotFoundException('Fare rule not found');
    await this.prisma.fareRule.delete({ where: { id } });
    await this.auditService.log({ action: 'DELETE', entityType: 'FareRule', entityId: id });
    return { deleted: true, id };
  }

  createSegmentFareRule(dto: any) {
    const { validFrom, validUntil, passengerCategory, ...rest } = dto;
    return this.prisma.segmentFareRule.create({
      data: {
        ...rest,
        validFrom: parseEthiopianTime(validFrom),
        validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
      },
      include: { seatClass: true, route: true },
    });
  }

  getSegmentFares(routeId: string) {
    return this.prisma.segmentFareRule.findMany({
      where: { routeId },
      include: { seatClass: true, route: true },
      orderBy: [{ originStopSequence: 'asc' }, { destinationStopSequence: 'asc' }],
    });
  }

  deleteSegmentFareRule(id: string) {
    return this.prisma.segmentFareRule.delete({ where: { id } });
  }

  updateSegmentFareRule(id: string, dto: any) {
    const { validFrom, validUntil, passengerCategory, ...rest } = dto;
    return this.prisma.segmentFareRule.update({
      where: { id },
      data: {
        ...rest,
        validFrom: validFrom ? parseEthiopianTime(validFrom) : undefined,
        validUntil: validUntil ? parseEthiopianTime(validUntil) : null,
      },
      include: { seatClass: true, route: true },
    });
  }

  async getFareRules(scheduleId?: string) {
    const where: any = {};
    if (scheduleId) where.tripId = scheduleId;
    return this.prisma.fareRule.findMany({
      where,
      include: { seatClass: true },
      orderBy: { createdAt: 'desc' },
    });
  }

  getFareFromEngine(scheduleId: string, seatClassId: string, nationality?: string) {
    return this.fareEngine.calculateForSchedule(scheduleId, seatClassId, nationality);
  }

  async getAllFaresFromEngine(scheduleId: string, nationality?: string) {
    try {
      const schedule = await this.prisma.trainSchedule.findUnique({
        where: { id: scheduleId },
        select: { routeId: true, originStationId: true, destinationStationId: true },
      });
      if (!schedule) throw new NotFoundException('Schedule not found');
      if (!schedule.routeId) throw new BadRequestException('Schedule has no associated route');
      return await this.fareEngine.calculateAllForSchedule(scheduleId, nationality);
    } catch (error) {
      throw new BadRequestException(
        error instanceof Error ? error.message : 'Failed to calculate fares for schedule',
      );
    }
  }

  async syncFaresFromEngine(scheduleId: string): Promise<{ synced: number; errors: string[] }> {
    const results = await this.fareEngine.calculateAllForSchedule(scheduleId);
    const errors: string[] = [];
    let synced = 0;
    const now = new Date();

    for (const fare of results as any[]) {
      try {
        const seatClass = await this.prisma.seatClass.findFirst({ where: { name: fare.seatClassName } });
        if (!seatClass) { errors.push(`Seat class not found: ${fare.seatClassName}`); continue; }

        await this.prisma.fareRule.updateMany({
          where: { tripId: scheduleId, seatClassId: seatClass.id, validUntil: null },
          data: { validUntil: now },
        });

        await this.prisma.fareRule.create({
          data: {
            tripId: scheduleId,
            seatClassId: seatClass.id,
            baseFareMinor: fare.totalMinor,
            currency: 'ETB',
            validFrom: now,
            validUntil: null,
          },
        });
        synced++;
      } catch (err) {
        errors.push(`${fare.seatClassName}: ${err instanceof Error ? err.message : String(err)}`);
      }
    }

    return { synced, errors };
  }

  async recalculateStopTimes(scheduleId: string) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      include: { route: { include: { stops: { orderBy: { sequence: 'asc' } } } } },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');
    if (!schedule.routeId || !schedule.route) throw new BadRequestException('Schedule has no associated route');

    const plannedTimes = computePlannedStopTimes(
      schedule.route,
      new Date(schedule.departureAt),
      new Date(schedule.arrivalAt),
    );
    const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
    await this.routesService.applyRouteToSchedule(schedule.routeId, scheduleId, plannedTimesMap);
    return { recalculated: true, scheduleId, stopCount: plannedTimes.length };
  }

  async assignCoaches(scheduleId: string, coaches: Array<{ coachId: string; positionNumber: number }>) {
    const schedule = await this.prisma.trainSchedule.findUnique({ where: { id: scheduleId } });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const coachIds = coaches.map(c => c.coachId);
    const existingCoaches = await this.prisma.coach.findMany({ where: { id: { in: coachIds } } });
    if (existingCoaches.length !== coachIds.length) throw new NotFoundException('One or more coaches not found');
    const inactiveCoach = existingCoaches.find(c => c.status !== 'ACTIVE');
    if (inactiveCoach) throw new BadRequestException(`Coach ${inactiveCoach.number} is not active`);

    await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });

    const data = coaches.map((c) => ({
      scheduleId,
      coachId: c.coachId,
      positionNumber: c.positionNumber,
      isOperational: true,
    }));

    await this.prisma.coachAssignment.createMany({ data });
    return { message: 'Coaches assigned successfully', count: coaches.length };
  }

  async getAssignedCoaches(scheduleId: string) {
    return this.prisma.coachAssignment.findMany({
      where: { scheduleId },
      include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } } },
      orderBy: { positionNumber: 'asc' },
    });
  }

  async updateSchedulePartial(id: string, dto: UpdateScheduleDto) {
    const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const updateData: any = {};
    let dep: Date | undefined;
    let arr: Date | undefined;

    if (dto.departureAt || dto.arrivalAt) {
      dep = dto.departureAt ? parseEthiopianTime(dto.departureAt) : new Date(schedule.departureAt);
      arr = dto.arrivalAt ? parseEthiopianTime(dto.arrivalAt) : new Date(schedule.arrivalAt);
      if (arr <= dep) throw new BadRequestException('Arrival time must be after departure time');
      if (dep.getTime() < Date.now()) throw new BadRequestException('departureAt must be in the future');
      updateData.departureAt = dep;
      updateData.arrivalAt = arr;
      updateData.durationMinutes = Math.round((arr.getTime() - dep.getTime()) / 60_000);
    }

    if (dto.status) updateData.status = dto.status;
    if (dto.isPackageOnly !== undefined) updateData.isPackageOnly = dto.isPackageOnly;

    if (Object.keys(updateData).length > 0) {
      await this.prisma.trainSchedule.update({ where: { id }, data: updateData });
    }

    // departureAt/arrivalAt changed — the per-stop TripStopTime rows were computed against the
    // OLD times and are now stale (same interpolation createSchedule/updateSchedule use). Left
    // unfixed, check-in cutoff enforcement and search silently keep using outdated per-stop
    // arrival/departure estimates for every intermediate stop.
    if (dep && arr && schedule.routeId) {
      const route = await this.prisma.route.findUnique({
        where: { id: schedule.routeId },
        include: { stops: { orderBy: { sequence: 'asc' } } },
      });
      if (route && route.stops.length >= 2) {
        const plannedTimes = computePlannedStopTimes(route, dep, arr);
        const plannedTimesMap = Object.fromEntries(plannedTimes.map(t => [t.sequence, t]));
        await this.routesService.applyRouteToSchedule(schedule.routeId, id, plannedTimesMap);
      }
    }

    if (dto.coaches !== undefined) {
      if (dto.coaches.length > 0) {
        await this.assignCoaches(id, dto.coaches);
      } else {
        await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
      }
    }

    return this.getSchedule(id);
  }

  async removeCoachAssignment(scheduleId: string, coachId: string) {
    const assignment = await this.prisma.coachAssignment.findFirst({ where: { scheduleId, coachId } });
    if (!assignment) throw new NotFoundException('Coach assignment not found');
    await this.prisma.coachAssignment.delete({ where: { id: assignment.id } });
    return { message: 'Coach assignment removed' };
  }

  // ── Route Fare Rule Overrides ──────────────────────────────────────────────

  listRouteFareRules(routeId: string) {
    return this.prisma.routeFareRule.findMany({
      where: { routeId },
      include: { seatClass: true, route: true },
      orderBy: { createdAt: 'desc' },
    });
  }

  async createRouteFareRule(dto: {
    routeId: string;
    seatClassId: string;
    passengerCategory?: string;
    baseFareMinor: number;
    validFrom: string;
    validUntil?: string;
  }) {
    const [route, seatClass] = await Promise.all([
      this.prisma.route.findUnique({ where: { id: dto.routeId } }),
      this.prisma.seatClass.findUnique({ where: { id: dto.seatClassId } }),
    ]);
    if (!route) throw new NotFoundException('Route not found');
    if (!seatClass) throw new NotFoundException('Seat class not found');
    const rule = await this.prisma.routeFareRule.create({
      data: {
        routeId: dto.routeId,
        seatClassId: dto.seatClassId,
        passengerCategory: (dto.passengerCategory as any) ?? 'ADULT',
        baseFareMinor: dto.baseFareMinor,
        validFrom: parseEthiopianTime(dto.validFrom),
        validUntil: dto.validUntil ? parseEthiopianTime(dto.validUntil) : null,
      },
      include: { seatClass: true, route: true },
    });
    await this.auditService.log({ action: 'CREATE', entityType: 'RouteFareRule', entityId: rule.id, newData: { routeId: dto.routeId, seatClassId: dto.seatClassId, baseFareMinor: dto.baseFareMinor } });
    return rule;
  }

  async updateRouteFareRule(id: string, dto: { baseFareMinor?: number; surchargeMinor?: number; validFrom?: string; validUntil?: string }) {
    const rule = await this.prisma.routeFareRule.findUnique({ where: { id } });
    if (!rule) throw new NotFoundException('Route fare rule not found');
    return this.prisma.routeFareRule.update({
      where: { id },
      data: {
        ...(dto.baseFareMinor !== undefined && { baseFareMinor: dto.baseFareMinor }),
        ...(dto.surchargeMinor !== undefined && { surchargeMinor: dto.surchargeMinor }),
        ...(dto.validFrom && { validFrom: parseEthiopianTime(dto.validFrom) }),
        ...(dto.validUntil !== undefined && { validUntil: dto.validUntil ? parseEthiopianTime(dto.validUntil) : null }),
      },
      include: { seatClass: true, route: true },
    });
  }

  async deleteRouteFareRule(id: string) {
    const rule = await this.prisma.routeFareRule.findUnique({ where: { id } });
    if (!rule) throw new NotFoundException('Route fare rule not found');
    await this.prisma.routeFareRule.delete({ where: { id } });
    await this.auditService.log({ action: 'DELETE', entityType: 'RouteFareRule', entityId: id });
    return { deleted: true, id };
  }
}
