import { Injectable } from '@nestjs/common';
import { Interval, getISODay, isSameDay } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';

import { generateIntervalForTimeRange, getHoursInInterval } from 'src/utils';
import { PUBLIC_HOLIDAYS_HH } from './public-holidays';

enum PersonnelCostCondition {
  NACHTZUSCHLAG = 'NACHTZUSCHLAG',
  SAMSTAGSZUSCHLAG = 'SAMSTAGSZUSCHLAG',
  SONNTAGSZUSCHLAG = 'SONNTAGSZUSCHLAG',
  FEIERTAGSZUSCHLAG = 'FEIERTAGSZUSCHLAG',
}

@Injectable()
export class PersonnelCostCalculationService {
  private baseCost: Record<
    'KTW' | 'OTHER',
    {
      base: number;
      withAdditional: number;
    }
  > = {
    KTW: {
      base: 1733,
      withAdditional: 2306,
    },
    OTHER: {
      base: 1440,
      withAdditional: 1797,
    },
  };
  private costTypes = [
    {
      description: 'Nachtzuschlag von 20 Uhr bis 0 Uhr',
      condition: PersonnelCostCondition.NACHTZUSCHLAG,
      percent: 25,
      timeRange: '20:00-23:59',
    },
    {
      description: 'Nachtzuschlag von 0 Uhr bis 4 Uhr',
      condition: PersonnelCostCondition.NACHTZUSCHLAG,
      percent: 40,
      timeRange: '00:00-04:00',
    },
    {
      description: 'Samstagszuschlag',
      condition: PersonnelCostCondition.SAMSTAGSZUSCHLAG,
      percent: 15,
    },
    {
      description: 'Sonntagszuschlag',
      condition: PersonnelCostCondition.SONNTAGSZUSCHLAG,
      percent: 50,
    },
    {
      description: 'Feiertagszuschlag',
      condition: PersonnelCostCondition.FEIERTAGSZUSCHLAG,
      percent: 125,
    },
  ];

  calculatePersonnelCostInInterval(
    interval: Interval,
    ordinanceType: string,
    employeeAmount: number,
  ): number {
    const { base, withAdditional } =
      this.getBaseCostForOrdinanceType(ordinanceType);
    const baseCostInInterval = getHoursInInterval(interval) * withAdditional;
    const additionalCosts = this.calculateAdditionalCosts(interval, base);
    return (baseCostInInterval + additionalCosts) * employeeAmount;
  }

  calculateAdditionalCosts(interval: Interval, baseCost: number): number {
    return this.costTypes
      .map(({ condition, percent, timeRange }) => {
        switch (condition) {
          case PersonnelCostCondition.NACHTZUSCHLAG:
            return this.calculateNachtZuschlag(
              interval,
              baseCost,
              percent,
              timeRange,
            );
          case PersonnelCostCondition.SAMSTAGSZUSCHLAG:
            return this.calculateWeekdayZuschlag(
              interval,
              baseCost,
              percent,
              6,
            );
          case PersonnelCostCondition.SONNTAGSZUSCHLAG:
            return this.calculateWeekdayZuschlag(
              interval,
              baseCost,
              percent,
              7,
            );
          case PersonnelCostCondition.FEIERTAGSZUSCHLAG:
            return this.calculateFeiertagZuschlag(interval, baseCost, percent);
        }
      })
      .reduce((sum, cost) => (sum += cost), 0);
  }

  private calculateNachtZuschlag(
    interval: Interval,
    baseCost: number,
    percent: number,
    timeRange: string,
  ): number {
    const timeRangeIntervalForStart = generateIntervalForTimeRange(
      timeRange,
      'Europe/Berlin',
      new Date(interval.start),
    );
    const timeRangeIntervalForEnd = generateIntervalForTimeRange(
      timeRange,
      'Europe/Berlin',
      new Date(interval.end),
    );
    const costsForStart =
      (getHoursInInterval(interval, timeRangeIntervalForStart) *
        baseCost *
        percent) /
      100;
    const costsForEnd =
      (getHoursInInterval(interval, timeRangeIntervalForEnd) *
        baseCost *
        percent) /
      100;
    return costsForStart > costsForEnd ? costsForStart : costsForEnd;
  }

  private calculateWeekdayZuschlag(
    interval: Interval,
    baseCost: number,
    percent: number,
    weekday: number,
  ): number {
    if (getISODay(toZonedTime(interval.start, 'Europe/Berlin')) === weekday) {
      return (getHoursInInterval(interval) * baseCost * percent) / 100;
    }
    return 0;
  }

  private calculateFeiertagZuschlag(
    interval: Interval,
    baseCost: number,
    percent: number,
  ): number {
    if (
      PUBLIC_HOLIDAYS_HH.some((day) =>
        isSameDay(
          new Date(day),
          new Date(toZonedTime(interval.start, 'Europe/Berlin')),
        ),
      )
    ) {
      return (getHoursInInterval(interval) * baseCost * percent) / 100;
    }
    return 0;
  }

  private getBaseCostForOrdinanceType(ordinanceType: string): {
    base: number;
    withAdditional: number;
  } {
    if (ordinanceType.indexOf('KTW') > -1) {
      return this.baseCost.KTW;
    }
    return this.baseCost.OTHER;
  }
}
