import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CreateRouteDto, AddRouteStopDto, UpdateRouteDto, SetRouteCoachTemplateDto } from './routes.dto';
import { DeleteOperationException } from '../../common/exceptions/delete-operation.exception';
import { AuditService } from '../../common/audit.service';
import { parseEthiopianTime } from '../../common/utils/timezone.utils';
import { computePlannedStopTimes } from '../../common/utils/schedule-times.utils';

@Injectable()
export class RoutesService {
  constructor(private prisma: PrismaService, private auditService: AuditService) {}

  // ── Route CRUD ─────────────────────────────────────────────────────────────

  /**
   * distanceKm is CUMULATIVE distance from the route origin, not distance from the previous
   * stop (that's what travelMinutesToStop is for) — fare pricing computes a segment's distance
   * as destStop.distanceKm - originStop.distanceKm, so a route with equal or decreasing values
   * across stops silently produces zero/negative segment distances, which the fare engine
   * rejects (caught and swallowed by search into a bare "N/A" instead of a visible error). Catch
   * the mistake here instead, with a message that names the exact stops involved.
   */
  private validateStopDistances(stops: { sequence: number; stationId: string; distanceKm?: number | null }[]): void {
    const sorted = [...stops].sort((a, b) => a.sequence - b.sequence);
    let prevDistance = sorted[0]?.distanceKm ?? 0;
    for (let i = 1; i < sorted.length; i++) {
      const stop = sorted[i];
      if (stop.distanceKm == null) {
        throw new BadRequestException(
          `Stop ${stop.sequence} is missing distanceKm (cumulative distance in km from the route origin). This is required for fare pricing.`,
        );
      }
      if (stop.distanceKm <= prevDistance) {
        throw new BadRequestException(
          `Stop ${stop.sequence}'s distanceKm (${stop.distanceKm}) must be greater than stop ${sorted[i - 1].sequence}'s distanceKm (${prevDistance}) — distanceKm is cumulative distance from the route origin, not distance from the previous stop. Equal or decreasing values make fare pricing between these stops fail silently.`,
        );
      }
      prevDistance = stop.distanceKm;
    }
  }

  async createRoute(dto: CreateRouteDto) {
    const existing = await this.prisma.route.findUnique({ where: { code: dto.code } });
    if (existing) throw new ConflictException(`Route code "${dto.code}" already exists`);

    if (dto.stops.length < 2) throw new BadRequestException('A route must have at least 2 stops');

    const seqs = dto.stops.map(s => s.sequence);
    if (new Set(seqs).size !== seqs.length) throw new ConflictException('Duplicate sequence numbers in stop list');

    this.validateStopDistances(dto.stops);

    const stationIds = [...new Set(dto.stops.map(s => s.stationId))];
    const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
    if (stations.length !== stationIds.length) throw new BadRequestException('One or more station IDs not found');

    const route = await this.prisma.route.create({
      data: {
        code: dto.code,
        name: dto.name,
        description: dto.description,
        active: dto.active ?? true,
        ...(dto.checkinMinutesBefore != null ? { checkinMinutesBefore: dto.checkinMinutesBefore } : {}),
        effectiveFrom: parseEthiopianTime(dto.effectiveFrom),
        effectiveUntil: dto.effectiveUntil ? parseEthiopianTime(dto.effectiveUntil) : null,
        stops: {
          create: dto.stops.map(s => ({
            stationId: s.stationId,
            sequence: s.sequence,
            distanceKm: s.distanceKm != null ? parseFloat(String(s.distanceKm)) : null,
            checkinMinutesBefore: s.checkinMinutesBefore ?? null,
            travelMinutesToStop: s.travelMinutesToStop ?? null,
          })),
        },
      },
      include: { stops: { include: { route: false }, orderBy: { sequence: 'asc' } } },
    });
    await this.auditService.log({ action: 'CREATE', entityType: 'Route', entityId: route.id, newData: { code: route.code, name: route.name } });
    return route;
  }

  async listRoutes(activeOnly = false) {
    return this.prisma.route.findMany({
      where: activeOnly ? { active: true } : undefined,
      include: {
        stops: { orderBy: { sequence: 'asc' } },
        _count: { select: { stops: true } },
      },
      orderBy: { code: 'asc' },
    });
  }

  async getRoute(id: string) {
    const route = await this.prisma.route.findUnique({
      where: { id },
      include: {
        stops: {
          orderBy: { sequence: 'asc' },
          include: {
            route: false,
          },
        },
      },
    });
    if (!route) throw new NotFoundException('Route not found');

    // Enrich stops with station details
    const stationIds = route.stops.map(s => s.stationId);
    const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
    const stationMap = Object.fromEntries(stations.map(s => [s.id, s]));

    return {
      ...route,
      stops: route.stops.map(s => ({ ...s, station: stationMap[s.stationId] })),
    };
  }

  async updateRoute(id: string, dto: UpdateRouteDto) {
    const route = await this.prisma.route.findUnique({ where: { id } });
    if (!route) throw new NotFoundException('Route not found');

    if (dto.stops && dto.stops.length >= 2) this.validateStopDistances(dto.stops);

    await this.prisma.route.update({
      where: { id },
      data: {
        name: dto.name,
        description: dto.description,
        active: dto.active,
        ...(dto.effectiveFrom ? { effectiveFrom: parseEthiopianTime(dto.effectiveFrom) } : {}),
        // effectiveUntil is nullable (open-ended route) — distinguish "field not sent" (leave
        // untouched) from "explicitly cleared" (null → set to null), not just truthy/falsy.
        ...(dto.effectiveUntil !== undefined
          ? { effectiveUntil: dto.effectiveUntil ? parseEthiopianTime(dto.effectiveUntil) : null }
          : {}),
        ...(dto.checkinMinutesBefore != null ? { checkinMinutesBefore: dto.checkinMinutesBefore } : {}),
      },
    });

    if (dto.stops && dto.stops.length >= 2) {
      await this.prisma.routeStop.deleteMany({ where: { routeId: id } });
      await this.prisma.routeStop.createMany({
        data: dto.stops.map(s => ({
          routeId: id,
          stationId: s.stationId,
          sequence: s.sequence,
          distanceKm: s.distanceKm != null ? parseFloat(String(s.distanceKm)) : null,
          checkinMinutesBefore: s.checkinMinutesBefore ?? null,
          travelMinutesToStop: s.travelMinutesToStop ?? null,
        })),
      });

      // Propagate new stop timing to all future schedules on this route so that
      // per-stop check-in cutoffs reflect the updated travelMinutesToStop values.
      const futureSchedules = await this.prisma.trainSchedule.findMany({
        where: { routeId: id, status: { in: ['SCHEDULED', 'BOARDING'] }, departureAt: { gt: new Date() } },
        select: { id: true, departureAt: true, arrivalAt: true },
      });
      const stopsForTiming = dto.stops
        .map(s => ({ sequence: s.sequence, distanceKm: s.distanceKm ?? null, travelMinutesToStop: s.travelMinutesToStop ?? null, checkinMinutesBefore: s.checkinMinutesBefore ?? null }))
        .sort((a, b) => a.sequence - b.sequence);
      for (const sched of futureSchedules) {
        const times = computePlannedStopTimes({ id, stops: stopsForTiming }, new Date(sched.departureAt), new Date(sched.arrivalAt));
        await this.applyRouteToSchedule(id, sched.id, Object.fromEntries(times.map(t => [t.sequence, t])));
      }
    }

    await this.auditService.log({ action: 'UPDATE', entityType: 'Route', entityId: id, newData: { name: dto.name, active: dto.active } });
    return this.prisma.route.findUnique({
      where: { id },
      include: { stops: { orderBy: { sequence: 'asc' } } },
    });
  }

  async deleteRoute(id: string, cascade = false) {
    const route = await this.prisma.route.findUnique({ 
      where: { id },
      include: {
        schedules: true,
        stops: true
      }
    });
    if (!route) throw new NotFoundException('Route not found');
    
    if (!cascade) {
      const constraints = [];
      if (route.schedules.length > 0) {
        constraints.push({
          entityName: 'schedule',
          count: route.schedules.length,
          action: 'delete' as const
        });
      }
      
      if (constraints.length > 0) {
        throw new DeleteOperationException('Route', `${route.code} (${route.name})`, constraints);
      }
    }
    
    if (cascade) {
      const scheduleIds = route.schedules.map(s => s.id);
      if (scheduleIds.length > 0) {
        await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
        await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
        await this.prisma.tripLiveStatus.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
        await this.prisma.menuItem.deleteMany({ where: { scheduleId: { in: scheduleIds } } });
        await this.prisma.journeySegment.deleteMany({ where: { scheduleId: { in: scheduleIds } } });

        const bookings = await this.prisma.booking.findMany({
          where: { OR: [{ scheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
          select: { id: true },
        });
        if (bookings.length > 0) {
          const bookingIds = bookings.map(b => b.id);
          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 } } });
        }

        const packages = await this.prisma.travelPackage.findMany({
          where: { OR: [{ outboundScheduleId: { in: scheduleIds } }, { returnScheduleId: { in: scheduleIds } }] },
          select: { id: true },
        });
        if (packages.length > 0) {
          const packageIds = packages.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.deleteMany({ where: { id: { in: scheduleIds } } });
      }
      await this.prisma.routeStop.deleteMany({ where: { routeId: id } });
      await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId: id } });
    }

    await this.prisma.route.delete({ where: { id } });
    await this.auditService.log({ action: 'DELETE', entityType: 'Route', entityId: id, oldData: { code: route.code, name: route.name } });
    return { deleted: true, id };
  }

  // ── Route Stops ────────────────────────────────────────────────────────────

  async addStop(routeId: string, dto: AddRouteStopDto) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');

    const station = await this.prisma.station.findUnique({ where: { id: dto.stationId } });
    if (!station) throw new NotFoundException(`Station ${dto.stationId} not found`);
    if (!station.isOperational) throw new BadRequestException(`Station ${dto.stationId} is not operational`);

    const existing = await this.prisma.routeStop.findUnique({
      where: { routeId_sequence: { routeId, sequence: dto.sequence } },
    });
    if (existing) throw new ConflictException(`Sequence ${dto.sequence} already exists on this route`);

    const otherStops = await this.prisma.routeStop.findMany({ where: { routeId } });
    this.validateStopDistances([...otherStops, { sequence: dto.sequence, stationId: dto.stationId, distanceKm: dto.distanceKm }]);

    return this.prisma.routeStop.create({
      data: {
        routeId,
        stationId: dto.stationId,
        sequence: dto.sequence,
        distanceKm: dto.distanceKm != null ? parseFloat(String(dto.distanceKm)) : null,
        checkinMinutesBefore: dto.checkinMinutesBefore ?? null,
        travelMinutesToStop: dto.travelMinutesToStop ?? null,
      },
    });
  }

  async removeStop(routeId: string, sequence: number) {
    const stop = await this.prisma.routeStop.findUnique({
      where: { routeId_sequence: { routeId, sequence } },
    });
    if (!stop) throw new NotFoundException(`Stop at sequence ${sequence} not found on route`);

    const total = await this.prisma.routeStop.count({ where: { routeId } });
    if (total <= 2) throw new BadRequestException('A route must retain at least 2 stops');

    await this.prisma.routeStop.delete({ where: { routeId_sequence: { routeId, sequence } } });
    return { deleted: true, sequence };
  }

  async getStops(routeId: string) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');

    const stops = await this.prisma.routeStop.findMany({
      where: { routeId },
      orderBy: { sequence: 'asc' },
    });

    const stationIds = stops.map(s => s.stationId);
    const stations = await this.prisma.station.findMany({ where: { id: { in: stationIds } } });
    const stationMap = Object.fromEntries(stations.map(s => [s.id, s]));

    return stops.map(s => ({ ...s, station: stationMap[s.stationId] }));
  }

  async getSchedulesForRoute(routeId: string) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');

    return this.prisma.trainSchedule.findMany({
      where: { routeId },
      include: { train: true, originStation: true, destinationStation: true },
      orderBy: { departureAt: 'asc' },
    });
  }

  async getRouteCoachTemplate(routeId: string) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');
    return this.prisma.routeCoachTemplate.findMany({
      where: { routeId },
      include: { coach: { include: { coachType: true } } },
      orderBy: { positionNumber: 'asc' },
    });
  }

  async setRouteCoachTemplate(routeId: string, dto: SetRouteCoachTemplateDto) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');

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

    const positions = dto.coaches.map(c => c.positionNumber);
    if (new Set(positions).size !== positions.length) throw new BadRequestException('Duplicate positionNumber values');

    await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId } });
    await this.prisma.routeCoachTemplate.createMany({
      data: dto.coaches.map(c => ({ routeId, coachId: c.coachId, positionNumber: c.positionNumber })),
    });

    return this.getRouteCoachTemplate(routeId);
  }

  async removeRouteCoachTemplate(routeId: string) {
    const route = await this.prisma.route.findUnique({ where: { id: routeId } });
    if (!route) throw new NotFoundException('Route not found');
    await this.prisma.routeCoachTemplate.deleteMany({ where: { routeId } });
    return { deleted: true, routeId };
  }

  // ── Used by SchedulesService ───────────────────────────────────────────────

  /**
   * Copies RouteStop definitions into TripStopTime rows for a schedule.
   * plannedTimes maps sequence → { arrivalAt?, departureAt? } for actual timing.
   */
  async applyRouteToSchedule(
    routeId: string,
    scheduleId: string,
    plannedTimes: Record<number, { plannedArrivalAt?: string; plannedDepartureAt?: string }>,
  ) {
    const stops = await this.prisma.routeStop.findMany({
      where: { routeId },
      orderBy: { sequence: 'asc' },
    });
    if (stops.length === 0) throw new BadRequestException('Route has no stops defined');

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

    await this.prisma.tripStopTime.createMany({
      data: stops.map(s => {
        const times = plannedTimes[s.sequence] ?? {};
        return {
          scheduleId,
          stationId: s.stationId,
          sequence: s.sequence,
          plannedArrivalAt: times.plannedArrivalAt ? new Date(times.plannedArrivalAt) : null,
          plannedDepartureAt: times.plannedDepartureAt ? new Date(times.plannedDepartureAt) : null,
        };
      }),
    });

    const intermediateCount = Math.max(0, stops.length - 2);
    await this.prisma.trainSchedule.update({
      where: { id: scheduleId },
      data: { stopsCount: intermediateCount },
    });

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