import { Logger } from '@nestjs/common';
import { Anomaly, Prisma } from '@prisma/client';
import { Interval, endOfDay, format, startOfDay } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';

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

export class WorkingEmployeeNotInDistributionSystemDetector extends AnomalyDetector {
  private readonly logger = new Logger(
    WorkingEmployeeNotInDistributionSystemDetector.name,
  );
  readonly type = AnomalyTypes.WorkingEmployeeNotInDistributionSystem;

  async detect(inInterval: Interval): Promise<Prisma.AnomalyCreateInput[]> {
    const attendanceRegistrationInInterval =
      await this.dataService.findAttendanceRegistrationInInterval(inInterval, [
        AttendanceRegistrationEvents.ClockedIn,
      ]);

    const anomalies = await Promise.all(
      attendanceRegistrationInInterval.map(
        async ({
          attendanceRegistrationId,
          // As worktime and plannedtime use the internal employeeId of dyflexis we use it as the personnelNumber entered in dispoLive
          employeeId: personnelNumber,
          dateTime,
        }) => {
          try {
            const employee =
              await this.dataService.findEmployeeByPersonnelNumber(
                `${personnelNumber}`,
              );
            if (!employee) return;

            const plannedTime =
              await this.dataService.findPlannedTimeByPersonnelNumberForDay(
                `${personnelNumber}`,
                dateTime,
              );
            if (
              plannedTime.find(
                ({ department }) =>
                  department.toLowerCase() == 'Büro'.toLowerCase() ||
                  department.toLowerCase() == 'Leitstelle'.toLowerCase(),
              )
            ) {
              return;
            }
            const toursWithEmployee =
              await this.dataService.findToursInIntervalWithEmployee(
                {
                  start: startOfDay(new Date(dateTime)),
                  end: endOfDay(new Date(dateTime)),
                },
                employee.id,
              );
            if (toursWithEmployee.length === 0) {
              this.addAssociatedEntity(
                'attendanceRegistration',
                `${attendanceRegistrationId}`,
              );
              this.addAssociatedEntity('employee', employee.id);
              plannedTime.map(({ id }) =>
                this.addAssociatedEntity('plannedTime', id),
              );

              return this.createAnomaly(
                `${attendanceRegistrationId}-${employee.id}`,
                format(new Date(dateTime), 'yyyy-MM-dd'),
                `${employee.name} ${employee.surname}`,
              );
            }
          } catch (error) {
            this.logger.warn(
              `Employee does not exist! PersonnelNumber: ${personnelNumber}`,
              { error },
            );
            return;
          }
        },
      ),
    );
    return anomalies.filter((anomaly) => !!anomaly);
  }

  async validate(anomaly: Anomaly): Promise<Anomaly> {
    const [attendanceRegistrationId, ...employeeIdParts] =
      anomaly.id.split('-');
    const attendanceRegistration =
      await this.dataService.findAttendanceRegistrationById(
        +attendanceRegistrationId,
      );
    if (!attendanceRegistration) {
      this.logger.error(
        `Cannot verify anomaly ${anomaly.id}. AttendanceRegistration ${attendanceRegistrationId} couldn't be found`,
      );
      return anomaly;
    }
    const employeeId = employeeIdParts.join('-');
    const employee = await this.dataService.findEmployeeById(employeeId);

    if (!employee || !employee?.personnelNumber) {
      this.logger.error(
        `Cannot verify anomaly ${anomaly.id}. Employee (${employeeId}) does not exist or cannot be synced ${JSON.stringify(employee)}`,
      );
      return anomaly;
    }

    const plannedTime =
      await this.dataService.findPlannedTimeByPersonnelNumberForDay(
        `${employee?.personnelNumber}`,
        attendanceRegistration.dateTime,
      );
    if (
      plannedTime.find(
        ({ department }) =>
          department.toLowerCase() == 'Büro'.toLowerCase() ||
          department.toLowerCase() == 'Leitstelle'.toLowerCase(),
      )
    ) {
      return {
        ...anomaly,
        resolvedAt: new Date(),
      };
    }
    const toursWithEmployee =
      await this.dataService.findToursInIntervalWithEmployee(
        {
          start: startOfDay(new Date(attendanceRegistration.dateTime)),
          end: endOfDay(new Date(attendanceRegistration.dateTime)),
        },
        employee.id,
      );
    if (toursWithEmployee.length > 0) {
      return {
        ...anomaly,
        resolvedAt: new Date(),
      };
    }
    return {
      ...anomaly,
    };
  }

  createDescription(name: string): string {
    return `${name} ist keinem Fahrzeug zugewiesen`;
  }

  createSolution(date: string): Prisma.SolutionTargetCreateInput[] {
    return [
      {
        name: `Mitarbeiter ${date}`,
        steps: {
          create: [
            {
              description:
                'Ist der Mitarbeiter teil des Fahrerteams, dann informieren Sie ihn, dass er sich am Fahrzeug anmelden muss',
            },
            {
              description:
                'Ist der Mitarbeiter nicht teil des Fahrerteams, akzeptieren Sie diese Anomalie',
            },
          ],
        },
      },
    ];
  }

  private createAnomaly(
    anomalyId: string,
    dateString: string,
    name: string,
  ): Prisma.AnomalyCreateInput {
    return {
      ...this.getAnomalySkeleton(),
      id: anomalyId,
      type: this.type,
      groupKey: dateString,
      groupDescription: `${format(toZonedTime(new Date(dateString), 'Europe/Berlin'), 'dd.MM.yy')}`,
      solution: {
        create: this.createSolution(
          format(
            toZonedTime(new Date(dateString), 'Europe/Berlin'),
            'dd.MM.yy',
          ),
        ),
      },
      description: this.createDescription(name),
    };
  }
}
