import { Injectable } from '@nestjs/common';
import {
  addDays,
  addMilliseconds,
  areIntervalsOverlapping,
  differenceInCalendarDays,
  eachDayOfInterval,
  eachHourOfInterval,
  endOfDay,
  endOfMonth,
  getISODay,
  Interval,
  isSunday,
  set,
  startOfDay,
  startOfMonth,
} from 'date-fns';
import { TourAmountForecastService } from './tour-amount-forecast.service';
import { TimePerTourMetricObjectType } from './manager.object-type';
import { CalculationService } from '../feat-calculation/calculation.service';
import { generateIntervalForTimeRange, groupBy } from 'src/utils';
import { CarEntityService } from '../feat-business-objects/car-entity/car-entity.service';

@Injectable()
export class TimePerTourMetricsService {
  constructor(
    private readonly calculationService: CalculationService,
    private readonly tourAmountForecastService: TourAmountForecastService,
    private readonly carEntityService: CarEntityService,
  ) {}

  async forMonth(date: Date, ordinanceType: string) {
    const fullRangeInterval = {
      start: startOfMonth(date),
      end: endOfMonth(date),
    };

    const subIntervals = eachDayOfInterval(fullRangeInterval)
      .filter((day) => !isSunday(day))
      .reduce(
        (subIntervals, day) => [
          ...subIntervals,
          generateIntervalForTimeRange('05:00-04:59', 'Europe/Berlin', day),
        ],
        [] as Interval[],
      );

    return this.generateTimePerTourMetric(
      fullRangeInterval,
      subIntervals,
      ordinanceType,
    );
  }

  async forDay(date: Date, ordinanceType: string) {
    const fullRangeInterval = generateIntervalForTimeRange(
      '05:00-04:59',
      'Europe/Berlin',
      date,
    );
    const subIntervals = eachHourOfInterval(fullRangeInterval)
      .reduce(
        (subIntervals, hour, index, array) => [
          ...subIntervals,
          ...(index === array.length - 1
            ? []
            : [{ start: hour, end: array[index + 1] }]),
        ],
        [] as Interval[],
      )
      .map(({ start, end }, index, array) => ({
        start,
        end: index + 1 !== array.length ? addMilliseconds(end, -1) : end,
      }));

    return this.generateTimePerTourMetric(
      fullRangeInterval,
      subIntervals,
      ordinanceType,
    );
  }

  private async generateTimePerTourMetric(
    intervalClamp: Interval,
    subIntervals: Interval[],
    ordinanceType: string,
  ): Promise<TimePerTourMetricObjectType[]> {
    const carEntities = await this.carEntityService.getCarsInInterval(
      intervalClamp,
      ordinanceType,
    );

    const forecastPeriod: Interval = {
      start: startOfDay(new Date()),
      end: endOfDay(addDays(new Date(), 7)),
    };
    const forecastForOrdinanceType =
      await this.calculateForecastForOrdinanceType(
        forecastPeriod,
        intervalClamp,
        ordinanceType,
      );

    return Promise.all(
      subIntervals.map(async (interval) => {
        const tourAmount = carEntities.reduce(
          (sum, carEntity) => (sum += carEntity.tourAmountInInterval(interval)),
          0,
        );
        const availableTime = carEntities.reduce(
          (sum, carEntity) =>
            (sum += carEntity.availableTimeInInterval(interval)),
          0,
        );
        let revenue = 0;
        let personnelCost = 0;
        let consumptionCost = 0;

        const categoryAmounts = Object.entries(
          groupBy(
            carEntities.flatMap((ce) => ce.toursInInterval(interval)),
            ({ category }) => category,
          ),
        ).reduce(
          (sum, [category, tours]) => [
            ...sum,
            { category, amount: tours.length },
          ],
          [],
        );
        for (const carEntity of carEntities) {
          const {
            personnelCosts,
            consumptionCost: cc,
            toursRevenue,
          } = await carEntity.breakEvenInInterval(
            interval,
            this.calculationService,
          );
          revenue += toursRevenue;
          personnelCost += personnelCosts;
          consumptionCost += cc;
        }
        let forecast = 0;
        let forecastRevenue = 0;
        if (areIntervalsOverlapping(interval, forecastPeriod)) {
          const dayDiffFromNow = differenceInCalendarDays(
            interval.start,
            new Date(),
          );
          const weekday = getISODay(interval.start);
          const tourAmountAtDateCreatedToday = carEntities.reduce(
            (sum, carEntity) =>
              (sum += carEntity.getTourAmountAtDateCreatedToday(
                new Date(interval.start),
              )),
            0,
          );
          forecast =
            forecastForOrdinanceType[weekday]
              ?.slice(0, dayDiffFromNow + 1)
              .reduce((sum, tourAmount) => (sum += tourAmount), 0) -
            (tourAmountAtDateCreatedToday ?? 0);

          if (forecast >= 0) {
            const billingInformation =
              this.calculationService.getModellBillingInformationByOrdinanceType(
                ordinanceType,
                set(new Date(), {
                  hours: 13,
                  minutes: 0,
                  seconds: 0,
                  milliseconds: 0,
                }),
              );
            forecastRevenue =
              billingInformation.revenuePerTour * Math.round(forecast) -
              billingInformation.consumptionCostsPerTour * Math.round(forecast);
          }
        }

        return {
          interval,
          tourAmount: Math.round(tourAmount * 100) / 100,
          availableTime: Math.round(availableTime * 100) / 100,
          breakEven:
            Math.round((revenue - personnelCost - consumptionCost) * 100) / 100,
          revenue,
          averageRevenue: isNaN(revenue / tourAmount)
            ? 0
            : revenue / tourAmount,
          personnelCost,
          consumptionCost,
          forecast: forecast < 0 || isNaN(forecast) ? 0 : forecast,
          forecastRevenue,
          categoryAmounts,
        };
      }),
    );
  }

  private async calculateForecastForOrdinanceType(
    forecastPeriod: Interval,
    requestedInterval: Interval,
    ordinanceType: string,
  ): Promise<Record<string, number[]>> {
    if (areIntervalsOverlapping(requestedInterval, forecastPeriod)) {
      return this.tourAmountForecastService.calculateTourAmountForecastFourWeekPeriod(
        ordinanceType,
      );
    }
    return {};
  }
}
