import { Logger } from '@nestjs/common';
import { Anomaly, Prisma, Tour } from '@prisma/client';
import {
  addDays,
  areIntervalsOverlapping,
  endOfDay,
  format,
  Interval,
  isBefore,
  isSameDay,
  set,
  startOfDay,
} from 'date-fns';
import { fromZonedTime, toZonedTime } from 'date-fns-tz';

import { AnomalyTypes } from 'src/modules/feat-business-objects/entities/anomaly/anomaly-types.enum';
import { AnomalyDetector } from './anomaly-detector.class';

export class OneTourAtATimeDetector extends AnomalyDetector {
  private readonly logger = new Logger(OneTourAtATimeDetector.name);
  readonly type = AnomalyTypes.OneTourAtATime;

  private groupToursByDay(tours: Tour[]): Record<string, Tour[]> {
    return tours.reduce(
      (acc, cur) => {
        const dayString = new Date(cur.startDate).toDateString();
        const group = acc[dayString] ?? [];
        group.push(cur);
        return { ...acc, [dayString]: group };
      },
      {} as Record<string, Tour[]>,
    );
  }

  async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
    const toursInRange = await this.dataService.findToursInInterval(inInterval);

    const toursMap = this.groupToursByDay(toursInRange);
    const result = [];
    for (const [, tours] of Object.entries(toursMap)) {
      const intermediateStep = tours
        .map(
          (
            {
              operationId,
              startDate,
              startBegin,
              startEnd,
              targetBegin,
              target,
              empty,
              carName,
              driverId,
              driverName,
              codriverId,
              codriverName,
            },
            _,
            tours,
          ) => {
            // somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
            const [start] = [startBegin, startEnd].sort();
            // somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
            const [end] = [targetBegin, target, empty]
              .filter((val) => !!val)
              .sort()
              .reverse();

            if (!start || !end) {
              if (isBefore(new Date(), startDate)) {
                this.logger.warn(
                  `Tour ${operationId} started in the past, but is missing start or end time`,
                );
              }
              return [];
            }
            const tourInterval = this.getTourInterval(start, end, startDate);

            const toursInInterval = tours
              .filter(
                (tour) => isSameDay(tour.startDate, startDate),
                // isWithinInterval(tour.startDate, {
                //   start: startOfDay(startDate),
                //   end: endOfDay(startDate),
                // }),
              )
              .filter(
                ({
                  operationId: tourListOperationId,
                  carName: tourListCarName,
                }) =>
                  tourListOperationId !== operationId &&
                  carName !== tourListCarName,
              )
              .filter(
                ({
                  startDate: tourListStartDate,
                  startBegin: tourListStartBegin,
                  startEnd: tourListStartEnd,
                  targetBegin: tourListTargetBegin,
                  target: tourListTarget,
                  empty: tourListEmpty,
                }) => {
                  // somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
                  const [tourListStart] = [
                    tourListStartBegin,
                    tourListStartEnd,
                  ].sort();
                  // somehow empty can be before everything else, so we use targetBegin and empty and take the higher value ...
                  const [tourListEnd] = [
                    tourListTargetBegin,
                    tourListTarget,
                    tourListEmpty,
                  ]
                    .filter((val) => !!val)
                    .sort()
                    .reverse();

                  if (!tourListStart || !tourListEnd) {
                    return false;
                  }

                  const tourListInterval = this.getTourInterval(
                    tourListStart,
                    tourListEnd,
                    tourListStartDate,
                  );
                  try {
                    return areIntervalsOverlapping(
                      tourInterval,
                      tourListInterval,
                    );
                  } catch (error) {
                    this.logger.error(error, {
                      what: 'tourListInterval',
                      tourListInterval,
                      tourInterval,
                    });
                    return false;
                  }
                },
              );

            const toursInIntervalWithDriver = toursInInterval.filter(
              ({
                driverId: tourListDriverId,
                codriverId: tourListCodriverId,
              }) =>
                driverId === tourListDriverId ||
                driverId === tourListCodriverId,
            );
            const toursInIntervalWithCodriver = toursInInterval.filter(
              ({
                driverId: tourListDriverId,
                codriverId: tourListCodriverId,
              }) =>
                codriverId === tourListDriverId ||
                codriverId === tourListCodriverId,
            );

            const overlappingCarsWithDriver = toursInIntervalWithDriver.map(
              ({ carName }) => carName,
            );
            const overlappingTourInfoWithDriver = toursInIntervalWithDriver.map(
              ({ operationId, startDate }) => ({
                startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
                operationId,
              }),
            );
            const overlappingCarsWithCodriver = toursInIntervalWithCodriver.map(
              ({ carName }) => carName,
            );
            const overlappingTourInfoWithCodriver =
              toursInIntervalWithCodriver.map(({ operationId, startDate }) => ({
                startDate: format(new Date(startDate), 'HH:mm dd.MM.yy'),
                operationId,
              }));
            return [
              toursInIntervalWithDriver.length
                ? this.createAnomaly(
                    `${operationId}-${driverId}-${this.type}`,
                    operationId,
                    driverName,
                    startDate,
                    [carName, ...overlappingCarsWithDriver],
                    [
                      {
                        operationId,
                        startDate: format(
                          new Date(startDate),
                          'HH:mm dd.MM.yy',
                        ),
                      },
                      ...overlappingTourInfoWithDriver,
                    ],
                  )
                : undefined,
              toursInIntervalWithCodriver.length
                ? this.createAnomaly(
                    `${operationId}-${codriverId}-${this.type}`,
                    operationId,
                    codriverName,
                    startDate,
                    [carName, ...overlappingCarsWithCodriver],
                    [
                      {
                        operationId,
                        startDate: format(
                          new Date(startDate),
                          'HH:mm dd.MM.yy',
                        ),
                      },
                      ...overlappingTourInfoWithCodriver,
                    ],
                  )
                : undefined,
            ];
          },
        )
        .flat()
        .filter((anomaly) => !!anomaly);
      result.push(intermediateStep);
    }
    return result.flat();
  }

  async validate(anomaly: Anomaly): Promise<Anomaly> {
    const [operationId, ...remainingIdParts] = anomaly.id.split('-');
    const employeeId = remainingIdParts.slice(0, -1).join('-');

    try {
      const tour = await this.dataService.findTourByOperationId(operationId);
      const { startBegin, startEnd, target, targetBegin, empty, startDate } =
        tour;

      // somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
      const [start] = [startBegin, startEnd].sort();
      // somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
      const [end] = [targetBegin, target, empty]
        .filter((val) => !!val)
        .sort()
        .reverse();

      const tourInterval: Interval = this.getTourInterval(
        start,
        end,
        startDate,
      );
      const startOfDayWithTour = startOfDay(startDate);
      const endOfDayWithTour = endOfDay(startDate);

      const toursWithEmployee =
        await this.dataService.findToursInIntervalWithEmployee(
          { start: startOfDayWithTour, end: endOfDayWithTour },
          employeeId,
          'KTW',
        );

      if (
        !toursWithEmployee
          .filter(
            ({ operationId: tourListOperationId }) =>
              operationId !== tourListOperationId,
          )
          .some(
            ({
              startDate: tourListStartDate,
              startBegin: tourListStartBegin,
              startEnd: tourListStartEnd,
              targetBegin: tourListTargetBegin,
              target: tourListTarget,
              empty: tourListEmpty,
            }) => {
              // somehow startBegin can be after empty, so we use startBegin and startEnd and take the lower value and hope for the best
              const [start] = [tourListStartBegin, tourListStartEnd].sort();
              // somehow empty can be before everything else, so we use targetBegin, target and empty and take the higher value ...
              const [end] = [tourListTargetBegin, tourListTarget, tourListEmpty]
                .filter((val) => !!val)
                .sort()
                .reverse();

              const tourListTourInterval = this.getTourInterval(
                start,
                end,
                tourListStartDate,
              );
              return areIntervalsOverlapping(
                tourInterval,
                tourListTourInterval,
              );
            },
          )
      ) {
        return {
          ...anomaly,
          resolvedAt: new Date(),
        };
      }
      return { ...anomaly };
    } catch (error) {
      this.logger.error(error, { anomalyId: anomaly.id });
      return anomaly;
    }
  }

  createDescription({ name }: { name: string }): string {
    return `${name} ist auf mehr als einer Tour gleichzeitig`;
  }

  createSolution({
    name,
    carNames,
    tourInfo,
  }: {
    name: string;
    carNames: string[];
    tourInfo: { operationId: string; startDate: string }[];
  }): Prisma.SolutionTargetCreateInput[] {
    return [
      {
        name: `Mitarbeiter ${name}`,
        steps: {
          create: [
            {
              description: `Sprechen Sie mit ${name} und klären Sie in welchem Fahrzeug er ist`,
            },
            {
              description: `Informieren Sie das Team des Fahrzeugs auf dem der Mitarbeiter nicht ist, dass sie sich neu anmelden müssen`,
            },
            {
              description: `Fahrzeuge mit diesem Mitarbeiter: ${carNames.join(
                ', ',
              )}`,
            },
          ],
        },
      },
      {
        name: 'DispoLive',
        steps: {
          create: [
            {
              description: `Prüfen Sie ob bei den hier aufgelisteten Touren der Fahrer und Beifahrer richtig eingetragen sind`,
            },
            ...tourInfo.map(({ operationId, startDate }) => ({
              description: `Einsatznummer: ${operationId} - ${startDate}`,
            })),
          ],
        },
      },
    ];
  }

  private createAnomaly(
    anomalyId: string,
    operationId: string,
    name: string,
    startDate,
    carNames: string[],
    tourInfo: { operationId: string; startDate: string }[],
  ): Prisma.AnomalyCreateInput {
    return {
      ...this.getAnomalySkeleton(),
      id: anomalyId,
      type: this.type,
      groupKey: `${format(
        new Date(startDate),
        'yyyy-MM-dd-HH-mm',
      )}-${operationId}`,
      groupDescription: `${operationId} - ${format(
        toZonedTime(new Date(startDate), 'Europe/Berlin'),
        'HH:mm dd.MM.yy',
      )}`,
      solution: {
        create: this.createSolution({ name, carNames, tourInfo }),
      },
      description: this.createDescription({ name }),
    };
  }

  private getTourInterval(
    startTime: string,
    endTime: string,
    startDate: Date | string,
  ): Interval {
    return {
      start: this.getStartOfTour(new Date(startDate), startTime),
      end: this.getStartOfTour(new Date(startDate), endTime),
    };
  }

  private getStartOfTour(startDate: Date, startBegin: string) {
    if (!startBegin) return null;
    const zonedTime = toZonedTime(startDate, 'Europe/Berlin');
    const [hours, minutes] = startBegin.split(':').map((v) => +v);
    let baseDate = new Date(zonedTime);
    if (hours >= 0 && hours < 3 && startDate.getHours() > 3) {
      baseDate = addDays(baseDate, 1);
    }
    return fromZonedTime(set(baseDate, { hours, minutes }), 'Europe/Berlin');
  }
}
