import { HttpService } from '@nestjs/axios';
import { Injectable, Logger } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import {
  Interval,
  eachDayOfInterval,
  endOfDay,
  format,
  startOfDay,
} from 'date-fns';
import {
  Observable,
  combineLatest,
  concatMap,
  delay,
  forkJoin,
  from,
  map,
  of,
  reduce,
  switchMap,
} from 'rxjs';
import { EmployeeDispoLiveRaw, PatientRaw, TourRaw } from './source.interface';
import { SourceRepository } from './source.repository';
import { chunk } from 'src/utils';
import { ConfigService } from 'src/core/config/config.service';
import { TourExtractor } from './extractors/tour.extractor';

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

  constructor(
    private readonly http: HttpService,
    private readonly repository: SourceRepository,
    private readonly config: ConfigService,
  ) {}

  extractTour(id: string, jobId: string) {
    return this.fetchTour(id).pipe(map((tour) => ({ ...tour, jobId })));
  }

  extractTours(
    interval: Interval,
    jobId: string,
  ): Observable<Prisma.BatchPayload> {
    const intervals = chunk(eachDayOfInterval(interval), 30).map((dates) => ({
      start: startOfDay(dates.at(0)),
      end: endOfDay(dates.at(-1)),
    }));

    return this.fetchTours(intervals).pipe(
      map((rawTours) => {
        return rawTours;
      }),
      switchMap((rawTours) =>
        from(this.repository.createTours(rawTours, jobId)),
      ),
    );
  }

  private fetchTour(id: string): Observable<TourRaw> {
    const fahrberichteEndpoint = 'data/fahrberichte';
    const carsEndpoint = 'data/stammdatenFahrzeuge';

    const fetchCars = (
      authCookies: string,
    ): Observable<{ id: string; name: string }[]> =>
      this.http
        .post<{ data: { _id: string; name: string }[] }>(
          `${this.config.dispoliveBaseUrl()}/${carsEndpoint}`,
          {},
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) =>
            response.data.data.map(({ _id, name }) => ({ id: _id, name })),
          ),
        );

    const fetchTour = (id: string, authCookies: string) =>
      this.http
        .post<{ data: TourRaw[] }>(
          `${this.config.dispoliveBaseUrl()}/${fahrberichteEndpoint}`,
          {
            filter: { _id: id },
          },
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) => TourExtractor.extract(response.data.data?.at(0))),
        );

    return this.dispoliveLogin(
      this.config.dispoliveUsername(),
      this.config.dispolivePassword(),
    ).pipe(
      switchMap((authCookies) =>
        forkJoin([of(authCookies), fetchCars(authCookies)]),
      ),
      switchMap(([authCookies, cars]) =>
        fetchTour(id, authCookies).pipe(
          map(({ carNo, ...tour }) => ({
            ...tour,
            carNo,
            carName: cars.find(({ id }) => id === carNo)?.name ?? tour.carName,
          })),
        ),
      ),
    );
  }

  private fetchTours(intervals: Interval[]): Observable<TourRaw[]> {
    const archiveEndpoint = 'custom/getArchive/getArchive';
    const carsEndpoint = 'data/stammdatenFahrzeuge';

    const fetchCars = (
      authCookies: string,
    ): Observable<{ id: string; name: string }[]> =>
      this.http
        .post<{ data: { _id: string; name: string }[] }>(
          `${this.config.dispoliveBaseUrl()}/${carsEndpoint}`,
          {},
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) =>
            response.data.data.map(({ _id, name }) => ({ id: _id, name })),
          ),
        );

    const fetchToursInInterval = (interval: Interval, authCookies: string) =>
      this.http
        .post<TourRaw[]>(
          `${this.config.dispoliveBaseUrl()}/${archiveEndpoint}`,
          {
            dateFrom: format(new Date(interval.start), 'yyyy-MM-dd'),
            dateTo: format(new Date(interval.end), 'yyyy-MM-dd'),
          },
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) =>
            response.data.map((tour) => TourExtractor.extract(tour)),
          ),
        );

    return this.dispoliveLogin(
      this.config.dispoliveUsername(),
      this.config.dispolivePassword(),
    ).pipe(
      switchMap((authCookies) =>
        forkJoin([of(authCookies), fetchCars(authCookies)]),
      ),
      switchMap(([authCookies, cars]) =>
        from(intervals).pipe(
          delay(300),
          concatMap((interval) => fetchToursInInterval(interval, authCookies)),
          reduce((full, tours) => [...full, ...tours], []),
          map((tours) =>
            tours.map(({ carNo, ...tour }) => {
              return {
                ...tour,
                carNo,
                carName:
                  cars.find(({ id }) => id === carNo)?.name ?? tour.carName,
              };
            }),
          ),
        ),
      ),
    );
  }

  extractEmployees(jobId: string): Observable<Prisma.BatchPayload> {
    return this.dispoliveLogin(
      this.config.dispoliveUsername(),
      this.config.dispolivePassword(),
    ).pipe(
      switchMap((authCookies) => this.fetchEmployees(authCookies)),
      switchMap((rawEmployees) =>
        from(this.repository.createEmployeesDispoLive(rawEmployees, jobId)),
      ),
    );
  }

  private fetchEmployees(
    authCookies: string,
  ): Observable<EmployeeDispoLiveRaw[]> {
    const employeeEndpoint = 'data/mitarbeiter';

    return this.http
      .post<{ data: EmployeeDispoLiveRaw[] }>(
        `${this.config.dispoliveBaseUrl()}/${employeeEndpoint}`,
        { sort: { surname: 1, status_name: -1 } },
        {
          headers: {
            'Content-Type': 'application/json',
            Cookie: authCookies,
          },
        },
      )
      .pipe(
        map((response) =>
          response.data.data.map((employee) => {
            const {
              _id,
              pScheinAblaufDatum,
              name,
              surname,
              persId,
              importPersId,
              qualifikation,
            } = employee;
            return {
              id: _id,
              pScheinAblaufDatum,
              name,
              surname,
              persId,
              importPersId,
              qualifikation:
                !!qualifikation && typeof qualifikation === 'object'
                  ? //@ts-expect-error ts is wrong here
                    qualifikation?._id
                  : (qualifikation ?? undefined),
            };
          }),
        ),
      );
  }

  extractPatients(jobId: string): Observable<Prisma.BatchPayload> {
    return this.dispoliveLogin(
      this.config.dispoliveUsername(),
      this.config.dispolivePassword(),
    ).pipe(
      switchMap((authCookies) => this.fetchPatients(authCookies)),
      switchMap((rawPatients) =>
        from(this.repository.createPatients(rawPatients, jobId)),
      ),
    );
  }

  private fetchPatients(authCookies: string): Observable<PatientRaw[]> {
    const patientEndpoint = 'data/patientenStammdaten';
    const careDegreeEndpoint = 'data/pflegestufe';
    const disabilityMarkEndpoint = 'data/schwerbehindertenausweis';

    const fetchCareDegrees = (
      authCookies: string,
    ): Observable<{ id: string; careDegree: string }[]> =>
      this.http
        .post<{ data: { _id: string; pflegestufe: string }[] }>(
          `${this.config.dispoliveBaseUrl()}/${careDegreeEndpoint}`,
          {},
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) =>
            response.data.data.map(({ _id, pflegestufe }) => ({
              id: _id,
              careDegree: pflegestufe,
            })),
          ),
        );
    const fetchDisabilityMarks = (
      authCookies: string,
    ): Observable<{ id: string; disabilityMark: string }[]> =>
      this.http
        .post<{ data: { _id: string; behinderungShortcut: string }[] }>(
          `${this.config.dispoliveBaseUrl()}/${disabilityMarkEndpoint}`,
          {},
          {
            headers: {
              'Content-Type': 'application/json',
              Cookie: authCookies,
            },
          },
        )
        .pipe(
          map((response) =>
            response.data.data.map(({ _id, behinderungShortcut }) => ({
              id: _id,
              disabilityMark: behinderungShortcut,
            })),
          ),
        );

    return combineLatest([
      fetchCareDegrees(authCookies),
      fetchDisabilityMarks(authCookies),
    ]).pipe(
      switchMap(([careDegrees, disabilityMarks]) =>
        this.http
          .post<{ data: PatientRaw[] }>(
            `${this.config.dispoliveBaseUrl()}/${patientEndpoint}`,
            { sort: { surname: 1, status_name: -1 } },
            {
              headers: {
                'Content-Type': 'application/json',
                Cookie: authCookies,
              },
            },
          )
          .pipe(
            map((response) =>
              response.data.data.map((patient) => {
                const {
                  _id,
                  name,
                  surname,
                  street,
                  zip,
                  city,
                  birthday,
                  kkId,
                  krankenkasse,
                  pflegestufe,
                  behinderung1,
                  behinderung2,
                } = patient;
                return {
                  id: _id,
                  name,
                  surname,
                  street,
                  zip,
                  city,
                  birthday,
                  kkId,
                  krankenkasse,
                  pflegestufe: careDegrees.find(({ id }) => id === pflegestufe)
                    ?.careDegree,
                  behinderung1: disabilityMarks.find(
                    ({ id }) => id === behinderung1,
                  )?.disabilityMark,
                  behinderung2: disabilityMarks.find(
                    ({ id }) => id === behinderung2,
                  )?.disabilityMark,
                };
              }),
            ),
          ),
      ),
    );
  }

  private dispoliveLogin(
    username: string,
    password: string,
  ): Observable<string> {
    const endpoint = 'login';

    return this.http
      .post(
        `${this.config.dispoliveBaseUrl()}/${endpoint}`,
        {
          username,
          password,
        },
        {
          headers: {
            'Content-Type': 'application/json',
          },
        },
      )
      .pipe(
        map((response) =>
          response.headers['set-cookie']
            // To retrieve only the necessary information of the 'set-cookie' header, we combine them with the same separator, which is used inside one 'set-cookie' request (e.g "key=value; path=/; httponly")
            .join(';')
            .split(';')
            .filter((cookie) => cookie.indexOf('koa.sid') > -1)
            .join('; '),
        ),
      );
  }
}
