import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { SearchTripsDto, FareQuoteDto } from './search.dto';
import { CurrencyService } from '../currency/currency.service';
import { FareEngineService } from '../fare-engine/fare-engine.service';
import { SegmentsService } from '../segments/segments.service';
import { Currency } from '@prisma/client';

const POINTS_TO_MINOR = 10;

@Injectable()
export class SearchService {
  constructor(
    private prisma: PrismaService,
    private currencyService: CurrencyService,
    private fareEngine: FareEngineService,
    private segmentsService: SegmentsService,
  ) {}

  async searchTrips(dto: SearchTripsDto) {
    const [direct, transit] = await Promise.all([
      this.searchSchedules(
        dto.originStationId,
        dto.destinationStationId,
        dto.date,
        dto.adultCount,
        dto.childCount,
        dto.nationality,
      ),
      this.searchTransitOptions(
        dto.originStationId,
        dto.destinationStationId,
        dto.date,
        dto.adultCount,
        dto.childCount,
        dto.nationality,
      ),
    ]);

    const outbound = [...direct, ...transit];

    if (dto.journeyType === 'ROUND_TRIP') {
      const [returnDirect, returnTransit] = await Promise.all([
        this.searchSchedules(
          dto.destinationStationId,
          dto.originStationId,
          dto.returnDate ?? dto.date,
          dto.adultCount,
          dto.childCount,
          dto.nationality,
        ),
        this.searchTransitOptions(
          dto.destinationStationId,
          dto.originStationId,
          dto.returnDate ?? dto.date,
          dto.adultCount,
          dto.childCount,
          dto.nationality,
        ),
      ]);

      const allReturn = [...returnDirect, ...returnTransit];
      const latestOutboundArrival = outbound.length > 0
        ? Math.max(...outbound.map((s: any) => new Date(s.arrivalAt ?? s.leg2?.arrivalAt).getTime()))
        : Date.now();

      const inbound = allReturn.filter((s: any) =>
        new Date(s.departureAt ?? s.leg1?.departureAt).getTime() > latestOutboundArrival
      );

      return { journeyType: 'ROUND_TRIP', outbound, inbound };
    }

    return { journeyType: 'ONE_WAY', outbound };
  }

  private async searchSchedules(
    originStationId: string,
    destinationStationId: string,
    dateStr: string,
    adultCount: number,
    childCount?: number,
    nationality?: string,
  ) {
    const [y, m, d] = dateStr.split('-').map(Number);
    const date    = new Date(y, m - 1, d, 0, 0, 0, 0);
    const nextDay = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
    const totalPassengers = adultCount + (childCount ?? 0);

    const schedules = await this.prisma.trainSchedule.findMany({
      where: {
        status: { in: ['SCHEDULED', 'BOARDING'] },
        departureAt: { gte: date, lt: nextDay },
        stopTimes: { some: { stationId: originStationId } },
      },
      include: {
        train: true,
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        coachAssignments: {
          include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
        },
      },
    });

    const results: any[] = [];
    for (const schedule of schedules) {
      const result = await this.buildScheduleResult(schedule, originStationId, destinationStationId, totalPassengers, nationality);
      if (result) results.push(result);
    }
    return results;
  }

  // ── Transit search ─────────────────────────────────────────────────────────
  // Finds pairs of schedules (leg1: origin→transit, leg2: transit→destination)
  // where the passenger has between MIN_CONNECTION and MAX_CONNECTION minutes
  // to change trains at the transit station.
  private readonly MIN_CONNECTION_MINUTES = 30;
  private readonly MAX_CONNECTION_MINUTES = 360;

  private async searchTransitOptions(
    originStationId: string,
    destinationStationId: string,
    dateStr: string,
    adultCount: number,
    childCount?: number,
    nationality?: string,
  ) {
    // Find all stations that can serve as transit points:
    // they must be a stop after origin on some schedule AND
    // a stop before destination on another schedule on the same day.
    const [y, m, d] = dateStr.split('-').map(Number);
    const dayStart = new Date(y, m - 1, d, 0, 0, 0, 0);
    const dayEnd   = new Date(y, m - 1, d + 1, 0, 0, 0, 0);
    const totalPassengers = adultCount + (childCount ?? 0);

    // Load all schedules on this date that pass through origin
    const leg1Schedules = await this.prisma.trainSchedule.findMany({
      where: {
        status: { in: ['SCHEDULED', 'BOARDING'] },
        departureAt: { gte: dayStart, lt: dayEnd },
        stopTimes: { some: { stationId: originStationId } },
      },
      include: {
        train: true,
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        coachAssignments: {
          include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
        },
      },
    });

    const results: any[] = [];

    for (const leg1 of leg1Schedules) {
      const originStop = leg1.stopTimes.find((s: any) => s.stationId === originStationId);
      if (!originStop) continue;

      // Every stop after origin on leg1 is a candidate transit station
      const candidateTransitStops = leg1.stopTimes.filter(
        (s: any) => s.sequence > originStop.sequence,
      );

      for (const transitStop of candidateTransitStops) {
        // leg1 must NOT already contain the final destination
        const leg1HasDest = leg1.stopTimes.some((s: any) => s.stationId === destinationStationId);
        if (leg1HasDest) continue; // direct route exists — already returned by searchSchedules

        const transitStationId = transitStop.stationId;
        const leg1ArrivalAt = transitStop.plannedArrivalAt ?? transitStop.plannedDepartureAt ?? leg1.arrivalAt;

        // Find leg2 schedules departing from the transit station within the connection window,
        // and reaching the final destination. Search up to the next calendar day to handle
        // overnight connections.
        const connWindowStart = new Date(new Date(leg1ArrivalAt).getTime() + this.MIN_CONNECTION_MINUTES * 60_000);
        const connWindowEnd   = new Date(new Date(leg1ArrivalAt).getTime() + this.MAX_CONNECTION_MINUTES * 60_000);

        const leg2Schedules = await this.prisma.trainSchedule.findMany({
          where: {
            status: { in: ['SCHEDULED', 'BOARDING'] },
            departureAt: { gte: connWindowStart, lte: connWindowEnd },
            stopTimes: { some: { stationId: transitStationId } },
          },
          include: {
            train: true,
            originStation: true,
            destinationStation: true,
            stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
            coachAssignments: {
              include: { coach: { include: { seats: true, coachType: { include: { seatClasses: true } } } } },
            },
          },
        });

        for (const leg2 of leg2Schedules) {
          const leg2TransitStop = leg2.stopTimes.find((s: any) => s.stationId === transitStationId);
          const leg2DestStop    = leg2.stopTimes.find((s: any) => s.stationId === destinationStationId);

          if (!leg2TransitStop || !leg2DestStop) continue;
          if (leg2TransitStop.sequence >= leg2DestStop.sequence) continue;

          // Build individual leg result objects (reuse existing per-schedule logic)
          const [leg1Result, leg2Result] = await Promise.all([
            this.buildScheduleResult(leg1, originStationId, transitStationId, totalPassengers, nationality),
            this.buildScheduleResult(leg2, transitStationId, destinationStationId, totalPassengers, nationality),
          ]);

          if (!leg1Result || !leg2Result) continue;
          if (!leg1Result.hasAvailability || !leg2Result.hasAvailability) continue;

          const leg2DepartureAt = leg2TransitStop.plannedDepartureAt ?? leg2.departureAt;
          const connectionMinutes = Math.round(
            (new Date(leg2DepartureAt).getTime() - new Date(leg1ArrivalAt).getTime()) / 60_000,
          );

          const leg1MinFare = Math.min(...(leg1Result.faresByClass as any[]).map((f: any) => f.baseFareMinor).filter((n: number) => n > 0), Infinity);
          const leg2MinFare = Math.min(...(leg2Result.faresByClass as any[]).map((f: any) => f.baseFareMinor).filter((n: number) => n > 0), Infinity);
          const combinedMinFareMinor = (isFinite(leg1MinFare) ? leg1MinFare : 0) + (isFinite(leg2MinFare) ? leg2MinFare : 0);

          results.push({
            type: 'TRANSIT',
            transitStationId,
            transitStationName: transitStop.station.name,
            connectionMinutes,
            leg1: leg1Result,
            leg2: leg2Result,
            combinedMinFareMinor,
            // Convenience top-level fields so round-trip filter can read them uniformly
            departureAt: leg1Result.departureAt,
            arrivalAt:   leg2Result.arrivalAt,
            totalDurationMinutes:
              leg1Result.durationMinutes + connectionMinutes + leg2Result.durationMinutes,
          });
        }
      }
    }

    return results;
  }

  // Builds the same result shape as searchSchedules for a single schedule+leg,
  // extracted so both direct and transit paths share identical output.
  private async buildScheduleResult(
    schedule: any,
    originStationId: string,
    destinationStationId: string,
    totalPassengers: number,
    nationality?: string,
  ) {
    const originStop = schedule.stopTimes.find((s: any) => s.stationId === originStationId);
    const destStop   = schedule.stopTimes.find((s: any) => s.stationId === destinationStationId);
    if (!originStop || !destStop || originStop.sequence >= destStop.sequence) return null;

    const availabilityByClass: Record<string, number> = {};
    for (const assignment of schedule.coachAssignments) {
      const seatClassNames = assignment.coach.coachType?.seatClasses?.map((sc: any) => sc.name) || ['Standard'];
      const isBedCoach = assignment.coach.seats.some((s: any) => s.bedPosition);

      if (isBedCoach) {
        for (const bedPosition of ['upper', 'middle', 'lower']) {
          let count = 0;
          for (const seat of assignment.coach.seats) {
            if (seat.bedPosition !== bedPosition || seat.status === 'BLOCKED' || !seat.seatNumber?.trim()) continue;
            const free = await this.segmentsService.isSeatFreeForLeg(schedule.id, seat.id, originStop.sequence, destStop.sequence);
            if (free) count++;
          }
          if (count > 0) {
            const matchingClass = seatClassNames.find((n: string) => n.toLowerCase().includes(bedPosition));
            if (matchingClass) availabilityByClass[matchingClass] = (availabilityByClass[matchingClass] ?? 0) + count;
          }
        }
      } else {
        let available = 0;
        for (const seat of assignment.coach.seats) {
          if (seat.status === 'BLOCKED' || !seat.seatNumber?.trim()) continue;
          const free = await this.segmentsService.isSeatFreeForLeg(schedule.id, seat.id, originStop.sequence, destStop.sequence);
          if (free) available++;
        }
        for (const name of seatClassNames) availabilityByClass[name] = (availabilityByClass[name] ?? 0) + available;
      }
    }

    const faresByClass = await this.calculateFaresForSegment(schedule, originStationId, destinationStationId, nationality);
    const coachTypes   = await this.buildCoachTypeDetails(schedule, faresByClass);
    const legDepartureAt = originStop.plannedDepartureAt ?? schedule.departureAt;
    const legArrivalAt   = destStop.plannedArrivalAt   ?? schedule.arrivalAt;

    return {
      type: 'DIRECT',
      scheduleId: schedule.id,
      trainNumber: schedule.train.number,
      trainName:   schedule.train.name,
      origin: { id: originStop.stationId, code: originStop.station.code, name: originStop.station.name, city: originStop.station.city, sequence: originStop.sequence },
      destination: { id: destStop.stationId, code: destStop.station.code, name: destStop.station.name, city: destStop.station.city, sequence: destStop.sequence },
      departureAt: legDepartureAt,
      arrivalAt:   legArrivalAt,
      durationMinutes: Math.round((new Date(legArrivalAt).getTime() - new Date(legDepartureAt).getTime()) / 60_000),
      status: schedule.status,
      stops: schedule.stopTimes
        .filter((st: any) => st.sequence >= originStop.sequence && st.sequence <= destStop.sequence)
        .map((st: any) => ({ stationId: st.stationId, stationName: st.station.name, sequence: st.sequence, plannedArrivalAt: st.plannedArrivalAt, plannedDepartureAt: st.plannedDepartureAt })),
      availabilityByClass,
      hasAvailability: Object.values(availabilityByClass).some(n => n >= totalPassengers),
      faresByClass,
      coachTypes,
    };
  }

  async getFareQuote(dto: FareQuoteDto) {
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: dto.scheduleId },
      include: {
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
      },
    });
    if (!schedule) throw new NotFoundException('Schedule not found');

    const originStop = schedule.stopTimes.find((s: any) => s.stationId === dto.originStationId);
    const destStop = schedule.stopTimes.find((s: any) => s.stationId === dto.destinationStationId);
    if (!originStop || !destStop || originStop.sequence >= destStop.sequence) {
      throw new NotFoundException('Origin or destination not found on this schedule');
    }

    const seatClass = await this.prisma.seatClass.findFirst({ where: { name: dto.seatClassName } });

    const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
    const fullRoute = `${schedule.originStation.code}-${schedule.destinationStation.code}`;
    const now = new Date();
    const nationality = dto.nationality;

    const candidates = await this.prisma.fareRule.findMany({
      where: {
        seatClassId: seatClass?.id,
        validFrom: { lte: now },
        OR: [
          { validUntil: null },
          { validUntil: { gte: now } },
        ],
      },
    });

    const bestMatch = this.selectBestFareRule(
      candidates,
      dto.scheduleId,
      segmentRoute,
      fullRoute,
      nationality,
    );

    const baseFareMinor = bestMatch?.baseFareMinor
      ?? await this.resolveScheduleFare(dto.scheduleId, seatClass?.id, dto.seatClassName);

    const adultCount = dto.adultCount;
    const childCount = dto.childCount ?? 0;
    const adultFareMinor = baseFareMinor * adultCount;
    const paidChildrenCount = Math.max(0, childCount - 1);
    const childFareMinor = baseFareMinor * paidChildrenCount;
    const totalBaseFareMinor = adultFareMinor + childFareMinor;

    let discountMinor = 0;
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > now) {
        discountMinor = promo.percentOff
          ? Math.round(totalBaseFareMinor * promo.percentOff / 100)
          : (promo.amountOffMinor ?? 0);
      }
    }

    const loyaltyMinor = (dto.loyaltyRedemptionPoints ?? 0) * POINTS_TO_MINOR;
    const taxesMinor = Math.round(totalBaseFareMinor * 0.05);
    const totalMinor = Math.max(0, totalBaseFareMinor - discountMinor - loyaltyMinor + taxesMinor);

    const displayCurrency = dto.displayCurrency ?? Currency.ETB;
    const displayTotalMinor = displayCurrency !== Currency.ETB
      ? await this.currencyService.convertAmount(totalMinor, Currency.ETB, displayCurrency)
      : totalMinor;

    return {
      scheduleId: dto.scheduleId,
      originStationId: dto.originStationId,
      destinationStationId: dto.destinationStationId,
      segmentRoute,
      seatClassName: dto.seatClassName,
      nationality: dto.nationality,
      adultCount, childCount,
      baseFareMinor, adultFareMinor, childFareMinor,
      freeChildrenCount: Math.min(childCount, 1),
      paidChildrenCount, totalBaseFareMinor,
      discountMinor, loyaltyRedemptionMinor: loyaltyMinor,
      taxesFeesMinor: taxesMinor, totalMinor,
      currency: 'ETB', displayCurrency, displayTotalMinor,
    };
  }

  private async calculateFaresForSegment(
    schedule: any,
    originStationId: string,
    destinationStationId: string,
    nationality?: string,
  ): Promise<Array<{ seatClassName: string; baseFareMinor: number }>> {
    const seatClassIds: string[] = Array.from(
      new Set(
        schedule.coachAssignments
          .flatMap((a: any) => a.coach.coachType?.seatClasses || [])
          .map((sc: any) => sc.id)
          .filter((id: any) => id)
      )
    );

    if (seatClassIds.length === 0) {
      console.log(`No seat classes assigned to schedule ${schedule.id}`);
      return [];
    }

    const seatClasses = await this.prisma.seatClass.findMany({
      where: {
        isActive: true,
        id: { in: seatClassIds }
      },
      orderBy: { baseFareMinor: 'asc' },
    });

    if (seatClasses.length === 0) {
      console.log(`No active seat classes for schedule ${schedule.id}`);
      return [];
    }

    if (schedule.routeId) {
      const results = await Promise.all(
        seatClasses.map(async (sc) => {
          try {
            const fare = await this.fareEngine.calculate({
              routeId: schedule.routeId,
              originStationId,
              destinationStationId,
              seatClassId: sc.id,
              nationality,
              scheduleId: schedule.id,
            });
            return {
              seatClassName: fare.seatClassName,
              baseFareMinor: fare.baseFarePerPassengerMinor,
            };
          } catch (error) {
            console.error(`Failed to calculate fare for ${sc.name}:`, (error as Error).message);
            return null;
          }
        }),
      );

      const validResults = results.filter((r): r is { seatClassName: string; baseFareMinor: number } => r !== null);
      if (validResults.length > 0) {
        return validResults;
      }
    }

    const originStation = await this.prisma.station.findUnique({ where: { id: originStationId } });
    const destStation = await this.prisma.station.findUnique({ where: { id: destinationStationId } });

    if (originStation && destStation) {
      const segmentRoute = `${originStation.code}-${destStation.code}`;
      const now = new Date();

      const fareRules = await this.prisma.fareRule.findMany({
        where: {
          route: segmentRoute,
          seatClassId: { in: seatClassIds },
          validFrom: { lte: now },
          OR: [
            { validUntil: null },
            { validUntil: { gte: now } },
          ],
        },
      });

      if (fareRules.length > 0) {
        console.log(`Found ${fareRules.length} fare rules for segment ${segmentRoute}`);
        const seatClassMap = Object.fromEntries(seatClasses.map(sc => [sc.id, sc.name]));
        return fareRules.map(rule => ({
          seatClassName: seatClassMap[rule.seatClassId] || 'Unknown',
          baseFareMinor: rule.baseFareMinor,
        }));
      }
    }

    console.log(`No fares found via engine or rules for ${originStationId} to ${destinationStationId}`);
    return [];
  }

  private async buildCoachTypeDetails(
    schedule: any,
    faresByClass: Array<{ seatClassName: string; baseFareMinor: number }>,
  ): Promise<Array<{
    coachTypeId: string;
    coachTypeName: string;
    coachTypeCode: string;
    coachId: string;
    classes: Array<{ name: string; baseFareMinor: number }>;
  }>> {
    const coachTypeMap = new Map<
      string,
      { coachType: any; classNames: Set<string>; coachId: string }
    >();

    for (const assignment of schedule.coachAssignments) {
      const coachType = assignment.coach.coachType;
      if (!coachType) continue;

      if (!coachTypeMap.has(coachType.id)) {
        coachTypeMap.set(coachType.id, {
          coachType,
          classNames: new Set(),
          coachId: assignment.coach.id,
        });
      }

      const entry = coachTypeMap.get(coachType.id)!;
      coachType.seatClasses?.forEach((sc: any) => entry.classNames.add(sc.name));
    }

    const result = [];
    for (const [, { coachType, classNames, coachId }] of coachTypeMap) {
      const classes = Array.from(classNames)
        .map((className) => {
          const fareInfo = faresByClass.find((f) => f.seatClassName === className);
          if (!fareInfo) return null;
          return { name: className, baseFareMinor: fareInfo.baseFareMinor };
        })
        .filter((c): c is { name: string; baseFareMinor: number } => c !== null)
        .sort((a, b) => a.baseFareMinor - b.baseFareMinor);

      result.push({
        coachTypeId: coachType.id,
        coachTypeName: coachType.name,
        coachTypeCode: coachType.code,
        coachId,
        classes,
      });
    }

    return result.sort((a, b) => {
      const minPriceA = Math.min(...a.classes.map((c) => c.baseFareMinor));
      const minPriceB = Math.min(...b.classes.map((c) => c.baseFareMinor));
      return minPriceA - minPriceB;
    });
  }

  private async resolveScheduleFare(scheduleId: string, seatClassId?: string, seatClassName?: string): Promise<number> {
    if (!seatClassId) throw new NotFoundException(`Seat class '${seatClassName}' not found`);
    const schedule = await this.prisma.trainSchedule.findUnique({
      where: { id: scheduleId },
      select: { routeId: true, originStationId: true, destinationStationId: true },
    });
    if (!schedule?.routeId) throw new NotFoundException('Schedule has no route configured for fare calculation');
    const fare = await this.fareEngine.calculate({
      routeId: schedule.routeId,
      originStationId: schedule.originStationId,
      destinationStationId: schedule.destinationStationId,
      seatClassId,
    });
    return fare.baseFarePerPassengerMinor;
  }

  private getDefaultFareForClass(_className: string): never {
    throw new Error('getDefaultFareForClass should not be called — use resolveScheduleFare instead');
  }

  private defaultFare(_seatClassName: string): never {
    throw new Error('defaultFare should not be called — use resolveScheduleFare instead');
  }

  private selectBestFareRule(
    candidates: any[],
    scheduleId: string,
    segmentRoute: string,
    fullRoute: string,
    nationality?: string,
  ): any | null {
    const priorities = [
      { tripId: scheduleId, route: segmentRoute, nationality },
      { tripId: scheduleId, route: segmentRoute, nationality: null },
      { tripId: scheduleId, route: fullRoute, nationality },
      { tripId: scheduleId, route: fullRoute, nationality: null },
      { tripId: scheduleId, route: null, nationality },
      { tripId: scheduleId, route: null, nationality: null },
      { tripId: null, route: segmentRoute, nationality },
      { tripId: null, route: segmentRoute, nationality: null },
      { tripId: null, route: fullRoute, nationality },
      { tripId: null, route: fullRoute, nationality: null },
      { tripId: null, route: null, nationality },
      { tripId: null, route: null, nationality: null },
    ];

    for (const priority of priorities) {
      const match = candidates.find(
        (c) =>
          c.tripId === priority.tripId &&
          c.route === priority.route &&
          c.nationality === priority.nationality,
      );
      if (match) return match;
    }

    return null;
  }
}
