import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { reassureOwnerIdExists } from '../../../shared/helpers/index.js';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { TransactionsProvider } from '../../transactions/providers/transactions.provider.js';
import type { IGetTransactionsByChargeIdsResult } from '../../transactions/types.js';
import type {
  IDeleteBusinessTripExpenseParams,
  IDeleteBusinessTripExpenseQuery,
  IGetBusinessTripsExpensesByBusinessTripIdsQuery,
  IGetBusinessTripsExpensesByIdsQuery,
  IInsertBusinessTripExpenseParams,
  IInsertBusinessTripExpenseQuery,
  IUpdateBusinessTripExpenseParams,
  IUpdateBusinessTripExpenseQuery,
} from '../types.js';
import { BusinessTripAccommodationsExpensesProvider } from './business-trips-expenses-accommodations.provider.js';
import { BusinessTripCarRentalExpensesProvider } from './business-trips-expenses-car-rental.provider.js';
import { BusinessTripFlightsExpensesProvider } from './business-trips-expenses-flights.provider.js';
import { BusinessTripOtherExpensesProvider } from './business-trips-expenses-other.provider.js';
import { BusinessTripTravelAndSubsistenceExpensesProvider } from './business-trips-expenses-travel-and-subsistence.provider.js';
import { BusinessTripsProvider } from './business-trips.provider.js';

const getBusinessTripsExpensesByBusinessTripIds = sql<IGetBusinessTripsExpensesByBusinessTripIdsQuery>`
  SELECT *
  FROM accounter_schema.extended_business_trip_transactions
  WHERE business_trip_id IN $$businessTripIds;`;

const getBusinessTripsExpensesByIds = sql<IGetBusinessTripsExpensesByIdsQuery>`
  SELECT *
  FROM accounter_schema.extended_business_trip_transactions
  WHERE id IN $$expenseIds;`;

const updateBusinessTripExpense = sql<IUpdateBusinessTripExpenseQuery>`
  UPDATE accounter_schema.business_trips_transactions
  SET
  business_trip_id = COALESCE(
    $businessTripId,
    business_trip_id
  ),
  category = COALESCE(
    $category,
    category
  )
  WHERE
    id = $businessTripExpenseId
  RETURNING *;
`;

const insertBusinessTripExpense = sql<IInsertBusinessTripExpenseQuery>`
  INSERT INTO accounter_schema.business_trips_transactions (business_trip_id, category, owner_id)
  VALUES($businessTripId, $category, $ownerId)
  RETURNING *;`;

const deleteBusinessTripExpense = sql<IDeleteBusinessTripExpenseQuery>`
  DELETE FROM accounter_schema.business_trips_transactions
  WHERE id = $businessTripExpenseId
  RETURNING id;
`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class BusinessTripExpensesProvider {
  constructor(
    private db: TenantAwareDBClient,
    private adminContextProvider: AdminContextProvider,
    private businessTripsProvider: BusinessTripsProvider,
    private flightExpensesProvider: BusinessTripFlightsExpensesProvider,
    private accommodationsExpensesProvider: BusinessTripAccommodationsExpensesProvider,
    private travelAndSubsistenceExpensesProvider: BusinessTripTravelAndSubsistenceExpensesProvider,
    private otherExpensesProvider: BusinessTripOtherExpensesProvider,
    private carRentalExpensesProvider: BusinessTripCarRentalExpensesProvider,
    private transactionsProvider: TransactionsProvider,
  ) {}

  private async batchBusinessTripsExpensesByBusinessTripIds(businessTripIds: readonly string[]) {
    const businessTrips = await getBusinessTripsExpensesByBusinessTripIds.run(
      {
        businessTripIds,
      },
      this.db,
    );
    return businessTripIds.map(id =>
      businessTrips.filter(record => record.business_trip_id === id),
    );
  }

  public getBusinessTripsExpensesByBusinessTripIdLoader = new DataLoader((ids: readonly string[]) =>
    this.batchBusinessTripsExpensesByBusinessTripIds(ids),
  );

  private async batchBusinessTripsExpenseMatchesByIds(expenseIds: readonly string[]) {
    const businessTripsExpenses = await getBusinessTripsExpensesByIds.run(
      {
        expenseIds,
      },
      this.db,
    );
    return expenseIds.map(id => businessTripsExpenses.find(record => record.id === id));
  }

  public getBusinessTripsExpensesByIdLoader = new DataLoader((ids: readonly string[]) =>
    this.batchBusinessTripsExpenseMatchesByIds(ids),
  );

  public updateBusinessTripExpense(params: IUpdateBusinessTripExpenseParams) {
    return updateBusinessTripExpense.run(params, this.db);
  }

  public async insertBusinessTripExpense(params: IInsertBusinessTripExpenseParams) {
    if (params.businessTripId) {
      this.invalidateByBusinessTripId(params.businessTripId);
    }
    const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
    return insertBusinessTripExpense.run(reassureOwnerIdExists(params, ownerId), this.db);
  }

  public async getBusinessTripExtendedExpensesByChargeId(chargeId: string) {
    const businessTrip =
      await this.businessTripsProvider.getBusinessTripsByChargeIdLoader.load(chargeId);
    if (!businessTrip) {
      throw new GraphQLError(`Business trip not found for charge id ${chargeId}`);
    }
    const [
      allExpenses,
      flightExpenses,
      accommodationsExpenses,
      travelAndSubsistenceExpenses,
      otherExpenses,
      carRentalExpenses,
    ] = await Promise.all([
      this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTrip.id),
      this.flightExpensesProvider.getBusinessTripsFlightsExpensesByBusinessTripIdLoader.load(
        businessTrip.id,
      ),
      this.accommodationsExpensesProvider.getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.load(
        businessTrip.id,
      ),
      this.travelAndSubsistenceExpensesProvider.getBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdLoader.load(
        businessTrip.id,
      ),
      this.otherExpensesProvider.getBusinessTripsOtherExpensesByBusinessTripIdLoader.load(
        businessTrip.id,
      ),
      this.carRentalExpensesProvider.getBusinessTripsCarRentalExpensesByBusinessTripIdLoader.load(
        businessTrip.id,
      ),
    ]);

    const extendedIds = new Set<string>(
      [
        ...flightExpenses,
        ...accommodationsExpenses,
        ...travelAndSubsistenceExpenses,
        ...otherExpenses,
        ...carRentalExpenses,
      ].map(t => t.id),
    );

    return {
      nonExtendedExpenses: allExpenses?.filter(t => !extendedIds.has(t.id!)),
      flightExpenses,
      accommodationsExpenses,
      travelAndSubsistenceExpenses,
      otherExpenses,
      carRentalExpenses,
    };
  }

  public async getBusinessTripExtendedExpensesByBusinessTripId(businessTripId: string) {
    const [
      allExpenses,
      flightExpenses,
      accommodationsExpenses,
      travelAndSubsistenceExpenses,
      otherExpenses,
      carRentalExpenses,
    ] = await Promise.all([
      this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTripId),
      this.flightExpensesProvider.getBusinessTripsFlightsExpensesByBusinessTripIdLoader.load(
        businessTripId,
      ),
      this.accommodationsExpensesProvider.getBusinessTripsAccommodationsExpensesByBusinessTripIdLoader.load(
        businessTripId,
      ),
      this.travelAndSubsistenceExpensesProvider.getBusinessTripsTravelAndSubsistenceExpensesByBusinessTripIdLoader.load(
        businessTripId,
      ),
      this.otherExpensesProvider.getBusinessTripsOtherExpensesByBusinessTripIdLoader.load(
        businessTripId,
      ),
      this.carRentalExpensesProvider.getBusinessTripsCarRentalExpensesByBusinessTripIdLoader.load(
        businessTripId,
      ),
    ]);

    const extendedIds = new Set<string>(
      [
        ...flightExpenses,
        ...accommodationsExpenses,
        ...travelAndSubsistenceExpenses,
        ...otherExpenses,
        ...carRentalExpenses,
      ].map(t => t.id),
    );

    return {
      nonExtendedExpenses: allExpenses?.filter(t => !extendedIds.has(t.id!)),
      flightExpenses,
      accommodationsExpenses,
      travelAndSubsistenceExpenses,
      otherExpenses,
      carRentalExpenses,
    };
  }

  public async getTransactionsByBusinessTripId(businessTripId: string) {
    const chargeIds =
      await this.businessTripsProvider.getChargeIdsByBusinessTripIdLoader.load(businessTripId);
    return this.transactionsProvider.transactionsByChargeIDLoader
      .loadMany(chargeIds)
      .then(
        res => res.filter(r => !(r instanceof Error)).flat() as IGetTransactionsByChargeIdsResult[],
      );
  }

  public deleteBusinessTripExpense(params: IDeleteBusinessTripExpenseParams) {
    if (params.businessTripExpenseId) {
      this.invalidateById(params.businessTripExpenseId);
    }
    return deleteBusinessTripExpense.run(params, this.db);
  }

  private invalidateExpenseExtensionsById(expenseId: string) {
    this.flightExpensesProvider.invalidateById(expenseId);
    this.accommodationsExpensesProvider.invalidateById(expenseId);
    this.travelAndSubsistenceExpensesProvider.invalidateById(expenseId);
    this.otherExpensesProvider.invalidateById(expenseId);
    this.carRentalExpensesProvider.invalidateById(expenseId);
  }

  private invalidateExpenseExtensionsByBusinessTripId(businessTripId: string) {
    this.flightExpensesProvider.invalidateByBusinessTripId(businessTripId);
    this.accommodationsExpensesProvider.invalidateByBusinessTripId(businessTripId);
    this.travelAndSubsistenceExpensesProvider.invalidateByBusinessTripId(businessTripId);
    this.otherExpensesProvider.invalidateByBusinessTripId(businessTripId);
    this.carRentalExpensesProvider.invalidateByBusinessTripId(businessTripId);
  }

  public async invalidateById(expenseId: string) {
    const expense = await this.getBusinessTripsExpensesByIdLoader.load(expenseId);
    if (expense?.business_trip_id) {
      this.getBusinessTripsExpensesByBusinessTripIdLoader.clear(expense.business_trip_id);
      this.invalidateExpenseExtensionsByBusinessTripId(expense.business_trip_id);
    }
    this.getBusinessTripsExpensesByIdLoader.clear(expenseId);
    this.invalidateExpenseExtensionsById(expenseId);
  }

  public async invalidateByBusinessTripId(businessTripId: string) {
    const expenses = await this.getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTripId);
    for (const expense of expenses ?? []) {
      if (expense.id) {
        this.getBusinessTripsExpensesByIdLoader.clear(expense.id);
        this.invalidateExpenseExtensionsById(expense.id);
      }
    }
    this.getBusinessTripsExpensesByBusinessTripIdLoader.clear(businessTripId);
    this.invalidateExpenseExtensionsByBusinessTripId(businessTripId);
  }

  public clearCache() {
    this.getBusinessTripsExpensesByBusinessTripIdLoader.clearAll();
    this.getBusinessTripsExpensesByIdLoader.clearAll();
  }
}
