import { Injectable, 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, CreateFareRuleDto, UpdateScheduleStatusDto, UpdateStopTimeDto, ListSchedulesDto } from './schedules.dto';

@Injectable()
export class SchedulesService {
  constructor(
    private prisma: PrismaService,
    private routesService: RoutesService,
    private fareEngine: FareEngineService,
  ) {}

  // ── Schedule CRUD ──────────────────────────────────────────────────────────

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

    if (dto.date) {
      const date = new Date(dto.date);
      const nextDay = new Date(date.getTime() + 86_400_000);
      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) {
    const dep = new Date(dto.departureAt);
    const arr = new Date(dto.arrivalAt);
    if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');

    // Validate route exists and has stops
    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');
    if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');

    // Auto-generate plannedTimes if not provided or empty
    let plannedTimes = dto.plannedTimes;
    if (!plannedTimes || plannedTimes.length === 0) {
      const totalDuration = arr.getTime() - dep.getTime();
      const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
      
      plannedTimes = route.stops.map((stop, index) => {
        let stopTime: Date;
        
        if (index === 0) {
          // First stop - use departure time
          stopTime = dep;
        } else if (index === route.stops.length - 1) {
          // Last stop - use arrival time
          stopTime = arr;
        } else {
          // Intermediate stops - calculate based on distance proportion
          const stopDistance = stop.distanceKm || 0;
          const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
          stopTime = new Date(dep.getTime() + totalDuration * progress);
        }
        
        return {
          sequence: stop.sequence,
          plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
          plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
        };
      });
    }

    // Validate all route stop sequences are covered by plannedTimes
    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(', ')}`);
    }

    // Derive origin and destination from first and last route stop
    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 },
    });

    // Copy route stops into TripStopTime with the provided planned times
    const plannedTimesMap = Object.fromEntries(
      plannedTimes.map(t => [t.sequence, t]),
    );
    await this.routesService.applyRouteToSchedule(dto.routeId, schedule.id, plannedTimesMap);

    return this.getSchedule(schedule.id);
  }

  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' }] }, seatClass: true } } },
          orderBy: { positionNumber: 'asc' },
        },
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');

    // Compute effective seat statuses from SeatHold + JourneySegment
    // (seat.status DB column is no longer written during booking)
    const allSeatIds = schedule.coachAssignments.flatMap(a => a.coach.seats.map(s => s.id));
    const effectiveStatuses = await this.resolveEffectiveStatuses(id, allSeatIds);

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

  /**
   * Computes effective seat status for a schedule by checking active SeatHolds
   * and confirmed JourneySegments. The DB seat.status column is not written
   * during segment-based booking, so this overlay is required.
   * Priority: BLOCKED (physical) > BOOKED (confirmed) > HELD (active hold) > AVAILABLE
   */
  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');

    const dep = new Date(dto.departureAt);
    const arr = new Date(dto.arrivalAt);
    if (arr <= dep) throw new BadRequestException('arrivalAt must be after departureAt');

    // Validate route exists and has stops
    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');
    if (route.stops.length < 2) throw new BadRequestException('Route must have at least 2 stops');

    // Derive origin and destination from first and last route stop
    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),
      },
    });

    // Delete existing stop times and recreate
    await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });

    // Auto-generate plannedTimes if not provided
    let plannedTimes = dto.plannedTimes;
    if (!plannedTimes || plannedTimes.length === 0) {
      const totalDuration = arr.getTime() - dep.getTime();
      const totalDistance = route.stops[route.stops.length - 1].distanceKm || 0;
      
      plannedTimes = route.stops.map((stop, index) => {
        let stopTime: Date;
        
        if (index === 0) {
          stopTime = dep;
        } else if (index === route.stops.length - 1) {
          stopTime = arr;
        } else {
          const stopDistance = stop.distanceKm || 0;
          const progress = totalDistance > 0 ? stopDistance / totalDistance : index / (route.stops.length - 1);
          stopTime = new Date(dep.getTime() + totalDuration * progress);
        }
        
        return {
          sequence: stop.sequence,
          plannedArrivalAt: index === 0 ? undefined : stopTime.toISOString(),
          plannedDepartureAt: index === route.stops.length - 1 ? undefined : stopTime.toISOString(),
        };
      });
    }

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

    return this.getSchedule(id);
  }

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

  async deleteSchedule(id: string) {
    const schedule = await this.prisma.trainSchedule.findUnique({ where: { id } });
    if (!schedule) throw new NotFoundException('Schedule not found');
    
    // Delete related records first (in dependency order)
    await this.prisma.journeySegment.deleteMany({ where: { scheduleId: id } });
    await this.prisma.seatHold.deleteMany({ where: { scheduleId: id } });
    await this.prisma.booking.deleteMany({ where: { scheduleId: id } });
    await this.prisma.tripStopTime.deleteMany({ where: { scheduleId: id } });
    await this.prisma.coachAssignment.deleteMany({ where: { scheduleId: id } });
    
    return this.prisma.trainSchedule.delete({ where: { id } });
  }

  // ── Stop Times (per-schedule overrides) ───────────────────────────────────

  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 ? new Date(dto.plannedArrivalAt) : undefined,
        plannedDepartureAt: dto.plannedDepartureAt ? new Date(dto.plannedDepartureAt) : undefined,
        status: dto.status,
      },
      include: { station: true },
    });
  }

  // ── Fare Rules ─────────────────────────────────────────────────────────────

  createFareRule(dto: CreateFareRuleDto) {
    const { validFrom, validUntil, scheduleId, nationality, ...rest } = dto;
    return this.prisma.fareRule.create({
      data: {
        ...rest,
        tripId: scheduleId,
        nationality,
        validFrom: new Date(validFrom),
        validUntil: validUntil ? new Date(validUntil) : null,
      },
    });
  }

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

  getAllFaresFromEngine(scheduleId: string, nationality?: string) {
    return this.fareEngine.calculateAllForSchedule(scheduleId, nationality);
  }

  /**
   * Recalculate fares for all active seat classes on a schedule using the fare engine
   * and upsert them as FareRule records scoped to this 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; }

        // Expire any existing active rule for this schedule + seat class
        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 };
  }

  // ── Coach Assignments ──────────────────────────────────────────────────────

  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');

    // Validate all coaches exist
    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');
    }

    // Remove existing assignments
    await this.prisma.coachAssignment.deleteMany({ where: { scheduleId } });

    // Create new assignments
    await this.prisma.coachAssignment.createMany({
      data: coaches.map(c => ({
        scheduleId,
        coachId: c.coachId,
        positionNumber: c.positionNumber,
        isOperational: true,
      })),
    });

    return { message: 'Coaches assigned successfully', count: coaches.length };
  }

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

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