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

@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');
    // Origin and destination must be distinct stops, but EITHER direction is valid: a round-trip
    // return leg traverses the same route high→low (e.g. C→A), so we price the segment by its
    // absolute distance rather than rejecting the reverse order.
    if (originStop.sequence === destStop.sequence)
      throw new BadRequestException('Origin and destination must be different stops on this route');

    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 nationalityUpper = (dto.nationality ?? '').toUpperCase();
    const nationalityType = (nationalityUpper === 'ETHIOPIAN' || nationalityUpper === 'DJIBOUTIAN')
      ? 'LOCAL' : 'INTERNATIONAL';

    const nationalitySeatClass = await this.prisma.seatClass.findFirst({
      where: {
        coachTypeId: seatClass.coachTypeId,
        nationalityType,
        bedPosition: seatClass.bedPosition ?? null,
        isActive: true,
      },
    }) ?? seatClass;

    const totalDistanceKm = Math.abs(destStop.distanceKm! - originStop.distanceKm!);
    if (totalDistanceKm <= 0 || isNaN(totalDistanceKm))
      throw new BadRequestException('Invalid distance calculation - check route stop distances');

    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 fareSource: string;
    let insuranceFactor = 1;
    let usdToEtbRate = 1;
    // When insuranceFeeMinor is used as a multiplier in the formula it must not
    // be added again as a flat fee. This flag tracks that.
    let insuranceAlreadyInBase = false;

    const segmentOverride = await this.prisma.segmentFareRule.findFirst({
      where: {
        routeId: route.id,
        seatClassId: dto.seatClassId,
        originStopSequence: originStop.sequence,
        destinationStopSequence: destStop.sequence,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
        nationality: dto.nationality ?? null,
      },
    }) ?? await this.prisma.segmentFareRule.findFirst({
      where: {
        routeId: route.id,
        seatClassId: dto.seatClassId,
        originStopSequence: originStop.sequence,
        destinationStopSequence: destStop.sequence,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
        nationality: null,
      },
    });

    // Route-level fare override: checked after segment (most specific) but before
    // schedule-scoped rules and the global seat-class tariff (least specific).
    const routeFareOverride = segmentOverride ? null : await this.prisma.routeFareRule.findFirst({
      where: {
        routeId: route.id,
        seatClassId: nationalitySeatClass.id,
        validFrom: { lte: now },
        OR: [{ validUntil: null }, { validUntil: { gte: now } }],
      },
      orderBy: { validFrom: 'desc' },
    });

    if (segmentOverride) {
      baseFarePerPassengerMinor = segmentOverride.baseFareMinor;
      if (nationalityType === 'INTERNATIONAL' && !segmentOverride.nationality) {
        baseFarePerPassengerMinor *= 2;
      }
      ratePerKmMinor = totalDistanceKm > 0 ? Math.round(baseFarePerPassengerMinor / totalDistanceKm) : 0;
      fareSource = 'SEGMENT_FARE_RULE';
    } else if (routeFareOverride) {
      // Stored as a per-km rate (same unit as SeatClass.baseFareMinor × 100).
      // Insurance factor and USD→ETB conversion are applied identically to the
      // global seat-class formula so the override is a pure rate substitution.
      const ratePerKmEtb = routeFareOverride.baseFareMinor / 100;
      insuranceFactor = nationalitySeatClass.insuranceFeeMinor > 0
        ? nationalitySeatClass.insuranceFeeMinor / 100 : 1;
      usdToEtbRate = await this.currencyService.getExchangeRate(Currency.USD, Currency.ETB);
      ratePerKmMinor = routeFareOverride.baseFareMinor;
      baseFarePerPassengerMinor = Math.round(
        totalDistanceKm * ratePerKmEtb * insuranceFactor * usdToEtbRate,
      );
      fareSource = 'ROUTE_FARE_OVERRIDE';
      insuranceAlreadyInBase = true;
    } else if (fareRule?.tripId) {
      baseFarePerPassengerMinor = fareRule.baseFareMinor;
      if (nationalityType === 'INTERNATIONAL' && !fareRule.nationality) {
        baseFarePerPassengerMinor *= 2;
      }
      ratePerKmMinor = totalDistanceKm > 0 ? Math.round(baseFarePerPassengerMinor / totalDistanceKm) : 0;
      fareSource = 'SCHEDULE_FARE_RULE';
    } else {
      // Distance-based formula:
      //   baseFare (minor) = distanceKm × (baseFareMinor / 100) × insuranceFactor × usdToEtbRate
      // baseFareMinor stored as integer (e.g. 300 = 3.00 ETB/km), divided by 100 to get ETB/km.
      // insuranceFeeMinor stored as integer (e.g. 102 = 1.02 multiplier), divided by 100; defaults to 1 if unset.
      // usdToEtbRate fetched live from CurrencyExchangeRate table.
      // Insurance is already baked into baseFarePerPassengerMinor — do NOT add it again as a flat fee.
      const ratePerKmEtb = nationalitySeatClass.baseFareMinor / 100;
      insuranceFactor = nationalitySeatClass.insuranceFeeMinor > 0
        ? nationalitySeatClass.insuranceFeeMinor / 100
        : 1;
      usdToEtbRate = await this.currencyService.getExchangeRate(Currency.USD, Currency.ETB);
      ratePerKmMinor = nationalitySeatClass.baseFareMinor;
      baseFarePerPassengerMinor = Math.round(
        totalDistanceKm * ratePerKmEtb * insuranceFactor * usdToEtbRate,
      );
      fareSource = 'SEAT_CLASS_BASE_FARE';
      insuranceAlreadyInBase = true;
    }

    const premiumPerPassenger    = seatClass.premiumMinor ?? 0;
    const insurancePerPassenger  = insuranceAlreadyInBase ? 0 : (seatClass.insuranceFeeMinor ?? 0);
    const farePerPassengerMinor  = baseFarePerPassengerMinor + premiumPerPassenger + insurancePerPassenger;

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

    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 totalEtbMinor = subtotalMinor - discountMinor;

    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})`,
      `Nationality:    ${dto.nationality ?? 'unspecified'} → ${nationalityType}${nationalityType === 'INTERNATIONAL' ? ' (2× surcharge applied)' : ''} → ${nationalitySeatClass.name}`,
      `Rate per km:    ${routeFareOverride ? routeFareOverride.baseFareMinor : nationalitySeatClass.baseFareMinor} minor → ${(routeFareOverride ? routeFareOverride.baseFareMinor : nationalitySeatClass.baseFareMinor) / 100} ETB/km${routeFareOverride ? ' [ROUTE OVERRIDE]' : ''}`,
      `Insurance:      ${nationalitySeatClass.insuranceFeeMinor} minor → factor ${insuranceFactor}${insuranceAlreadyInBase ? ' (baked into base fare)' : ''}`,
      `USD→ETB rate:   ${usdToEtbRate}`,
      `Base fare/pax:  ${totalDistanceKm} km × (${nationalitySeatClass.baseFareMinor} / 100) × ${insuranceFactor} × ${usdToEtbRate} = ${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 [1 per adult] + ${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`,
      `Total (ETB):    ${totalEtbMinor} ETB minor`,
      ``,
      `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,
      seatClassId: nationalitySeatClass.id,
      seatClassName: nationalitySeatClass.name,
      totalDistanceKm,
      ratePerKmMinor,
      insuranceFactor,
      usdToEtbRate,
      baseFarePerPassengerMinor,
      premiumPerPassenger,
      insurancePerPassenger,
      farePerPassengerMinor,
      adultCount,
      childCount,
      freeChildrenCount,
      paidChildrenCount,
      subtotalMinor,
      discountMinor,
      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 => ({
        seatClassId: rule.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.',
    );
  }
}
