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

    // Resolve fare: FareRule (schedule-scoped → route-scoped) takes precedence over distance×rate
    const now = new Date();
    const [originStation, destStation] = await Promise.all([
      this.prisma.station.findUnique({ where: { id: dto.originStationId } }),
      this.prisma.station.findUnique({ where: { id: dto.destinationStationId } }),
    ]);
    const segmentRoute = originStation && destStation
      ? `${originStation.code}-${destStation.code}` : null;
    const fullRoute = `${route.code}`;

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

    const fareRule = this.pickBestFareRule(
      fareRuleCandidates,
      dto.scheduleId,
      segmentRoute,
      fullRoute,
      dto.nationality,
    );

    let baseFarePerPassengerMinor: number;
    let ratePerKmMinor: number;
    let totalDistanceKm: number;
    let fareSource: string;

    if (fareRule) {
      // Flat fare from FareRule — distance is informational only
      baseFarePerPassengerMinor = fareRule.baseFareMinor;
      totalDistanceKm = legStops.reduce((sum, s) => sum + (s.distanceKm ?? 0), 0);
      ratePerKmMinor = totalDistanceKm > 0 ? Math.round(baseFarePerPassengerMinor / totalDistanceKm) : 0;
      fareSource = fareRule.tripId ? 'SCHEDULE_FARE_RULE' : 'ROUTE_FARE_RULE';
    } else {
      // Distance × rate fallback
      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(', ')}`,
        );
      totalDistanceKm = legStops.reduce((sum, s) => sum + (s.distanceKm ?? 0), 0);
      ratePerKmMinor = seatClass.baseFareMinor;
      baseFarePerPassengerMinor = totalDistanceKm * ratePerKmMinor;
      fareSource = 'DISTANCE_RATE';
    }

    // Premium and insurance fees applied per passenger
    const premiumPerPassenger = seatClass.premiumMinor ?? 0;
    const insurancePerPassenger = seatClass.insuranceFeeMinor ?? 0;
    const farePerPassengerMinor = baseFarePerPassengerMinor + premiumPerPassenger + insurancePerPassenger;

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

    // Subtotal includes: (distance-based fare + premium + insurance) × passengers
    // First child is free, but pays premium and insurance
    const adultSubtotal = farePerPassengerMinor * adultCount;
    const freeChildSubtotal = (premiumPerPassenger + insurancePerPassenger) * freeChildrenCount;
    const paidChildSubtotal = farePerPassengerMinor * paidChildrenCount;
    const subtotalMinor = adultSubtotal + freeChildSubtotal + paidChildSubtotal;

    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 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`,
      `Premium/pax:    ${premiumPerPassenger} ETB minor`,
      `Insurance/pax:  ${insurancePerPassenger} ETB minor`,
      `Total fare/pax: ${farePerPassengerMinor} ETB minor`,
      ``,
      `Adults:         ${adultCount} × ${farePerPassengerMinor} = ${adultSubtotal} ETB minor`,
      `Children:       ${childCount} (${freeChildrenCount} free + ${paidChildrenCount} paid)`,
      `  Free child:   ${freeChildrenCount} × ${premiumPerPassenger + insurancePerPassenger} = ${freeChildSubtotal} ETB minor`,
      `  Paid child:   ${paidChildrenCount} × ${farePerPassengerMinor} = ${paidChildSubtotal} ETB minor`,
      ``,
      `Subtotal:       ${subtotalMinor} ETB minor`,
      `Discount:       ${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`,
      `Fare source:    ${fareSource}`,
    ].join('\n');

    return {
      fareSource,
      routeCode: route.code,
      originName: originStation?.name ?? dto.originStationId,
      destinationName: destStation?.name ?? dto.destinationStationId,
      seatClassName: seatClass.name,
      totalDistanceKm,
      ratePerKmMinor,
      baseFarePerPassengerMinor,
      premiumPerPassenger,
      insurancePerPassenger,
      farePerPassengerMinor,
      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: { baseFareMinor: '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);
  }

  private pickBestFareRule(
    candidates: any[],
    scheduleId?: string,
    segmentRoute?: string | null,
    fullRoute?: string,
    nationality?: string,
  ): any | null {
    const nat = nationality ?? null;
    const priorities = [
      { tripId: scheduleId, route: segmentRoute, nationality: nat },
      { tripId: scheduleId, route: segmentRoute, nationality: null },
      { tripId: scheduleId, route: fullRoute,    nationality: nat },
      { tripId: scheduleId, route: fullRoute,    nationality: null },
      { tripId: scheduleId, route: null,         nationality: nat },
      { tripId: scheduleId, route: null,         nationality: null },
      { tripId: null,       route: segmentRoute, nationality: nat },
      { tripId: null,       route: segmentRoute, nationality: null },
      { tripId: null,       route: fullRoute,    nationality: nat },
      { tripId: null,       route: fullRoute,    nationality: null },
      { tripId: null,       route: null,         nationality: nat },
      { tripId: null,       route: null,         nationality: null },
    ];
    for (const p of priorities) {
      const match = candidates.find(
        c => c.tripId === p.tripId && c.route === p.route && c.nationality === p.nationality,
      );
      if (match) return match;
    }
    return null;
  }

  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,
      scheduleId,
    });
  }

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

    if (schedule.routeId) {
      const seatClasses = await this.prisma.seatClass.findMany({
        where: { isActive: true },
        orderBy: { baseFareMinor: 'asc' },
      });

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

      return results.filter(Boolean);
    }

    const now = new Date();
    const fareRules = await this.prisma.fareRule.findMany({
      where: {
        tripId: scheduleId,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
      },
      orderBy: [{ seatClass: { baseFareMinor: 'asc' } }],
    });

    if (fareRules.length > 0) {
      const billingCurrency = resolveCurrencyFromNationality(nationality);
      const exchangeRate = await this.currencyService.getExchangeRate(Currency.ETB, billingCurrency);
      return fareRules.map(rule => {
        const seatClassId = rule.seatClassId;
        return {
          seatClassId,
          seatClassName: 'Unknown',
          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.',
    );
  }
}
