import { Inject, Injectable } from '@nestjs/common';
import { Prisma, Tour } from '@prisma/client';
import { endOfDay, Interval, startOfDay } from 'date-fns';
import { Kysely } from 'kysely';
import { KYSELY_CLIENT_PROVIDER_TOKEN } from 'src/core/database/database.module';
import { PrismaService } from 'src/core/database/prisma.service';
import { DB } from 'src/core/database/types';
import { AccountingState } from 'src/modules/app-accounting/accounting-state';
import { chunk } from 'src/utils';
import {
  createPrismaQueryFromAccountingFilters,
  TourAccountingFilterDto,
} from './tours.filter';

function onlyRealTours(): Prisma.TourWhereInput[] {
  return [
    {
      OR: [
        {
          carName: null,
        },
        {
          AND: [
            {
              carName: {
                not: 'Storno',
              },
            },
            {
              carName: {
                not: 'Löschen',
              },
            },
            {
              carName: {
                not: 'Abgabe',
              },
            },
          ],
        },
      ],
    },
    {
      check: {
        not: 3,
      },
    },
    {
      deletedAt: null,
    },
    {
      OR: [
        {
          patientId: null,
        },
        {
          AND: [
            {
              patientId: {
                // Pause
                not: 'b7424477-a4b2-42f7-b7e7-a4bef24f6979',
              },
            },
            {
              patientId: {
                // Feierabend
                not: '3554e1a9-9c07-45e5-b53c-692a77e4fa9a',
              },
            },
          ],
        },
      ],
    },
  ];
}

@Injectable()
export class TourRepository {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(KYSELY_CLIENT_PROVIDER_TOKEN) private readonly db: Kysely<DB>,
  ) {}

  async findByOperationId(operationId: string): Promise<Tour> {
    return await this.prisma.tour.findFirst({
      where: { operationId },
    });
  }

  async findById(tourId: string): Promise<Tour> {
    return await this.prisma.tour.findUnique({
      where: { id: tourId },
    });
  }

  async findInInterval(
    interval: Interval,
    ordinanceTypes: string[] = [],
  ): Promise<Tour[]> {
    return await this.prisma.tour.findMany({
      where: {
        AND: [
          {
            startDate: {
              gte: new Date(interval.start),
            },
          },
          {
            startDate: {
              lt: new Date(interval.end),
            },
          },
          {
            OR: [
              ...(ordinanceTypes.length
                ? ordinanceTypes.map((ot) => ({
                    ordinanceType: { contains: ot },
                  }))
                : []),
            ],
          },
          ...onlyRealTours(),
        ],
      },
      orderBy: {
        startDate: 'desc',
      },
    });
  }

  async findUpdatedAtBetween(
    firstId: string,
    lastId: string,
  ): Promise<{ id: string; updatedAt: Date }[]> {
    return await this.db
      .with('boundary_dates', (db) =>
        db
          .selectFrom('Tour')
          .select('startDate')
          .where('id', 'in', [firstId, lastId]),
      )
      .selectFrom('Tour')
      .select(['Tour.id', 'operationId', 'updatedAt'])
      .where((eb) =>
        eb.and([
          eb('Tour.startDate', '>=', (eb) =>
            eb
              .selectFrom('boundary_dates')
              .select(eb.fn.min('startDate').as('min_date')),
          ),
          eb('Tour.startDate', '<=', (eb) =>
            eb
              .selectFrom('boundary_dates')
              .select(eb.fn.max('startDate').as('max_date')),
          ),
        ]),
      )
      .orderBy('Tour.updatedAt', 'asc')
      .execute();
  }

  public findInIntervalWithEmployee(
    { start, end }: Interval,
    employeeId: string,
    ordinanceTypes: string[] = [],
  ): Promise<Tour[]> {
    return this.prisma.tour.findMany({
      where: {
        AND: [
          {
            OR: [
              {
                driverId: employeeId,
              },
              {
                codriverId: employeeId,
              },
            ],
          },
          {
            startDate: {
              gte: new Date(start),
            },
          },
          {
            startDate: {
              lt: new Date(end),
            },
          },
          {
            OR: [
              ...(ordinanceTypes.length
                ? ordinanceTypes.map((ot) => ({
                    ordinanceType: { contains: ot },
                  }))
                : []),
            ],
          },
          ...onlyRealTours(),
        ],
      },
    });
  }

  async findByTicketId(ticketId: string): Promise<Tour[]> {
    return await this.prisma.tour.findMany({
      where: { ticketId },
    });
  }

  async findTragehilfeTour(tour: Tour): Promise<Tour | null> {
    const {
      patientId,
      patientName,
      patientSurname,
      patientCity,
      patientStreet,
      patientZip,
    } = tour;
    return await this.prisma.tour.findFirst({
      where: {
        patientId,
        patientName,
        patientSurname,
        patientStreet,
        patientCity,
        patientZip,
        ordinanceType: 'Tragehilfe',
        AND: [
          {
            startDate: {
              gte: startOfDay(new Date(tour.startDate)),
            },
          },
          {
            startDate: {
              lt: endOfDay(new Date(tour.startDate)),
            },
          },
        ],
      },
    });
  }

  async createTour(tourInput: Prisma.TourCreateInput): Promise<Tour> {
    return await this.prisma.tour.create({
      data: {
        ...tourInput,
      },
    });
  }

  async updateTour(
    id: string,
    tourInput: Prisma.TourUpdateInput,
  ): Promise<Tour> {
    return await this.prisma.tour.update({
      where: { id },
      data: {
        ...tourInput,
      },
    });
  }

  async deleteTours(ids: string[]): Promise<Prisma.BatchPayload[]> {
    const chunks = chunk(ids, 2000);
    return await Promise.all(
      chunks.map((chunk) =>
        this.prisma.tour.updateMany({
          where: {
            id: {
              in: chunk,
            },
          },
          data: {
            deletedAt: new Date(),
          },
        }),
      ),
    );
  }

  // -------------------- Ticket System ------------------- //
  async findFirstRecurring(tour: Tour): Promise<Tour | null> {
    const institutionStreet =
      tour.direction === 'Hinfahrt' ? tour.targetStreet : tour.startStreet;
    return await this.prisma.tour.findFirst({
      where: {
        type: 'Serie',
        category: tour.category,
        ordinanceType: tour.ordinanceType,
        patientId: tour.patientId,
        patientName: tour.patientName,
        patientSurname: tour.patientSurname,
        patientStreet: tour.patientStreet,
        patientCity: tour.patientCity,
        patientZip: tour.patientZip,
        OR: [
          { targetStreet: institutionStreet },
          { startStreet: institutionStreet },
        ],
        rangeEndDate: {
          gte: tour.startDate,
        },
        ticketId: {
          not: null,
        },
      },
      orderBy: {
        startDate: 'desc',
      },
    });
  }

  async findOnSameDayWithOppositeDirection(tour: Tour): Promise<Tour | null> {
    return await this.prisma.tour.findFirst({
      where: {
        AND: [
          {
            startDate: {
              gt: startOfDay(new Date(tour.startDate)),
            },
          },
          {
            startDate: {
              lt: endOfDay(new Date(tour.startDate)),
            },
          },
          {
            startStreet: tour.targetStreet,
          },
          {
            targetStreet: tour.startStreet,
          },
          {
            direction: tour.direction === 'Hinfahrt' ? 'Rückfahrt' : 'Hinfahrt',
          },
        ],
      },
    });
  }

  async findWithTicket(interval: Interval): Promise<Tour[]> {
    return await this.prisma.tour.findMany({
      where: {
        AND: [
          {
            ticketId: {
              not: null,
            },
          },
          {
            startDate: {
              gte: new Date(interval.start),
            },
          },
          {
            startDate: {
              lt: new Date(interval.end),
            },
          },
        ],
      },
    });
  }

  async removeTourFromTicket(tourId: string): Promise<Tour> {
    return await this.prisma.tour.update({
      where: {
        id: tourId,
      },
      data: {
        Ticket: {
          disconnect: true,
        },
      },
    });
  }

  // --------------------- Accounting --------------------- //
  async findByAccountingState(
    state: AccountingState,
    filter: TourAccountingFilterDto,
    take?: number,
    skip?: number,
  ): Promise<{ tours: Tour[]; total: number }> {
    const checkValue = this.getCheckValueForAccountingState(state);
    const query: Prisma.TourFindManyArgs = {
      ...{
        ...(skip ? { skip, take: 10 } : {}),
        ...(take ? { take } : {}),
        ...(take && !skip ? { skip: 0 } : {}),
      },
      where: {
        check: checkValue,
        ...(checkValue === 2 && !filter?.deviation
          ? { revenueDeviation: { not: 0 } }
          : {}),
        empty: {
          not: null,
        },
        ticketId: {
          not: null,
        },
        AND: [
          ...createPrismaQueryFromAccountingFilters(filter),
          ...onlyRealTours(),
        ],
      },
      orderBy: { createdAt: 'desc' },
    };

    const [tours, count] = await this.prisma.$transaction([
      this.prisma.tour.findMany({ ...query }),
      this.prisma.tour.count({ where: query.where }),
    ]);
    return {
      tours,
      total: count,
    };
  }

  async calculateAccountingStateMeta(state: AccountingState): Promise<{
    count: number;
    revenueSum?: number;
    positiveRevenueDeviation?: number;
    negativeRevenueDeviation?: number;
  }> {
    const checkValue = this.getCheckValueForAccountingState(state);
    const ticketWithCurrentStateAndTours: Prisma.TourWhereInput = {
      check: checkValue,
      ...(checkValue === 2 ? { revenueDeviation: { not: 0 } } : {}),
      empty: {
        not: null,
      },
      ticketId: {
        not: null,
      },
      AND: [...onlyRealTours()],
    };

    const [
      count,
      revenueSum,
      positiveRevenueDeviation,
      negativeRevenueDeviation,
    ] = await this.prisma.$transaction([
      this.prisma.tour.count({
        where: { ...ticketWithCurrentStateAndTours },
      }),
      this.prisma.tour.aggregate({
        _sum: {
          revenue: true,
        },
        where: {
          ...ticketWithCurrentStateAndTours,
        },
      }),
      this.prisma.tour.aggregate({
        _sum: {
          revenueDeviation: true,
        },
        where: {
          ...ticketWithCurrentStateAndTours,
          revenueDeviation: {
            gt: 0,
          },
        },
      }),
      this.prisma.tour.aggregate({
        _sum: {
          revenueDeviation: true,
        },
        where: {
          ...ticketWithCurrentStateAndTours,
          revenueDeviation: {
            lt: 0,
          },
        },
      }),
    ]);

    return {
      count,
      revenueSum: revenueSum._sum.revenue ?? 0,
      positiveRevenueDeviation:
        positiveRevenueDeviation._sum.revenueDeviation ?? 0,
      negativeRevenueDeviation:
        negativeRevenueDeviation._sum.revenueDeviation ?? 0,
    };
  }

  private getCheckValueForAccountingState(state: AccountingState): number {
    switch (state) {
      case AccountingState.Billable:
        return 0;
      case AccountingState.PreparedForBilling:
        return 1;
      case AccountingState.Billed:
        return 2;
    }
  }
}
