import { Injector } from 'graphql-modules';
import type {
  BusinessTripSummaryCategories,
  BusinessTripSummaryRow,
} from '../../../__generated__/types.js';
import { CountryCode, Currency } from '../../../shared/enums.js';
import { formatCurrency, formatFinancialAmount } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { ExchangeProvider } from '../../exchange-rates/providers/exchange.provider.js';
import { getTransactionDebitDate } from '../../transactions/helpers/debit-date.helper.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import type { IGetTransactionsByIdsResult } from '../../transactions/types.js';
import type {
  flight_class,
  IGetAllTaxVariablesResult,
  IGetBusinessTripsCarRentalExpensesByBusinessTripIdsResult,
  IGetBusinessTripsExpensesByBusinessTripIdsResult,
  IGetBusinessTripsFlightsExpensesByBusinessTripIdsResult,
  IGetBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdsResult,
} from '../types.js';

export class BusinessTripError extends Error {
  constructor(message: string) {
    super(message);
  }
}

export type SummaryCategoryData = Partial<
  Record<Currency, { total: number; taxable: number; maxTaxable: number }>
>;
export type SummaryData = Record<BusinessTripSummaryCategories, SummaryCategoryData>;
export type AttendeeInfo = {
  name: string;
  arrival: Date | null;
  departure: Date | null;
  daysCount: number;
  nightsCount: number;
};

export function convertSummaryCategoryDataToRow(
  category: BusinessTripSummaryCategories,
  data: SummaryCategoryData,
): BusinessTripSummaryRow {
  const excessExpenditure = (data[Currency.Ils]?.taxable ?? 0) - (data[Currency.Ils]?.total ?? 0);
  return {
    type: category,
    totalForeignCurrency: formatFinancialAmount(data[Currency.Usd]?.total ?? 0, Currency.Usd),
    taxableForeignCurrency: formatFinancialAmount(data[Currency.Usd]?.taxable ?? 0, Currency.Usd),
    maxTaxableForeignCurrency: formatFinancialAmount(
      data[Currency.Usd]?.maxTaxable ?? 0,
      Currency.Usd,
    ),
    totalLocalCurrency: formatFinancialAmount(data[Currency.Ils]?.total ?? 0, Currency.Ils),
    taxableLocalCurrency: formatFinancialAmount(data[Currency.Ils]?.taxable ?? 0, Currency.Ils),
    maxTaxableLocalCurrency: formatFinancialAmount(
      data[Currency.Ils]?.maxTaxable ?? 0,
      Currency.Ils,
    ),
    excessExpenditure: formatFinancialAmount(Math.max(excessExpenditure, 0), Currency.Ils),
  };
}

export function calculateTotalReportSummaryCategory(data: Partial<SummaryData>) {
  const totalSumCategory = Object.values(data).reduce((acc, category) => {
    Object.entries(category).map(([currency, { total, taxable }]) => {
      acc[currency as 'ILS' | 'USD'] ||= { total: 0, taxable: 0, maxTaxable: 0 };
      acc[currency as 'ILS' | 'USD']!.total += total;
      acc[currency as 'ILS' | 'USD']!.taxable += taxable;
    });
    return acc;
  }, {});
  return totalSumCategory;
}

export function getExpenseCoreData(
  tripExpense: Pick<
    IGetBusinessTripsExpensesByBusinessTripIdsResult,
    'payed_by_employee' | 'amount' | 'date' | 'currency' | 'value_date' | 'id'
  >,
): {
  amount: number;
  currency: Currency;
  date: Date;
} {
  if (tripExpense.payed_by_employee) {
    if (!tripExpense.currency || !tripExpense.amount || !tripExpense.date) {
      throw new BusinessTripError(
        `Currency, amount or date not found for employee-paid trip expense ID ${tripExpense.id}`,
      );
    }
    return {
      amount: Number(tripExpense.amount),
      currency: formatCurrency(tripExpense.currency),
      date: new Date(tripExpense.date),
    };
  }
  if (!tripExpense.currency || !tripExpense.amount || !tripExpense.value_date) {
    throw new BusinessTripError(
      `Currency, amount or date not found for business trip expense ID ${tripExpense.id}`,
    );
  }
  return {
    amount: Number(tripExpense.amount),
    currency: formatCurrency(tripExpense.currency),
    date: new Date(tripExpense.value_date),
  };
}

async function getDefaultCurrenciesAmountsAndExchangeRate(
  injector: Injector,
  currency: Currency,
  amount: number,
  date: Date,
) {
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  const exchangeRatePromise =
    currency === defaultLocalCurrency
      ? Promise.resolve(1)
      : injector.get(ExchangeProvider).getExchangeRates(currency, defaultLocalCurrency, date);
  const usdRatePromise =
    currency === defaultCryptoConversionFiatCurrency
      ? Promise.resolve(1)
      : injector
          .get(ExchangeProvider)
          .getExchangeRates(currency, defaultCryptoConversionFiatCurrency, date);
  const [localRate, foreignRate] = await Promise.all([exchangeRatePromise, usdRatePromise]);
  const localAmount = localRate * amount;
  const foreignAmount = foreignRate * amount;
  return { localAmount, foreignAmount };
}

export async function getExpenseAmountsData(
  injector: Injector,
  businessTripExpense: IGetBusinessTripsExpensesByBusinessTripIdsResult,
) {
  try {
    const { amount, currency, date } = getExpenseCoreData(businessTripExpense);

    const { localAmount, foreignAmount } = await getDefaultCurrenciesAmountsAndExchangeRate(
      injector,
      currency,
      amount,
      date,
    );

    return { localAmount, foreignAmount };
  } catch (error) {
    // handle merged transaction of different currencies
    if (
      !(error instanceof Error) ||
      !error?.message?.includes('Currency, amount or date not found for business trip expense') ||
      !businessTripExpense.transaction_ids ||
      businessTripExpense.transaction_ids.length <= 1
    ) {
      throw error;
    }

    const transactions = await injector
      .get(TransactionsProvider)
      .transactionByIdLoader.loadMany(businessTripExpense.transaction_ids);

    const allTransactions = transactions.filter(
      transaction => transaction && !(transaction instanceof Error),
    ) as IGetTransactionsByIdsResult[];

    let localAmount = 0;
    let foreignAmount = 0;

    await Promise.all(
      allTransactions.map(async transaction => {
        if (!transaction || transaction instanceof Error) {
          return;
        }
        const amount = Number(transaction.amount);
        const currency = transaction.currency as Currency;
        const date = getTransactionDebitDate(transaction);

        if (!amount || !currency || !date) {
          const errorMessage = `Currency, amount or date not found for transaction ID ${transaction.id}`;
          console.error(errorMessage);
          throw new BusinessTripError(errorMessage);
        }

        const { localAmount: transactionLocalAmount, foreignAmount: transactionForeignAmount } =
          await getDefaultCurrenciesAmountsAndExchangeRate(injector, currency, amount, date);

        localAmount += transactionLocalAmount;
        foreignAmount += transactionForeignAmount;
      }),
    );

    return { localAmount, foreignAmount };
  }
}

export async function flightExpenseDataCollector(
  injector: Injector,
  businessTripExpense: IGetBusinessTripsFlightsExpensesByBusinessTripIdsResult,
  partialSummaryData: Partial<SummaryData>,
): Promise<void> {
  // populate category
  partialSummaryData['FLIGHT'] ??= {};
  const category = partialSummaryData['FLIGHT'] as SummaryCategoryData;

  const { localAmount, foreignAmount } = await getExpenseAmountsData(injector, businessTripExpense);

  // calculate taxable amount
  const fullyTaxableClasses: flight_class[] = ['ECONOMY', 'PREMIUM_ECONOMY', 'BUSINESS'];
  if (!businessTripExpense.class) {
    console.error(`Flight class not found for flight expense ID ${businessTripExpense.id}`);
    throw new BusinessTripError('Flights expenses: some flights are missing class');
  }
  if (!fullyTaxableClasses.includes(businessTripExpense.class)) {
    console.error(
      `Taxability logic for flight class ${businessTripExpense.class} is not implemented yet (trip expense ID: ${businessTripExpense.id})`,
    );
    throw new BusinessTripError(
      `Flights expenses: taxability logic for class ${businessTripExpense.class} is not implemented yet`,
    );
  }

  // for all classes <= business, the amount is fully taxable
  const localTaxable = localAmount;
  const foreignTaxable = foreignAmount;

  // update amounts
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultLocalCurrency].total += localAmount;
  category[defaultLocalCurrency].taxable += localTaxable;
  category[defaultLocalCurrency].maxTaxable += localTaxable;
  category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultCryptoConversionFiatCurrency].total += foreignAmount;
  category[defaultCryptoConversionFiatCurrency].taxable += foreignTaxable;
  category[defaultCryptoConversionFiatCurrency].maxTaxable += foreignTaxable;

  return void 0;
}

export async function employeeAccommodationDataByTrip() {}

export async function otherExpensesDataCollector(
  injector: Injector,
  otherExpenses: IGetBusinessTripsExpensesByBusinessTripIdsResult[],
  partialSummaryData: Partial<SummaryData>,
): Promise<string | void> {
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  // populate category
  partialSummaryData['OTHER'] ??= {};
  const category = partialSummaryData['OTHER'] as SummaryCategoryData;
  category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };

  if (otherExpenses.length === 0) {
    return void 0;
  }

  await Promise.all(
    otherExpenses.map(async businessTripExpense => {
      const { localAmount, foreignAmount } = await getExpenseAmountsData(
        injector,
        businessTripExpense,
      );

      category[defaultLocalCurrency]!.total += localAmount;
      category[defaultLocalCurrency]!.taxable += localAmount;
      category[defaultLocalCurrency]!.maxTaxable += localAmount;
      category[defaultCryptoConversionFiatCurrency]!.total += foreignAmount;
      category[defaultCryptoConversionFiatCurrency]!.taxable += foreignAmount;
      category[defaultCryptoConversionFiatCurrency]!.maxTaxable += foreignAmount;
    }),
  );

  return void 0;
}

type ReportMetaData = {
  destinationCode: string | null;
  unAccommodatedDays: number;
  attendees: Map<string, AttendeeInfo>;
};

export async function travelAndSubsistenceExpensesDataCollector(
  injector: Injector,
  businessTripExpenses: IGetBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdsResult[],
  partialSummaryData: Partial<SummaryData>,
  taxVariables: IGetAllTaxVariablesResult,
  { destinationCode, unAccommodatedDays, attendees }: ReportMetaData,
): Promise<void> {
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  // populate category
  partialSummaryData['TRAVEL_AND_SUBSISTENCE'] ??= {};
  const category = partialSummaryData['TRAVEL_AND_SUBSISTENCE'] as SummaryCategoryData;
  category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };

  const totalBusinessDays = Array.from(attendees.values()).reduce(
    (acc, attendee) => acc + attendee.daysCount,
    0,
  );

  const maxTaxableUsd = getAttendeeTravelAndSubsistenceMaxTax(
    totalBusinessDays,
    unAccommodatedDays,
    destinationCode,
    taxVariables,
  );

  category[defaultLocalCurrency].maxTaxable += 0; // TODO: calculate
  category[defaultCryptoConversionFiatCurrency].maxTaxable += maxTaxableUsd;

  // set actual expense amounts
  await Promise.all(
    businessTripExpenses.map(async businessTripExpense => {
      const { localAmount, foreignAmount } = await getExpenseAmountsData(
        injector,
        businessTripExpense,
      );

      category[defaultLocalCurrency]!.total += localAmount;
      category[defaultCryptoConversionFiatCurrency]!.total += foreignAmount;
    }),
  );

  // set taxable amounts
  const taxableAmount = Math.max(
    category[defaultCryptoConversionFiatCurrency].total,
    maxTaxableUsd,
  );
  const taxablePortion =
    category[defaultCryptoConversionFiatCurrency].total === 0
      ? 0
      : taxableAmount / category[defaultCryptoConversionFiatCurrency].total;

  category[defaultLocalCurrency].taxable += category[defaultLocalCurrency].total * taxablePortion;
  category[defaultCryptoConversionFiatCurrency].taxable += taxableAmount;

  return void 0;
}

export async function carRentalExpensesDataCollector(
  injector: Injector,
  businessTripExpenses: IGetBusinessTripsCarRentalExpensesByBusinessTripIdsResult[],
  partialSummaryData: Partial<SummaryData>,
  taxVariables: IGetAllTaxVariablesResult,
  destinationCode: string | null,
): Promise<void> {
  const { defaultLocalCurrency, defaultCryptoConversionFiatCurrency } = await injector
    .get(AdminContextProvider)
    .getVerifiedAdminContext();
  // populate category
  partialSummaryData['CAR_RENTAL'] ??= {};
  const category = partialSummaryData['CAR_RENTAL'] as SummaryCategoryData;
  category[defaultLocalCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };
  category[defaultCryptoConversionFiatCurrency] ||= { total: 0, taxable: 0, maxTaxable: 0 };

  let rentalDays = 0;

  // set actual expense amounts
  await Promise.all(
    businessTripExpenses.map(async businessTripExpense => {
      if (!businessTripExpense.is_fuel_expense) {
        rentalDays += businessTripExpense.days;
      }

      const { localAmount, foreignAmount } = await getExpenseAmountsData(
        injector,
        businessTripExpense,
      );

      category[defaultLocalCurrency]!.total += localAmount;
      category[defaultCryptoConversionFiatCurrency]!.total += foreignAmount;
    }),
  );

  // set max taxable amounts
  const { max_car_rental_per_day } = taxVariables;
  const maxDailyRentalAmount = Number(max_car_rental_per_day);

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

  const increasedLimitDestination = isIncreasedLimitDestination(destinationCode) ? 1.25 : 1;

  const maxTaxableUsd = maxDailyRentalAmount * rentalDays * increasedLimitDestination * -1;

  category[defaultLocalCurrency].maxTaxable += 0; // TODO: calculate
  category[defaultCryptoConversionFiatCurrency].maxTaxable += maxTaxableUsd;

  // set taxable amounts
  const taxableAmount = Math.max(
    category[defaultCryptoConversionFiatCurrency].total,
    maxTaxableUsd,
  );
  const taxablePortion =
    category[defaultCryptoConversionFiatCurrency].total === 0
      ? 0
      : taxableAmount / category[defaultCryptoConversionFiatCurrency].total;

  category[defaultLocalCurrency].taxable += category[defaultLocalCurrency].total * taxablePortion;
  category[defaultCryptoConversionFiatCurrency].taxable += taxableAmount;

  return void 0;
}

export function isIncreasedLimitDestination(destinationCode: string | null) {
  if (!destinationCode) {
    return false;
  }
  const increasedLimitDestinations: CountryCode[] = [
    CountryCode.Angola,
    CountryCode.Australia,
    CountryCode.Austria,
    CountryCode.Belgium,
    CountryCode.Cameroon,
    CountryCode.Canada,
    CountryCode.Denmark,
    CountryCode['United Arab Emirates (the)'],
    CountryCode.Finland,
    CountryCode.France,
    CountryCode.Germany,
    CountryCode.Greece,
    CountryCode['Hong Kong'],
    CountryCode.Iceland,
    CountryCode.Ireland,
    CountryCode.Italy,
    CountryCode.Japan,
    CountryCode['Korea (the Republic of)'],
    CountryCode["Korea (the Democratic People's Republic of)"],
    CountryCode.Luxembourg,
    CountryCode['Netherlands (the)'],
    CountryCode.Norway,
    CountryCode.Oman,
    CountryCode.Qatar,
    CountryCode.Spain,
    CountryCode.Sweden,
    CountryCode.Switzerland,
    CountryCode['Taiwan (Province of China)'],
    CountryCode['United Kingdom of Great Britain and Northern Ireland (the)'],
  ];
  return increasedLimitDestinations.includes(destinationCode.toLocaleUpperCase() as CountryCode);
}

export function onlyUnique(value: string, index: number, array: string[]) {
  return array.indexOf(value) === index;
}

export function getAttendeeTravelAndSubsistenceMaxTax(
  totalBusinessDays: number,
  unAccommodatedDays: number,
  destinationCode: string | null,
  taxVariables: IGetAllTaxVariablesResult,
): number {
  const { max_tns_with_accommodation, max_tns_without_accommodation } = taxVariables;
  const maxExpenseWithAccommodation = Number(max_tns_with_accommodation);
  const maxExpenseWithoutAccommodation = Number(max_tns_without_accommodation);

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

  const accommodatedDays = totalBusinessDays - unAccommodatedDays;

  const increasedLimitFactor = isIncreasedLimitDestination(destinationCode) ? 1.25 : 1;

  const maxTaxableUsd =
    (maxExpenseWithAccommodation * accommodatedDays +
      maxExpenseWithoutAccommodation * unAccommodatedDays) *
    increasedLimitFactor *
    -1;

  return maxTaxableUsd;
}

export * from './business-trip-report-accommodation.helper.js';
