import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { CurrencyService } from '../currency/currency.service';
import { FareCalculateDto, resolveCurrencyFromNationality } from './fare-engine.dto';
import { Currency } from '@prisma/client';

const TAX_RATE = 0.05;

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

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

    const originStop = route.stops.find(s => s.stationId === dto.originStationId);
    const destStop   = route.stops.find(s => s.stationId === dto.destinationStationId);

    if (!originStop) throw new BadRequestException('Origin station not found on this route');
    if (!destStop)   throw new BadRequestException('Destination station not found on this route');
    if (originStop.sequence >= destStop.sequence)
      throw new BadRequestException('Origin must come before destination in the route sequence');

    const legStops = route.stops.filter(
      s => s.sequence > originStop.sequence && s.sequence <= destStop.sequence,
    );

    const missingDistance = legStops.filter(s => s.distanceKm === null || s.distanceKm === undefined);
    if (missingDistance.length > 0)
      throw new BadRequestException(
        `Missing distanceKm on route stops at sequences: ${missingDistance.map(s => s.sequence).join(', ')}`,
      );

    const totalDistanceKm = legStops.reduce((sum, s) => sum + (s.distanceKm ?? 0), 0);

    const seatClass = await this.prisma.seatClass.findUnique({ where: { id: dto.seatClassId } });
    if (!seatClass) throw new NotFoundException('Seat class not found');
    if (!seatClass.isActive) throw new BadRequestException('Seat class is not active');

    const ratePerKmMinor = seatClass.basePrice;
    const baseFarePerPassengerMinor = totalDistanceKm * ratePerKmMinor;

    const adultCount        = dto.adultCount ?? 1;
    const childCount        = dto.childCount ?? 0;
    const freeChildrenCount = Math.min(childCount, 1);
    const paidChildrenCount = Math.max(0, childCount - 1);

    const subtotalMinor =
      baseFarePerPassengerMinor * adultCount +
      baseFarePerPassengerMinor * paidChildrenCount;

    let discountMinor = 0;
    let promoLabel = 'none';
    if (dto.promoCode) {
      const promo = await this.prisma.promotion.findUnique({ where: { code: dto.promoCode } });
      if (promo?.active && promo.validUntil > new Date()) {
        discountMinor = promo.percentOff
          ? Math.round(subtotalMinor * promo.percentOff / 100)
          : (promo.amountOffMinor ?? 0);
        promoLabel = `${dto.promoCode} (-${promo.percentOff ?? 0}%)`;
      }
    }

    const afterDiscountMinor = subtotalMinor - discountMinor;
    const taxMinor = Math.round(afterDiscountMinor * TAX_RATE);
    const totalEtbMinor = afterDiscountMinor + taxMinor;

    const billingCurrency = resolveCurrencyFromNationality(dto.nationality);
    const exchangeRate    = await this.currencyService.getExchangeRate(Currency.ETB, billingCurrency);
    const totalInBillingCurrency = Math.round(totalEtbMinor * exchangeRate);

    const [originStation, destStation] = await Promise.all([
      this.prisma.station.findUnique({ where: { id: dto.originStationId } }),
      this.prisma.station.findUnique({ where: { id: dto.destinationStationId } }),
    ]);

    const calculation = [
      `Distance:       ${totalDistanceKm} km (${originStation?.name} → ${destStation?.name})`,
      `Rate per km:    ${ratePerKmMinor} ETB minor (${seatClass.name})`,
      `Base fare/pax:  ${totalDistanceKm} km × ${ratePerKmMinor} = ${baseFarePerPassengerMinor} ETB minor`,
      `Passengers:     ${adultCount} adult(s) × ${baseFarePerPassengerMinor} = ${baseFarePerPassengerMinor * adultCount} ETB minor`,
      `Children:       ${childCount} child(ren) — ${freeChildrenCount} free, ${paidChildrenCount} paid`,
      `Subtotal:       ${subtotalMinor} ETB minor`,
      `Promo:          ${promoLabel} → -${discountMinor} ETB minor`,
      `Tax (5%):       +${taxMinor} ETB minor`,
      `Total (ETB):    ${totalEtbMinor} ETB minor`,
      `Nationality:    ${dto.nationality ?? 'unspecified'} → ${billingCurrency}`,
      `Exchange rate:  1 ETB = ${exchangeRate} ${billingCurrency}`,
      `Total (${billingCurrency}): ${totalInBillingCurrency} ${billingCurrency} minor`,
    ].join('\n');

    return {
      routeCode: route.code,
      originName: originStation?.name ?? dto.originStationId,
      destinationName: destStation?.name ?? dto.destinationStationId,
      seatClassName: seatClass.name,
      totalDistanceKm,
      ratePerKmMinor,
      baseFarePerPassengerMinor,
      adultCount,
      childCount,
      freeChildrenCount,
      paidChildrenCount,
      subtotalMinor,
      discountMinor,
      taxMinor,
      totalMinor: totalEtbMinor,
      billingCurrency,
      totalInBillingCurrency,
      exchangeRate,
      calculation,
    };
  }

  async compareClasses(
    routeId: string,
    originStationId: string,
    destinationStationId: string,
    nationality?: string,
    adultCount = 1,
    childCount = 0,
  ) {
    const seatClasses = await this.prisma.seatClass.findMany({
      where: { isActive: true },
      orderBy: { basePrice: 'asc' },
    });

    const results = await Promise.all(
      seatClasses.map(sc =>
        this.calculate({ routeId, originStationId, destinationStationId, seatClassId: sc.id, nationality, adultCount, childCount })
          .catch(() => null),
      ),
    );

    return results.filter(Boolean);
  }

  /** Resolve schedule → route/origin/destination, then calculate fare for one seat class. */
  async calculateForSchedule(scheduleId: string, seatClassId: string, nationality?: string) {
    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 this.calculate({
      routeId: schedule.routeId,
      originStationId: schedule.originStationId,
      destinationStationId: schedule.destinationStationId,
      seatClassId,
      nationality,
    });
  }

  /** Calculate fares for all active seat classes on a schedule. */
  async calculateAllForSchedule(scheduleId: string, nationality?: string) {
    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');

    // ── Route-based calculation (fare engine) ────────────────────────────────
    if (schedule.routeId) {
      const seatClasses = await this.prisma.seatClass.findMany({
        where: { isActive: true },
        orderBy: { basePrice: 'asc' },
      });

      const results = await Promise.all(
        seatClasses.map(sc =>
          this.calculate({
            routeId: schedule.routeId!,
            originStationId: schedule.originStationId,
            destinationStationId: schedule.destinationStationId,
            seatClassId: sc.id,
            nationality,
          }).catch(() => null),
        ),
      );

      return results.filter(Boolean);
    }

    // ── Fallback: FareRule records scoped to this schedule ───────────────────
    const now = new Date();
    const fareRules = await this.prisma.fareRule.findMany({
      where: {
        tripId: scheduleId,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
      },
      include: { seatClass: true },
      orderBy: { seatClass: { basePrice: 'asc' } },
    });

    if (fareRules.length > 0) {
      const billingCurrency = resolveCurrencyFromNationality(nationality);
      const exchangeRate = await this.currencyService.getExchangeRate(Currency.ETB, billingCurrency);
      return fareRules.map(rule => ({
        seatClassId: rule.seatClassId,
        seatClassName: rule.seatClass.name,
        baseFareMinor: rule.baseFareMinor,
        totalMinor: rule.baseFareMinor,
        billingCurrency,
        totalInBillingCurrency: Math.round(rule.baseFareMinor * exchangeRate),
        exchangeRate,
        source: 'FARE_RULE',
      }));
    }

    throw new BadRequestException(
      'Schedule has no associated route and no fare rules. Assign a route or create fare rules for this schedule.',
    );
  }
}
