import { Logger } from '@nestjs/common';
import { Anomaly, Employee, Prisma, Tour } from '@prisma/client';
import {
  Interval,
  addDays,
  addMilliseconds,
  addMinutes,
  differenceInMinutes,
  format,
  isBefore,
  isWithinInterval,
  set,
  subMilliseconds,
} from 'date-fns';
import { getTimezoneOffset, 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 EmployeesLoggedInDetector extends AnomalyDetector {
  private readonly logger = new Logger(EmployeesLoggedInDetector.name);
  readonly type = AnomalyTypes.EmployeesLoggedIn;

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

    const anomaliesByTour = await Promise.all(
      toursInRange.map(async (tour) => {
        return Promise.all([
          this.detectAnomalyForEmployee(tour, 'driver'),
          this.detectAnomalyForEmployee(tour, 'codriver'),
        ]);
      }),
    );
    return anomaliesByTour.filter((anomaly) => !!anomaly).flat();
  }

  async validate(anomaly: Anomaly): Promise<Anomaly> {
    const [operationId, ...remainingIdParts] = anomaly.id.split('-');
    try {
      const employeeId = remainingIdParts.slice(0, -1).join('-');
      const tour = await this.dataService.findTourByOperationId(operationId);
      const employee = await this.dataService.findEmployeeById(employeeId);
      const startOfTour = this.getStartOfTour(
        new Date(tour.startDate),
        [
          tour.startBegin,
          tour.startEnd,
          tour.target,
          tour.targetBegin,
          tour.empty,
        ].filter((val) => !!val)[0],
      );

      if (!startOfTour) return anomaly;

      if (this.validateEmployee(employee)) {
        const [isEmployeeLoggedIn] = await this.isEmployeeLoggedInAtDateTime(
          employee.personnelNumber,
          startOfTour,
        );

        if (isEmployeeLoggedIn) {
          return {
            ...anomaly,
            resolvedAt: new Date(),
          };
        } else {
          return { ...anomaly };
        }
      } else {
        return anomaly;
      }
    } catch (error) {
      this.logger.error(error, { anomalyId: anomaly.id });
      return anomaly;
    }
  }

  createDescription(name: string): string {
    return `${name} ist nicht in der Zeiterfassung angemeldet`;
  }

  createSolution({
    name,
    mobile,
    startOfTour,
    attendanceInterval,
  }: {
    name: string;
    mobile: string;
    startOfTour: Date;
    attendanceInterval: Interval;
  }): Prisma.SolutionTargetCreateInput[] {
    return [
      {
        name: 'Mitarbeiter',
        steps: {
          create: [
            {
              description: `Informieren Sie ${name}, dass er sich einstempeln muss. ${
                mobile ? `(${mobile})` : ''
              }`,
            },
            {
              description: `Zeitpunkt der Tour Anfahrt: ${format(
                addMilliseconds(
                  startOfTour,
                  getTimezoneOffset('Europe/Berlin', startOfTour),
                ),
                'dd.MM.yy HH:mm',
              )}`,
            },
            ...(attendanceInterval
              ? [
                  {
                    description: `Registrierte Anwesenheit: ${format(
                      addMilliseconds(
                        attendanceInterval.start,
                        getTimezoneOffset('Europe/Berlin', startOfTour),
                      ),
                      'dd.MM.yy HH:mm',
                    )}${
                      attendanceInterval.end
                        ? ` - ${format(
                            addMilliseconds(
                              attendanceInterval.end,
                              getTimezoneOffset('Europe/Berlin', startOfTour),
                            ),
                            'HH:mm',
                          )}`
                        : ''
                    }`,
                  },
                ]
              : []),
          ],
        },
      },
      {
        name: 'Dyflexis',
        steps: {
          create: [
            {
              description: `Korrigieren Sie den Zeitstempel auf den Arbeitsbeginn des Mitarbeiters`,
            },
          ],
        },
      },
    ];
  }

  private async detectAnomalyForEmployee(
    {
      id: tourId,
      driverId,
      codriverId,
      startDate,
      startBegin,
      startEnd,
      target,
      targetBegin,
      empty,
      operationId,
    }: Tour,
    driverType: 'driver' | 'codriver',
  ): Promise<Prisma.AnomalyCreateInput | undefined> {
    const employeeId = driverType === 'driver' ? driverId : codriverId;
    if (!employeeId) {
      return Promise.resolve(undefined);
    }

    const employee = await this.dataService.findEmployeeById(employeeId);
    if (!employee) return;

    const startOfTour = this.getStartOfTour(
      new Date(startDate),
      [startBegin, startEnd, target, targetBegin, empty]
        .filter((val) => !!val)
        .at(0),
    );

    if (!startOfTour) return Promise.resolve(undefined);

    if (this.validateEmployee(employee)) {
      const [isEmployeeLoggedIn, checkedInterval, associatedEntities] =
        await this.isEmployeeLoggedInAtDateTime(
          employee.personnelNumber,
          startOfTour,
        );

      if (!isEmployeeLoggedIn) {
        const employeeName = `${employee.name} ${employee.surname}`;
        this.addAssociatedEntity('employee', employee.id);
        this.addAssociatedEntity('tour', tourId);
        Object.entries(associatedEntities).map(([type, entityIds]) =>
          entityIds.map((id) => this.addAssociatedEntity(type, id)),
        );

        return this.createAnomaly(
          `${operationId}-${employeeId}-${this.type}`,
          operationId,
          employeeName,
          employee.mobile,
          startDate,
          startOfTour,
          checkedInterval,
        );
      }
    }
    return Promise.resolve(undefined);
  }

  private validateEmployee(employee: Employee | undefined) {
    if (!employee) {
      this.logger.warn(`Employee does not exist`);
      return false;
    }
    if (!employee.personnelNumber) {
      this.logger.warn(
        `Employee has no personnelNumber ${JSON.stringify({
          id: employee.id,
          surname: employee.surname,
          name: employee.name,
        })}`,
      );
      return false;
    }
    return true;
  }

  private createAnomaly(
    anomalyId: string,
    operationId: string,
    name: string,
    mobile: string,
    startDate: Date,
    startOfTour: Date,
    attendanceInterval: Interval,
  ): 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,
          mobile,
          startOfTour,
          attendanceInterval,
        }),
      },
      description: this.createDescription(name),
    };
  }

  private async isEmployeeLoggedInAtDateTime(
    personnelNumber: string,
    datetimeToCheck: Date,
  ): Promise<[boolean, Interval, Record<string, string[]>]> {
    const workTime = await this.dataService.findWorkTimeByPersonnelNumberForDay(
      personnelNumber,
      datetimeToCheck,
    );
    const attendanceRegistrations =
      await this.dataService.findAttendanceRegistrationByPersonnelNumberForShift(
        personnelNumber,
        datetimeToCheck,
      );

    const firstClockedIn = attendanceRegistrations.filter(
      ({ event }) => event === 'clocked in',
    )[0];
    const lastClockedOut = attendanceRegistrations
      .filter(({ event }) => event === 'clocked out')
      .reverse()[0];

    const datetimeToCheckWithBuffer = addMinutes(datetimeToCheck, 5);

    try {
      if (workTime) {
        const interval = {
          start: new Date(workTime.startDate),
          end: new Date(workTime.endDate),
        };
        return [
          isWithinInterval(datetimeToCheckWithBuffer, interval) ||
            isWithinInterval(datetimeToCheck, interval),
          interval,
          { workTime: [workTime.id] },
        ];
      } else if (
        firstClockedIn &&
        lastClockedOut &&
        isBefore(lastClockedOut.dateTime, firstClockedIn.dateTime)
      ) {
        return [
          isBefore(firstClockedIn.dateTime, datetimeToCheckWithBuffer) ||
            isBefore(firstClockedIn.dateTime, datetimeToCheck),
          {
            start: firstClockedIn.dateTime,
            end: undefined,
          },
          {
            attendanceRegistration: [
              `${firstClockedIn.attendanceRegistrationId}`,
              `${lastClockedOut.attendanceRegistrationId}`,
            ],
          },
        ];
      } else if (firstClockedIn && lastClockedOut) {
        const interval = {
          start: new Date(firstClockedIn.dateTime),
          end: new Date(lastClockedOut.dateTime),
        };

        return [
          isWithinInterval(datetimeToCheckWithBuffer, interval) ||
            isWithinInterval(datetimeToCheck, interval),
          interval,
          {
            attendanceRegistration: [
              `${firstClockedIn.attendanceRegistrationId}`,
              `${lastClockedOut.attendanceRegistrationId}`,
            ],
          },
        ];
      } else if (firstClockedIn && !lastClockedOut) {
        return [
          isBefore(new Date(firstClockedIn.dateTime), datetimeToCheck) ||
            isBefore(
              new Date(firstClockedIn.dateTime),
              datetimeToCheckWithBuffer,
            ),
          { start: firstClockedIn.dateTime, end: undefined },
          {
            attendanceRegistration: [
              `${firstClockedIn.attendanceRegistrationId}`,
            ],
          },
        ];
      } else {
        return [false, undefined, {}];
      }
    } catch (error) {
      this.logger.error(error, {
        timeInQuestion: datetimeToCheck.toISOString(),
        workTime,
        firstClockedIn,
        lastClockedOut,
      });
      return [false, undefined, {}];
    }
  }

  private getStartOfTour(startDate: Date, startBegin: string) {
    if (!startBegin) return null;

    const [hours, minutes] = startBegin.split(':').map((v) => +v);
    let modifiedTime = subMilliseconds(
      set(startDate, { hours, minutes }),
      getTimezoneOffset('Europe/Berlin', startDate),
    );
    if (differenceInMinutes(modifiedTime, startDate) / 60 < -4) {
      modifiedTime = addDays(modifiedTime, 1);
    }

    // this.logger.debug(
    //   `getStartOfTour(${startDate.toISOString()}, ${startBegin}): ${modifiedTime.toISOString()}`,
    // );
    return modifiedTime;
  }
}
