import { differenceInDays } from 'date-fns';
import { GraphQLError } from 'graphql';
import { Injector } from 'graphql-modules';
import type {
  BusinessTripSummary,
  BusinessTripSummaryCategories,
} from '../../../__generated__/types.js';
import {
  accommodationExpenseDataCollector,
  AttendeeInfo,
  BusinessTripError,
  calculateTotalReportSummaryCategory,
  carRentalExpensesDataCollector,
  convertSummaryCategoryDataToRow,
  flightExpenseDataCollector,
  onlyUnique,
  otherExpensesDataCollector,
  SummaryData,
  travelAndSubsistenceExpensesDataCollector,
} from '../helpers/business-trip-report.helper.js';
import { BusinessTripAttendeesProvider } from '../providers/business-trips-attendees.provider.js';
import { BusinessTripExpensesProvider } from '../providers/business-trips-expenses.provider.js';
import { BusinessTripTaxVariablesProvider } from '../providers/business-trips-tax-variables.provider.js';
import type { BusinessTripProto } from '../types.js';

export async function businessTripSummary(
  injector: Injector,
  dbBusinessTrip: BusinessTripProto,
): Promise<BusinessTripSummary> {
  try {
    const attendees = await injector
      .get(BusinessTripAttendeesProvider)
      .getBusinessTripsAttendeesByBusinessTripIdLoader.load(dbBusinessTrip.id);

    if (!attendees?.length) {
      return {
        rows: [],
        errors: ['Attendees are not set'],
      };
    }

    let fromDate: Date | undefined;
    let toDate: Date | undefined;

    attendees.map(attendee => {
      const { arrival, departure } = attendee;

      if (arrival && (!fromDate || arrival < fromDate)) {
        fromDate = arrival;
      }

      if (departure && (!toDate || departure > toDate)) {
        toDate = departure;
      }
    });

    if (!fromDate || !toDate) {
      return {
        rows: [],
        errors: ['Business trip dates are not set'],
      };
    }

    const expensesPromise = await injector
      .get(BusinessTripExpensesProvider)
      .getBusinessTripExtendedExpensesByBusinessTripId(dbBusinessTrip.id);
    const taxVariablesPromise = injector
      .get(BusinessTripTaxVariablesProvider)
      .getTaxVariablesByDateLoader.load(toDate);

    const [
      {
        flightExpenses,
        accommodationsExpenses,
        travelAndSubsistenceExpenses,
        otherExpenses,
        carRentalExpenses,
      },
      taxVariables,
    ] = await Promise.all([expensesPromise, taxVariablesPromise]);

    if (!taxVariables) {
      return {
        rows: [],
        errors: ['Tax variables are not set'],
      };
    }

    const summaryData: Partial<SummaryData> = {};
    const attendeesMap = new Map<string, AttendeeInfo>();
    attendees.map(attendee => {
      const { arrival, departure } = attendee;

      let daysCount = 0;
      let nightsCount = 0;
      if (arrival && departure) {
        daysCount = differenceInDays(departure, arrival) + 1;
        nightsCount = daysCount - 1;
      }
      attendeesMap.set(attendee.id, {
        name: attendee.name,
        arrival,
        departure,
        daysCount,
        nightsCount,
      });
    });

    const errors: string[] = [];

    const [unAccommodatedDays] = await Promise.all([
      accommodationExpenseDataCollector(
        injector,
        accommodationsExpenses,
        summaryData,
        dbBusinessTrip.destination,
        taxVariables,
        attendeesMap,
      ).catch(e => {
        if (e instanceof BusinessTripError) {
          errors.push(e.message);
        } else {
          console.error(`Error handling accommodation expenses`, e);
          throw new GraphQLError((e as Error)?.message ?? `Error handling accommodation expenses`);
        }
      }),
      ...flightExpenses.map(flightExpense =>
        flightExpenseDataCollector(injector, flightExpense, summaryData).catch(e => {
          if (e instanceof BusinessTripError) {
            errors.push(e.message);
          } else {
            console.error(`Error handling flight expenses`, e);
            throw new GraphQLError((e as Error)?.message ?? `Error handling flight expenses`);
          }
        }),
      ),
      otherExpensesDataCollector(injector, otherExpenses, summaryData).catch(e => {
        if (e instanceof BusinessTripError) {
          errors.push(e.message);
        } else {
          console.error(`Error handling other expenses`, e);
          throw new GraphQLError((e as Error)?.message ?? `Error handling other expenses`);
        }
      }),
      carRentalExpensesDataCollector(
        injector,
        carRentalExpenses,
        summaryData,
        taxVariables,
        dbBusinessTrip.destination,
      ).catch(e => {
        if (e instanceof BusinessTripError) {
          errors.push(e.message);
        } else {
          console.error(`Error handling car rental expenses`, e);
          throw new GraphQLError((e as Error)?.message ?? `Error handling car rental expenses`);
        }
      }),
    ]);

    await travelAndSubsistenceExpensesDataCollector(
      injector,
      travelAndSubsistenceExpenses,
      summaryData,
      taxVariables,
      {
        destinationCode: dbBusinessTrip.destination,
        attendees: attendeesMap,
        unAccommodatedDays: unAccommodatedDays ?? 0,
      },
    ).catch(e => {
      if (e instanceof BusinessTripError) {
        errors.push(e.message);
      } else {
        console.error(`Error handling travel and subsistence expenses`, e);
        throw new GraphQLError(
          (e as Error)?.message ?? `Error handling travel and subsistence expenses`,
        );
      }
    });

    const totalSumCategory = calculateTotalReportSummaryCategory(summaryData);
    summaryData['TOTAL'] = totalSumCategory;

    return {
      rows: Object.entries(summaryData).map(([category, data]) =>
        convertSummaryCategoryDataToRow(category as BusinessTripSummaryCategories, data),
      ),
      errors: errors.length ? errors.filter(onlyUnique) : undefined,
    };
  } catch (e) {
    console.error(`Error fetching business trip expenses`, e);
    throw new GraphQLError((e as Error)?.message ?? `Error fetching business trip expenses`);
  }
}
