import { Injectable, Logger } from '@nestjs/common';
import {
  CostType,
  CostTypeVariant,
  Tariff,
  TariffGroup,
  Tour,
} from '@prisma/client';
import { Interval, addDays, isSaturday, isWithinInterval } from 'date-fns';

import { generateIntervalForTimeRange } from 'src/utils';
import { TariffService } from '../feat-business-objects/entities/tariff/tariff.service';
import { TourService } from '../feat-business-objects/entities/tour/tour.service';
import { BILLING_INFORMATION_BY_ORDINANCE_TYPE } from './billing-information-by-ordinance-type';
import {
  calculateDesinfektionszuschlag,
  calculateFahrzeugzuschlag,
  calculateFeiertagszuschlag,
  calculateKilometerzuschlag,
  calculateNachtzuschlag,
  calculatePrivatVersicherung,
  calculateTSW,
  calculateWochenendzuschlag,
} from './cost-type-calculations';
import { PersonnelCostCalculationService } from './personnel-cost-calculation.service';

enum OrdinanceTypes {
  BTW = 'BTW',
  TSW = 'TSW',
  KTW = 'KTW',
}

@Injectable()
export class CalculationService {
  private readonly logger = new Logger(CalculationService.name);

  private healthInsurancesWithNoTenant = [];

  constructor(
    private readonly personnelCostCalculationService: PersonnelCostCalculationService,
    private readonly tariffService: TariffService,
    private readonly tourService: TourService,
  ) {}

  async calculateTourRevenue(tour: Tour): Promise<number> {
    const { healthInsurance, ordinanceType, startDate } = tour;
    if (!ordinanceType) {
      return 0;
    }
    const tariff = await this.findTariff(
      healthInsurance,
      ordinanceType.match(/([A-Z])\w+/).at(0),
      new Date(startDate),
    );
    if (!tariff) {
      this.logger.warn(
        `No tariff found for tour ${tour.operationId} (${healthInsurance}, ${ordinanceType})`,
      );
      return 0;
    }
    const servicePositions = await this.calculateProvidedServicePositions(
      tour,
      tariff,
    );
    return servicePositions.reduce((sum, [, value]) => (sum += value), 0) / 100;
  }

  async listProvidedServicePositions(tour: Tour): Promise<{
    tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup };
    servicePositions: [CostType, number][];
  }> {
    const { healthInsurance, ordinanceType, startDate } = tour;
    if (!ordinanceType) {
      return { tariff: undefined, servicePositions: [] };
    }
    const tariff = await this.findTariff(
      healthInsurance,
      ordinanceType.match(/([A-Z])\w+/).at(0),
      new Date(startDate),
    );
    if (!tariff) {
      this.logger.warn(
        `No tariff found for tour ${tour.operationId} (${healthInsurance}, ${ordinanceType})`,
      );
      return { tariff: undefined, servicePositions: [] };
    }
    return {
      tariff,
      servicePositions: await this.calculateProvidedServicePositions(
        tour,
        tariff,
      ),
    };
  }

  public calculatePersonnelCostInInterval(
    interval: Interval,
    employeeAmount: number,
    ordinanceType: string,
  ): number {
    return this.personnelCostCalculationService.calculatePersonnelCostInInterval(
      interval,
      ordinanceType,
      employeeAmount,
    );
  }

  public getBillingInformationByOrdinanceType(
    ordinanceType: string,
    date: Date,
  ) {
    if (isSaturday(date)) {
      if (ordinanceType.indexOf('KTW') > -1) {
        return {
          personnelCostsPerHour: 64.81 / 2,
          consumptionCostsPerTour: 4.02,
        };
      }
      return {
        personnelCostsPerHour: 23.27,
        consumptionCostsPerTour: 3.68,
      };
    }
    if (ordinanceType.indexOf('KTW') > -1) {
      if (
        isWithinInterval(
          date,
          generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
        )
      ) {
        return {
          personnelCostsPerHour: 68.5 / 2,
          consumptionCostsPerTour: 4.02,
        };
      }
      return {
        personnelCostsPerHour: 56.36 / 2,
        consumptionCostsPerTour: 4.02,
      };
    }

    if (
      isWithinInterval(
        date,
        generateIntervalForTimeRange('18:30-03:00', 'Europe/Berlin', date),
      )
    ) {
      return {
        personnelCostsPerHour: 24.59,
        consumptionCostsPerTour: 3.68,
      };
    }
    return {
      personnelCostsPerHour: 20.23,
      consumptionCostsPerTour: 3.68,
    };
  }

  public getModellBillingInformationByOrdinanceType(
    ordinanceType: string,
    date: Date,
  ) {
    const daysKey = isSaturday(date) ? 'Sa' : 'Mo-Fr';

    if (
      !Object.values(OrdinanceTypes).includes(ordinanceType as OrdinanceTypes)
    ) {
      throw new Error(
        `OrdinanceType not supported. Supported types ${Object.values(
          OrdinanceTypes,
        ).join(', ')}, got ${ordinanceType}`,
      );
    }

    const billingInformationsByIntervals =
      BILLING_INFORMATION_BY_ORDINANCE_TYPE[ordinanceType][daysKey] as Record<
        string,
        {
          personnelCostsPerHour: number;
          consumptionCostsPerTour: number;
          revenuePerTour: number;
        }
      >;
    const billingInformation = Object.entries(
      billingInformationsByIntervals,
    ).find(([timeRange]) =>
      isWithinInterval(
        date,
        generateIntervalForTimeRange(
          timeRange,
          'Europe/Berlin',
          date.getHours() >= 0 && date.getHours() <= 3
            ? addDays(date, -1)
            : date,
        ),
      ),
    );
    if (!billingInformation) {
      this.logger.warn("Couldn't find billing information", {
        date: date.toISOString(),
        ordinanceType,
      });
      throw new Error("Couldn't find billing information");
    }
    return billingInformation[1];
  }

  private async calculateProvidedServicePositions(
    tour: Tour,
    tariff: Tariff & { costTypes: CostType[]; tariffGroup: TariffGroup },
  ): Promise<[CostType, number][]> {
    const highestZuschlag = this.getHighestZuschlag(tour, tariff);
    // if TariffGroup is VDEK KTW, sum up costTypes
    const sumZuschlaege =
      tariff.tariffGroup?.description?.indexOf('VDEK KTW') > -1 ||
      tariff.tariffGroup?.description?.indexOf('DAK KTW') > -1 ||
      tariff.tariffGroup?.description?.indexOf('Privat') > -1;

    return Promise.all(
      tariff.costTypes.map(async (costType) => {
        switch (costType.variant) {
          case CostTypeVariant.GRUNDPREIS:
            return [costType, costType.costAmount];
          case CostTypeVariant.PRIVATVERSICHERUNG:
            return [costType, calculatePrivatVersicherung(tour, costType)];
          case CostTypeVariant.TSWDIFFERENZIERUNG:
            return [costType, calculateTSW(tour, costType)];
          case CostTypeVariant.DESINFEKTIONSZUSCHLAG:
            return [costType, calculateDesinfektionszuschlag(tour, costType)];
          case CostTypeVariant.KILOMETERZUSCHLAG:
            return [costType, calculateKilometerzuschlag(tour, costType)];

          // Currently the only async task in calculation
          case CostTypeVariant.TRAGEHILFE:
            const hasTragehilfe = await this.hasTragehilfe(tour);
            if (hasTragehilfe) return [costType, costType.costAmount];
            return [costType, 0];

          case CostTypeVariant.FAHRZEUGZUSCHLAG:
            return [costType, calculateFahrzeugzuschlag(tour, costType)];
          case CostTypeVariant.GRUNDPREIS_ZEIT:
          case CostTypeVariant.NACHTZUSCHLAG:
            if (
              sumZuschlaege ||
              highestZuschlag === CostTypeVariant.NACHTZUSCHLAG ||
              costType.variant !== CostTypeVariant.NACHTZUSCHLAG
            ) {
              return [costType, calculateNachtzuschlag(tour, costType)];
            }
            return [costType, 0];
          case CostTypeVariant.WOCHENENDZUSCHLAG:
            if (
              highestZuschlag === CostTypeVariant.WOCHENENDZUSCHLAG ||
              sumZuschlaege
            ) {
              return [costType, calculateWochenendzuschlag(tour, costType)];
            }
            return [costType, 0];
          case CostTypeVariant.FEIERTAGSZUSCHLAG:
            if (
              highestZuschlag === CostTypeVariant.FEIERTAGSZUSCHLAG ||
              sumZuschlaege
            ) {
              return [costType, calculateFeiertagszuschlag(tour, costType)];
            }
            return [costType, 0];
        }
      }),
    ).then(
      (servicePositions) =>
        servicePositions.filter(([, value]) => value !== 0) as [
          CostType,
          number,
        ][],
    );
  }

  private getHighestZuschlag(
    tour: Tour,
    tariff: Tariff & { costTypes: CostType[] },
  ): CostTypeVariant {
    const zuschlaegeHighestToLowest = tariff.costTypes
      .filter((costType) =>
        [
          CostTypeVariant.NACHTZUSCHLAG,
          CostTypeVariant.FEIERTAGSZUSCHLAG,
          CostTypeVariant.WOCHENENDZUSCHLAG,
        ].includes(costType.variant as any),
      )
      .map((costType): [CostTypeVariant, number] => {
        switch (costType.variant) {
          case CostTypeVariant.NACHTZUSCHLAG:
            return [
              CostTypeVariant.NACHTZUSCHLAG,
              calculateNachtzuschlag(tour, costType),
            ];
          case CostTypeVariant.WOCHENENDZUSCHLAG:
            return [
              CostTypeVariant.WOCHENENDZUSCHLAG,
              calculateWochenendzuschlag(tour, costType),
            ];
          case CostTypeVariant.FEIERTAGSZUSCHLAG:
            return [
              CostTypeVariant.FEIERTAGSZUSCHLAG,
              calculateFeiertagszuschlag(tour, costType),
            ];
        }
      })
      .sort(
        ([, costAmountA], [, costAmountB]) =>
          (costAmountA as number) - (costAmountB as number),
      )
      .reverse();
    if (zuschlaegeHighestToLowest.length === 0) {
      return undefined;
    }
    return zuschlaegeHighestToLowest[0][0];
  }

  private async findTariff(
    healthInsurance: string,
    ordinanceType: string,
    tourDate: Date,
  ) {
    if (!healthInsurance || healthInsurance === '') {
      return this.tariffService.findByTariffGroupDescription(
        'AOK',
        ordinanceType,
        tourDate,
      );
    }
    const tenant = await this.tariffService.findTenant(
      healthInsurance,
      ordinanceType,
    );
    if (!tenant) {
      this.healthInsurancesWithNoTenant.push(healthInsurance);
      return this.tariffService.findByTariffGroupDescription(
        'AOK',
        ordinanceType,
        tourDate,
      );
    }
    return this.tariffService.findByTenantId(
      tenant.id,
      ordinanceType,
      tourDate,
    );
  }

  private async hasTragehilfe(tour: Tour): Promise<boolean> {
    const tragehilfeTour = await this.tourService.findTragehilfeTour(tour);
    return !!tragehilfeTour;
  }
}
