import { Injector } from 'graphql-modules';
import type { BusinessTripAttendeeStayInput } from '../../../__generated__/types.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { BusinessTripAttendeesProvider } from '../providers/business-trips-attendees.provider.js';
import { BusinessTripAccommodationsExpensesProvider } from '../providers/business-trips-expenses-accommodations.provider.js';
import type {
  IGetAllTaxVariablesResult,
  IGetBusinessTripsAccommodationsExpensesByBusinessTripIdsResult,
  IGetLastFlightByDateAndAttendeeIdResult,
} from '../types.js';
import {
  AttendeeInfo,
  BusinessTripError,
  getExpenseAmountsData,
  isIncreasedLimitDestination,
  SummaryCategoryData,
  SummaryData,
} from './business-trip-report.helper.js';

type AccommodationTaxVariables = {
  upToSevenNights: number;
  eightOrMoreNights: number;
};

export async function accommodationExpenseDataCollector(
  injector: Injector,
  businessTripExpenses: IGetBusinessTripsAccommodationsExpensesByBusinessTripIdsResult[],
  partialSummaryData: Partial<SummaryData>,
  destinationCode: string | null,
  taxVariables: IGetAllTaxVariablesResult,
  attendeesMap: Map<string, AttendeeInfo>,
): Promise<number> {
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  // populate category
  partialSummaryData['ACCOMMODATION'] ??= {};
  const category = partialSummaryData['ACCOMMODATION'] as SummaryCategoryData;

  category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };

  // set actual expense amounts and collect attendee accommodation data
  const attendeesAccommodationMap = new Map<
    string,
    { localAmount: number; foreignAmount: number; nights: number }
  >();

  // update total amounts and fill attendees accommodation map by expenses
  await Promise.all(
    businessTripExpenses.map(async businessTripExpense =>
      expenseAccommodationTaxableAmounts(
        injector,
        businessTripExpense,
        attendeesAccommodationMap,
      ).then(({ localAmount, foreignAmount }) => {
        category[defaultLocalCurrency]!.total += localAmount;
        category[defaultCryptoConversionFiatCurrency]!.total += foreignAmount;
      }),
    ),
  );

  // get tax variables
  const accommodationTaxVariables = getAccommodationTaxVariablesUSD(destinationCode, taxVariables);

  let unAccommodatedDays = 0;

  // update taxable amounts and max taxable amounts by attendees
  await Promise.all(
    Array.from(attendeesMap.entries()).map(async ([attendeeId, attendeeInfo]) => {
      const {
        taxableLocal,
        taxableForeign,
        maxTaxableLocal,
        maxTaxableForeign,
        unAccommodatedAttendeeDays,
      } = await attendeeAccommodationTaxableAmounts(
        injector,
        attendeeId,
        attendeeInfo,
        accommodationTaxVariables,
        attendeesAccommodationMap.get(attendeeId),
      );

      category[defaultLocalCurrency]!.maxTaxable += maxTaxableLocal;
      category[defaultCryptoConversionFiatCurrency]!.maxTaxable += maxTaxableForeign;
      category[defaultLocalCurrency]!.taxable += taxableLocal;
      category[defaultCryptoConversionFiatCurrency]!.taxable += taxableForeign;

      unAccommodatedDays += unAccommodatedAttendeeDays;
    }),
  );

  return unAccommodatedDays;
}

function getAccommodationTaxVariablesUSD(
  destinationCode: string | null,
  taxVariables: IGetAllTaxVariablesResult,
): AccommodationTaxVariables {
  const { max_accommodation_per_night_first_7_nights, max_accommodation_per_night_nights_8_to_90 } =
    taxVariables;
  const increasedLimitDestination = isIncreasedLimitDestination(destinationCode) ? 1.25 : 1;

  const upToSevenNights =
    Number(max_accommodation_per_night_first_7_nights) * increasedLimitDestination;
  const eightOrMoreNights =
    Number(max_accommodation_per_night_nights_8_to_90) * increasedLimitDestination;

  if (Number.isNaN(upToSevenNights) || Number.isNaN(eightOrMoreNights)) {
    throw new BusinessTripError('Tax variables are not set');
  }

  return {
    upToSevenNights,
    eightOrMoreNights,
  };
}

async function expenseAccommodationTaxableAmounts(
  injector: Injector,
  businessTripExpense: IGetBusinessTripsAccommodationsExpensesByBusinessTripIdsResult,
  attendeesAccommodationMap: Map<
    string,
    { localAmount: number; foreignAmount: number; nights: number }
  >,
) {
  const { localAmount, foreignAmount } = await getExpenseAmountsData(injector, businessTripExpense);

  if (!businessTripExpense.nights_count) {
    console.error(
      `Nights count not found for accommodation trip expense ID ${businessTripExpense.id}`,
    );
    throw new BusinessTripError('Accommodation expenses: some expenses are missing nights count');
  }

  if (!Number.isInteger(businessTripExpense.nights_count)) {
    console.error(`Nights count must be an integer`);
    throw new BusinessTripError('Accommodation expenses: nights count must be an integer');
  }

  const localAmountPerNight = localAmount / businessTripExpense.nights_count;
  const foreignAmountPerNight = foreignAmount / businessTripExpense.nights_count;

  const attendeesStay = businessTripExpense.attendees_stay.filter(
    Boolean,
  ) as BusinessTripAttendeeStayInput[];
  let cumulativeExpenseAccommodationNights = 0;

  attendeesStay.map(async ({ attendeeId, nightsCount = 0 }) => {
    cumulativeExpenseAccommodationNights += nightsCount;

    if (!attendeesAccommodationMap.has(attendeeId)) {
      attendeesAccommodationMap.set(attendeeId, {
        nights: nightsCount,
        localAmount: localAmountPerNight * nightsCount,
        foreignAmount: foreignAmountPerNight * nightsCount,
      });
      return;
    }

    const attendeeAccommodationData = attendeesAccommodationMap.get(attendeeId)!;
    attendeesAccommodationMap.set(attendeeId, {
      nights: attendeeAccommodationData.nights + nightsCount,
      localAmount: attendeeAccommodationData.localAmount + localAmountPerNight * nightsCount,
      foreignAmount: attendeeAccommodationData.foreignAmount + foreignAmountPerNight * nightsCount,
    });
  });

  if (cumulativeExpenseAccommodationNights !== businessTripExpense.nights_count) {
    console.error(
      `Attendees nights count (${cumulativeExpenseAccommodationNights}) doesn't match total nights count (${businessTripExpense.nights_count}) for expense ID ${businessTripExpense.id}`,
    );
    throw new BusinessTripError('Accommodation expenses: attendees nights count mismatch');
  }

  return { localAmount, foreignAmount };
}

async function attendeeAccommodationTaxableAmounts(
  injector: Injector,
  attendeeId: string,
  attendeeInfo: Pick<AttendeeInfo, 'nightsCount' | 'arrival'>,
  accommodationTaxVariables: AccommodationTaxVariables,
  attendeeAccommodationData?: { nights: number; localAmount: number; foreignAmount: number },
) {
  const { nightsCount: totalNights, arrival } = attendeeInfo;

  if (!totalNights) {
    // no nights, no accommodation
    return {
      taxableLocal: 0,
      taxableForeign: 0,
      maxTaxableLocal: 0,
      maxTaxableForeign: 0,
      unAccommodatedAttendeeDays: 0,
    };
  }

  if (totalNights && !attendeeAccommodationData) {
    // no accommodation data, no accommodation
    return {
      taxableLocal: 0,
      taxableForeign: 0,
      maxTaxableLocal: 0,
      maxTaxableForeign: 0,
      unAccommodatedAttendeeDays: totalNights + 1,
    };
  }

  if (arrival === null) {
    console.error(`Arrival date not found for attendee ID ${attendeeId}`);
    throw new BusinessTripError('Accommodation expenses: arrival date not found');
  }

  const prevAccommodatedNights = await sumAccommodatedNightsOnPreviousConsecutiveTrips(
    injector,
    attendeeId,
    arrival,
  );

  // validate attendee accommodation data
  const { nights: accommodationNights, localAmount, foreignAmount } = attendeeAccommodationData!;

  if (accommodationNights > totalNights) {
    console.error(
      `Accommodated nights (${accommodationNights}) exceed total nights (${totalNights})`,
    );
    throw new BusinessTripError(
      'Accommodation expenses: accommodated nights exceed total nights stay',
    );
  }

  // calculate taxable amount
  const maxTaxableUsd = calculateMaxTaxableAttendeeAmountWithPrevTrips(
    accommodationNights,
    prevAccommodatedNights,
    accommodationTaxVariables,
  );

  const maxTaxableLocal = 0; // TODO: calculate
  const maxTaxableForeign = maxTaxableUsd;
  const unAccommodatedAttendeeDays =
    totalNights > accommodationNights ? totalNights - accommodationNights : 0;

  const effectiveTaxableForeignAmount = calculateEffectiveTaxableAmountOverWeek(
    foreignAmount,
    accommodationNights,
    prevAccommodatedNights,
    accommodationTaxVariables,
  );
  const taxableAmount = Math.max(effectiveTaxableForeignAmount, maxTaxableUsd); // amounts are negative, so we take the max (abs min)
  const taxablePortion = taxableAmount / foreignAmount;

  const taxableLocal = localAmount * taxablePortion;
  const taxableForeign = taxableAmount;

  return {
    taxableLocal,
    taxableForeign,
    maxTaxableLocal,
    maxTaxableForeign,
    unAccommodatedAttendeeDays,
  };
}

function calculateEffectiveTaxableAmountOverWeek(
  amount: number,
  nights: number,
  prevNights: number,
  accommodationTaxVariables: AccommodationTaxVariables,
): number {
  if (prevNights + nights < 7) {
    return amount;
  }
  if (prevNights > 7) {
    return calculateEffectiveTaxableAmount(amount, nights, accommodationTaxVariables);
  }
  const leftAccommodatedNightsUnderWeek = 7 - prevNights;
  const dailyAmount = amount / nights;

  // calculate leftAccommodatedNightsUnderWeek with full tax rate
  const fullTaxableNightsAmount = leftAccommodatedNightsUnderWeek * dailyAmount;

  // calculate rest of the days with lower tax rate
  const overSevenNights = Math.max(0, nights - leftAccommodatedNightsUnderWeek);
  const partialTaxableNightsAmount = overSevenNights * dailyAmount;

  return (
    fullTaxableNightsAmount +
    calculateEffectiveTaxableAmount(
      partialTaxableNightsAmount,
      overSevenNights,
      accommodationTaxVariables,
    )
  );
}

function calculateMaxTaxableAttendeeAmount(
  totalAttendeeNights: number,
  { upToSevenNights, eightOrMoreNights }: AccommodationTaxVariables,
): number {
  if (totalAttendeeNights === 0) {
    return 0;
  }

  let maxTaxableUsd = 0;

  if (totalAttendeeNights <= 7) {
    // up to 7 days
    maxTaxableUsd += totalAttendeeNights * upToSevenNights;
  } else if (totalAttendeeNights > 90) {
    // over 90 days
    maxTaxableUsd += totalAttendeeNights * eightOrMoreNights;
  } else {
    // 8 to 90 days
    const remainingNights = totalAttendeeNights - 7;
    maxTaxableUsd += 7 * upToSevenNights + remainingNights * upToSevenNights * 0.75;
  }

  maxTaxableUsd *= -1;

  return maxTaxableUsd;
}

function calculateMaxTaxableAttendeeAmountWithPrevTrips(
  totalAttendeeNights: number,
  prevAccommodatedNights: number,
  accommodationTaxVariables: AccommodationTaxVariables,
): number {
  if (!prevAccommodatedNights) {
    // case no previous trips, regular calculation of current trip
    return calculateMaxTaxableAttendeeAmount(totalAttendeeNights, accommodationTaxVariables);
  }

  if (prevAccommodatedNights + totalAttendeeNights > 90) {
    // case over 90 days, regular calculation of all days
    return calculateMaxTaxableAttendeeAmount(
      prevAccommodatedNights + totalAttendeeNights,
      accommodationTaxVariables,
    );
  }

  // case 8 to 90 days, calculate current trip taxable amount
  const allConsecutiveTripsTaxableAmount = calculateMaxTaxableAttendeeAmount(
    prevAccommodatedNights + totalAttendeeNights,
    accommodationTaxVariables,
  );
  const prevTripsTaxableAmount = calculateMaxTaxableAttendeeAmount(
    prevAccommodatedNights,
    accommodationTaxVariables,
  );
  const currentTripTaxableAmount = allConsecutiveTripsTaxableAmount - prevTripsTaxableAmount;
  return currentTripTaxableAmount;
}

function calculateEffectiveTaxableAmount(
  amount: number,
  accommodatedNights: number,
  accommodationTaxVariables: AccommodationTaxVariables,
): number {
  const minTaxableAmount = accommodatedNights * accommodationTaxVariables.eightOrMoreNights * -1; // negative amount since we're dealing with expenses

  if (amount > minTaxableAmount) {
    // dealing with negative amounts so sign is flipped
    return amount;
  }
  if (amount * 0.75 < minTaxableAmount) {
    // dealing with negative amounts so sign is flipped
    return amount * 0.75;
  }
  return minTaxableAmount;
}

/**
 * Calculate the sum of accommodated nights on previous consecutive trips
 * @param injector
 * @param attendeeId
 * @param arrival
 * @returns
 */
async function sumAccommodatedNightsOnPreviousConsecutiveTrips(
  injector: Injector,
  attendeeId: string,
  arrival: Date,
): Promise<number> {
  let attendeePreviousTrip: IGetLastFlightByDateAndAttendeeIdResult | undefined;
  [attendeePreviousTrip] = await injector
    .get(BusinessTripAttendeesProvider)
    .getLastFlightByDateAndAttendeeId({
      attendeeBusinessId: attendeeId,
      date: arrival,
    });
  if (!attendeePreviousTrip) {
    return 0;
  }
  const previousTripIds: string[] = [];
  do {
    previousTripIds.push(attendeePreviousTrip.business_trip_id);
    if (attendeePreviousTrip?.arrival) {
      [attendeePreviousTrip] = await injector
        .get(BusinessTripAttendeesProvider)
        .getLastFlightByDateAndAttendeeId({
          attendeeBusinessId: attendeeId,
          date: attendeePreviousTrip.arrival,
        });
    } else {
      attendeePreviousTrip = undefined;
    }
  } while (attendeePreviousTrip);
  if (previousTripIds.length === 0) {
    return 0;
  }

  const accommodationExpenses = await injector
    .get(BusinessTripAccommodationsExpensesProvider)
    .getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.loadMany(previousTripIds)
    .then(
      res =>
        res
          .filter(r => !(r instanceof Error))
          .flat() as IGetBusinessTripsAccommodationsExpensesByBusinessTripIdsResult[],
    );

  let accommodatedNights = 0;
  accommodationExpenses.map(expense => {
    const attendeesStay = expense.attendees_stay.filter(Boolean) as BusinessTripAttendeeStayInput[];
    attendeesStay.map(({ attendeeId: expenseAttendeeId, nightsCount }) => {
      if (expenseAttendeeId === attendeeId) {
        accommodatedNights += nightsCount;
      }
    });
  });
  return accommodatedNights;
}
