import DataLoader from 'dataloader';
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 type {
  IDeleteDepreciationRecordByChargeIdParams,
  IDeleteDepreciationRecordByChargeIdQuery,
  IDeleteDepreciationRecordParams,
  IDeleteDepreciationRecordQuery,
  IGetDepreciationRecordsByChargeIdsQuery,
  IGetDepreciationRecordsByDatesParams,
  IGetDepreciationRecordsByDatesQuery,
  IGetDepreciationRecordsByIdsQuery,
  IInsertDepreciationRecordParams,
  IInsertDepreciationRecordQuery,
  IUpdateDepreciationRecordParams,
  IUpdateDepreciationRecordQuery,
} from '../types.js';

const getDepreciationRecordsByChargeIds = sql<IGetDepreciationRecordsByChargeIdsQuery>`
  SELECT d.*, (d.activation_date + (CONCAT((100 / dc.percentage * 365)::text, ' day'))::interval)::date as expiration_date
  FROM accounter_schema.depreciation d
  LEFT JOIN accounter_schema.depreciation_categories dc
            ON d.category = dc.id
  WHERE d.charge_id IN $$chargeIds;`;

const getDepreciationRecordsByIds = sql<IGetDepreciationRecordsByIdsQuery>`
  SELECT d.*, (d.activation_date + (CONCAT((100 / dc.percentage * 365)::text, ' day'))::interval)::date as expiration_date
  FROM accounter_schema.depreciation d
  LEFT JOIN accounter_schema.depreciation_categories dc
            ON d.category = dc.id
  WHERE d.id IN $$depreciationRecordIds;`;

const getDepreciationRecordsByDates = sql<IGetDepreciationRecordsByDatesQuery>`
  WITH depreciation AS (SELECT d.*,
                              (activation_date +
                              (CONCAT((100 / dc.percentage * 365)::text, ' day'))::interval)::date AS expiration_date
                      FROM accounter_schema.depreciation d
                                LEFT JOIN accounter_schema.depreciation_categories dc
                                          ON d.category = dc.id
                      WHERE activation_date <= $toDate)
  SELECT *
  FROM depreciation
  WHERE expiration_date >= $fromDate;`;

const updateDepreciationRecord = sql<IUpdateDepreciationRecordQuery>`
  UPDATE accounter_schema.depreciation
  SET
  charge_id = COALESCE(
    $chargeId,
    charge_id
  ),
  amount = COALESCE(
    $amount,
    amount
  ),
  currency = COALESCE(
    $currency,
    currency
  ),
  activation_date = COALESCE(
    $activationDate,
    activation_date
  ),
  type = COALESCE(
    $type,
    type
  ),
  category = COALESCE(
    $categoryId,
    category
  )
  WHERE
    id = $id
  RETURNING *;`;

const insertDepreciationRecord = sql<IInsertDepreciationRecordQuery>`
  INSERT INTO accounter_schema.depreciation (charge_id, amount, currency, activation_date, type, category, owner_id)
  VALUES ($chargeId, $amount, $currency, $activationDate, $type, $categoryId, $ownerId)
  RETURNING *`;

const deleteDepreciationRecord = sql<IDeleteDepreciationRecordQuery>`
  DELETE FROM accounter_schema.depreciation
  WHERE id = $depreciationRecordId
  RETURNING id;
`;

const deleteDepreciationRecordByChargeId = sql<IDeleteDepreciationRecordByChargeIdQuery>`
  DELETE FROM accounter_schema.depreciation
  WHERE charge_id = $chargeId
  RETURNING id;
`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class DepreciationProvider {
  constructor(
    private db: TenantAwareDBClient,
    private adminContextProvider: AdminContextProvider,
  ) {}

  private async batchDepreciationRecordsByIds(depreciationRecordIds: readonly string[]) {
    const records = await getDepreciationRecordsByIds.run(
      {
        depreciationRecordIds,
      },
      this.db,
    );
    return depreciationRecordIds.map(id => records.find(record => record.id === id));
  }

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

  private async batchDepreciationRecordsByChargeIds(chargeIds: readonly string[]) {
    const records = await getDepreciationRecordsByChargeIds.run(
      {
        chargeIds,
      },
      this.db,
    );
    records.map(record => {
      this.getDepreciationRecordByIdLoader.prime(record.id, record);
    });
    return chargeIds.map(id => records.filter(record => record.charge_id === id));
  }

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

  public getDepreciationRecordsByDates(params: IGetDepreciationRecordsByDatesParams) {
    return getDepreciationRecordsByDates.run(params, this.db).then(records => {
      records.map(record => {
        this.getDepreciationRecordByIdLoader.prime(record.id, record);
      });
      return records;
    });
  }

  public updateDepreciationRecord(params: IUpdateDepreciationRecordParams) {
    this.clearCache();
    return updateDepreciationRecord.run(params, this.db);
  }

  public async insertDepreciationRecord(params: IInsertDepreciationRecordParams) {
    this.clearCache();
    const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
    return insertDepreciationRecord.run(reassureOwnerIdExists(params, ownerId), this.db);
  }

  public deleteDepreciationRecord(params: IDeleteDepreciationRecordParams) {
    this.clearCache();
    return deleteDepreciationRecord.run(params, this.db);
  }

  public deleteDepreciationRecordByChargeId(params: IDeleteDepreciationRecordByChargeIdParams) {
    this.clearCache();
    return deleteDepreciationRecordByChargeId.run(params, this.db);
  }

  public clearCache() {
    this.getDepreciationRecordByIdLoader.clearAll();
    this.getDepreciationRecordsByChargeIdLoader.clearAll();
  }
}
