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 date = new Date(dto.date);
    const nextDay = new Date(date.getTime() + 86_400_000);
    const totalPassengers = dto.adultCount + (dto.childCount ?? 0);

    // Find all schedules that have BOTH origin and destination as stops
    // (not just terminal-to-terminal) and depart on the requested date
    const schedules = await this.prisma.trainSchedule.findMany({
      where: {
        status: { in: ['SCHEDULED', 'BOARDING'] },
        departureAt: { gte: date, lt: nextDay },
        stopTimes: { some: { stationId: dto.originStationId } },
      },
      include: {
        train: true,
        originStation: true,
        destinationStation: true,
        stopTimes: { include: { station: true }, orderBy: { sequence: 'asc' } },
        coachAssignments: {
          include: { coach: { include: { seats: true, seatClass: true } } },
        },
      },
    });

    const results = [];

    for (const schedule of schedules) {
      const originStop = schedule.stopTimes.find(s => s.stationId === dto.originStationId);
      const destStop = schedule.stopTimes.find(s => s.stationId === dto.destinationStationId);

      // Both stops must exist and origin must come before destination
      if (!originStop || !destStop || originStop.sequence >= destStop.sequence) continue;

      // Compute per-seat availability for the requested segment range
      // A seat is available if no active booking/hold overlaps [originSeq, destSeq)
      const availabilityByClass: Record<string, number> = {};

      for (const assignment of schedule.coachAssignments) {
        const className = assignment.coach.seatClass.name;
        if (!availabilityByClass[className]) availabilityByClass[className] = 0;

        for (const seat of assignment.coach.seats) {
          if (seat.status === 'BLOCKED') continue;
          // Use segment-aware check — a seat booked A→B is still free for B→D
          const free = await this.segmentsService.isSeatFreeForLeg(
            schedule.id, seat.id,
            originStop.sequence, destStop.sequence,
          );
          if (free) availabilityByClass[className]++;
        }
      }

      // Departure/arrival times for the requested leg (not the full schedule)
      const legDepartureAt = originStop.plannedDepartureAt ?? schedule.departureAt;
      const legArrivalAt = destStop.plannedArrivalAt ?? schedule.arrivalAt;

      // Fetch fares for all seat classes - need to pass the SEARCH origin/destination, not schedule terminals
      const faresByClass = await this.calculateFaresForSegment(
        schedule,
        dto.originStationId,
        dto.destinationStationId,
        dto.nationality,
      );

      results.push({
        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 => st.sequence >= originStop.sequence && st.sequence <= destStop.sequence)
          .map(st => ({
          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,
      });
    }

    return results;
  }

  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 => s.stationId === dto.originStationId);
    const destStop = schedule.stopTimes.find(s => 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 } });

    // Compute route codes for fare lookup
    const segmentRoute = `${originStop.station.code}-${destStop.station.code}`;
    const fullRoute = `${schedule.originStation.code}-${schedule.destinationStation.code}`;
    const now = new Date();
    const nationality = dto.nationality;

    // Query fare rules with specificity ordering:
    // 1. schedule+segment+nationality
    // 2. schedule+segment
    // 3. schedule+full-route+nationality
    // 4. schedule+full-route
    // 5. schedule+global
    // 6. segment+nationality
    // 7. segment
    // 8. full-route+nationality
    // 9. full-route
    // 10. global
    const fareRule = await this.prisma.fareRule.findFirst({
      where: {
        seatClassId: seatClass?.id,
        validFrom: { lte: now },
        OR: [
          { validUntil: null },
          { validUntil: { gte: now } },
        ],
      },
      orderBy: [
        // Prioritize schedule-specific rules
        { tripId: { sort: 'desc', nulls: 'last' } },
        // Then prioritize nationality match
        { nationality: { sort: 'desc', nulls: 'last' } },
        // Most recent validFrom
        { validFrom: 'desc' },
      ],
    });

    // Manual specificity filtering to find best match
    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 ?? this.defaultFare(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,
    };
  }

  /**
   * Calculate fares for a specific segment of a schedule
   */
  private async calculateFaresForSegment(
    schedule: any,
    originStationId: string,
    destinationStationId: string,
    nationality?: string,
  ): Promise<Array<{ seatClassName: string; baseFareMinor: number }>> {
    // Get seat classes that are actually assigned to this schedule via coaches
    const assignedSeatClassIds: string[] = Array.from(
      new Set(
        schedule.coachAssignments.map((a: any) => a.coach.seatClass.id as string)
      )
    );
    
    // Get only the seat classes that are assigned to this schedule
    const seatClasses = await this.prisma.seatClass.findMany({
      where: { 
        isActive: true,
        id: { in: assignedSeatClassIds }
      },
      orderBy: { basePrice: 'asc' },
    });

    // If no coaches assigned, return empty array
    if (seatClasses.length === 0) {
      console.log(`No seat classes assigned to schedule ${schedule.id}`);
      return [];
    }

    // If schedule has a route, use route-based calculation
    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,
            });
            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;
      }
    }

    // Fallback: Try to get fares from FareRule table
    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: assignedSeatClassIds },
          validFrom: { lte: now },
          OR: [
            { validUntil: null },
            { validUntil: { gte: now } },
          ],
        },
        include: { seatClass: true },
      });

      if (fareRules.length > 0) {
        console.log(`Found ${fareRules.length} fare rules for segment ${segmentRoute}`);
        return fareRules.map(rule => ({
          seatClassName: rule.seatClass.name,
          baseFareMinor: rule.baseFareMinor,
        }));
      }
    }

    // Last resort: Return default fares only for assigned seat classes
    console.log(`No fares found, using defaults for ${originStationId} to ${destinationStationId}`);
    return seatClasses.map(sc => ({
      seatClassName: sc.name,
      baseFareMinor: this.getDefaultFareForClass(sc.name),
    }));
  }

  private getDefaultFareForClass(className: string): number {
    const defaults: Record<string, number> = {
      'Economy Regular': 35000,
      'Economy Bed': 49000,
      'VIP Bed': 63000,
    };
    return defaults[className] ?? 35000;
  }

  private defaultFare(seatClassName: string): number {
    const fares: Record<string, number> = {
      'Economy Regular': 45000,
      'Economy Bed': 65000,
      'VIP Bed': 95000,
    };
    return fares[seatClassName] ?? 45000;
  }

  /**
   * Fallback method to get fares from FareRule table when fare engine fails
   */
  private async getFallbackFares(
    scheduleId: string,
    originCode: string,
    destCode: string,
  ): Promise<Array<{ seatClassName: string; baseFareMinor: number }>> {
    const segmentRoute = `${originCode}-${destCode}`;
    const now = new Date();

    // Try to find fare rules for this segment
    const fareRules = await this.prisma.fareRule.findMany({
      where: {
        route: segmentRoute,
        validFrom: { lte: now },
        OR: [
          { validUntil: null },
          { validUntil: { gte: now } },
        ],
      },
      include: { seatClass: true },
    });

    if (fareRules.length > 0) {
      console.log(`Found ${fareRules.length} fare rules for segment ${segmentRoute}`);
      return fareRules.map(rule => ({
        seatClassName: rule.seatClass.name,
        baseFareMinor: rule.baseFareMinor,
      }));
    }

    // If no segment-specific rules, return default fares
    console.log(`No fare rules found for ${segmentRoute}, using defaults`);
    return [
      { seatClassName: 'Economy Regular', baseFareMinor: 35000 },
      { seatClassName: 'Economy Bed', baseFareMinor: 49000 },
      { seatClassName: 'VIP Bed', baseFareMinor: 63000 },
    ];
  }

  /**
   * Select the best matching fare rule based on specificity:
   * 1. schedule+segment+nationality
   * 2. schedule+segment
   * 3. schedule+full-route+nationality
   * 4. schedule+full-route
   * 5. schedule+global
   * 6. segment+nationality
   * 7. segment
   * 8. full-route+nationality
   * 9. full-route
   * 10. global
   */
  private selectBestFareRule(
    candidates: any[],
    scheduleId: string,
    segmentRoute: string,
    fullRoute: string,
    nationality?: string,
  ): any | null {
    const priorities = [
      // Schedule-specific rules
      { 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 },
      // Route-specific rules (no schedule)
      { tripId: null, route: segmentRoute, nationality },
      { tripId: null, route: segmentRoute, nationality: null },
      { tripId: null, route: fullRoute, nationality },
      { tripId: null, route: fullRoute, nationality: null },
      // Global rules
      { 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;
  }
}
