import { Injectable } from '@nestjs/common';
import { Tour } from '@prisma/client';
import {
  addDays,
  differenceInCalendarDays,
  eachDayOfInterval,
  endOfDay,
  format,
  getISODay,
  Interval,
  isSameDay,
  startOfDay,
} from 'date-fns';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';

@Injectable()
export class TourAmountForecastService {
  constructor(private readonly tourService: TourService) {}

  calculateTourAmountForecastFourWeekPeriod(
    ordinanceType: string | undefined,
  ): Promise<Record<string, number[]>> {
    return this.calculateTourAmountForecastForInterval(
      {
        start: startOfDay(addDays(new Date(), -28)),
        end: endOfDay(new Date()),
      },
      ordinanceType,
    );
  }

  async calculateTourAmountForecastForInterval(
    interval: Interval,
    ordinanceType: string | undefined,
  ): Promise<Record<string, number[]>> {
    const toursInInterval = await this.findToursInInterval(
      interval,
      ordinanceType,
    );
    const toursGroupedByDay = this.groupToursByDay(interval, toursInInterval);

    const tourCreationToStartDiffByDay = Object.entries(
      toursGroupedByDay,
    ).reduce(
      (group, [dateString, tours]) => ({
        ...group,
        [dateString]: this.calculateCreationToStartDifferenceOfTours(tours),
      }),
      {} as Record<string, Record<string, number>>,
    );

    const tourCreationToStartDiffsByWeekday = Object.entries(
      tourCreationToStartDiffByDay,
    ).reduce(
      (group, [dateString, diffs]) => {
        const weekday = getISODay(new Date(dateString));
        const existingDiffs = group[weekday] ?? {};
        const dayDifferences = [0, 1, 2, 3, 4, 5, 6, 7];
        const diffsCombined = dayDifferences.reduce(
          (combined, dayDifference) => {
            const diffForDay = diffs[dayDifference];
            return {
              ...combined,
              [dayDifference]: [
                ...(existingDiffs[dayDifference] ?? []),
                diffForDay,
              ],
            };
          },
          {} as Record<string, number[]>,
        );

        return {
          ...group,
          [weekday]: diffsCombined,
        };
      },
      {} as Record<string, Record<string, number[]>>,
    );

    const tourAmountForecastByWeekday = Object.entries(
      tourCreationToStartDiffsByWeekday,
    ).reduce(
      (group, [weekday, diffs]) => {
        const diffsMedian = Object.entries(diffs).reduce(
          (combined, [, tourAmounts]) => {
            const median =
              tourAmounts.reduce(
                (sum, tourAmount) => (sum += tourAmount),
                0.0,
              ) / tourAmounts.length;
            return [...combined, median];
          },
          [] as number[],
        );
        return {
          ...group,
          [weekday]: diffsMedian,
        };
      },
      {} as Record<string, number[]>,
    );

    return tourAmountForecastByWeekday;
  }

  private groupToursByDay(
    interval: Interval,
    tours: Tour[],
  ): Record<string, Tour[]> {
    const daysInInterval = eachDayOfInterval(interval);
    return daysInInterval.reduce(
      (group, day) => {
        return {
          ...group,
          [format(day, 'yyyy-MM-dd')]: tours.filter(({ startDate }) =>
            isSameDay(day, new Date(startDate)),
          ),
        };
      },
      {} as Record<string, Tour[]>,
    );
  }

  private calculateCreationToStartDifferenceOfTours(
    tours: Tour[],
  ): Record<string, number> {
    return tours.reduce(
      (diffs, tour) => {
        const tourCreationToStartDiff = differenceInCalendarDays(
          new Date(tour.startDate),
          new Date(tour.createdAt),
        );
        if (tourCreationToStartDiff > 7 || tourCreationToStartDiff < 0) {
          return diffs;
        }
        return {
          ...diffs,
          [tourCreationToStartDiff]: (diffs[tourCreationToStartDiff] ?? 0) + 1,
        };
      },
      { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0 } as Record<
        string,
        number
      >,
    );
  }

  private findToursInInterval(
    interval: Interval,
    ordinanceType: string | undefined,
  ) {
    return this.tourService.findInInterval(
      interval,
      ordinanceType ? [ordinanceType] : [],
    );
  }
}
