import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { TimelessDateString } from '../../../shared/types/index.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { AuthContextProvider } from '../../auth/providers/auth-context.provider.js';
import type {
  IDeleteCorporateTaxParams,
  IDeleteCorporateTaxQuery,
  IGetCorporateTaxesByCorporateIdsQuery,
  IGetCorporateTaxesByCorporateIdsResult,
  IInsertCorporateTaxParams,
  IInsertCorporateTaxQuery,
  IUpdateCorporateTaxParams,
  IUpdateCorporateTaxQuery,
} from '../types.js';

const getCorporateTaxesByCorporateIds = sql<IGetCorporateTaxesByCorporateIdsQuery>`
  SELECT *
  FROM accounter_schema.corporate_tax_variables
  WHERE corporate_id IN $$corporateIds
  ORDER BY date DESC;`;

const updateCorporateTax = sql<IUpdateCorporateTaxQuery>`
  UPDATE accounter_schema.corporate_tax_variables
  SET
  date = COALESCE(
    $date,
    date
  ),
  tax_rate = COALESCE(
    $taxRate,
    tax_rate
  ),
  original_tax_rate = COALESCE(
    $originalTaxRate,
    original_tax_rate
  )
  WHERE
    corporate_id = $corporateId
    AND date = $originalDate
  RETURNING *;`;

const insertCorporateTax = sql<IInsertCorporateTaxQuery>`
  INSERT INTO accounter_schema.corporate_tax_variables (corporate_id, date, tax_rate, original_tax_rate, owner_id)
  VALUES ($corporateId, $date, $taxRate, $originalTaxRate, $ownerId)
  RETURNING *`;

const deleteCorporateTax = sql<IDeleteCorporateTaxQuery>`
  DELETE FROM accounter_schema.corporate_tax_variables
  WHERE corporate_id = $corporateId
  AND date = $date
  RETURNING *`;

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class CorporateTaxesProvider {
  private businessIdCache: string | null = null;
  constructor(
    private db: TenantAwareDBClient,
    private authContextProvider: AuthContextProvider,
  ) {}

  private async getBusinessId(): Promise<string> {
    if (this.businessIdCache !== null) {
      return this.businessIdCache;
    }
    const authContext = await this.authContextProvider.getAuthContext();
    this.businessIdCache = authContext?.tenant.businessId ?? null;
    if (!this.businessIdCache) {
      throw new Error('Missing businessId in AuthContext');
    }
    return this.businessIdCache;
  }

  private allCorporateTaxesCache = new Map<
    string,
    Promise<IGetCorporateTaxesByCorporateIdsResult[]>
  >();
  public getAllCorporateTaxes(corporateId: string) {
    if (this.allCorporateTaxesCache.has(corporateId)) {
      return this.allCorporateTaxesCache.get(corporateId)!;
    }
    this.allCorporateTaxesCache.set(
      corporateId,
      getCorporateTaxesByCorporateIds.run({ corporateIds: [corporateId] }, this.db),
    );
    return this.allCorporateTaxesCache.get(corporateId)!;
  }

  private async batchCorporateTaxesByDates(
    taxRates: readonly { date: TimelessDateString; corporateId: string }[],
  ) {
    const corporateIds = [...new Set(taxRates.map(t => t.corporateId))];
    const rates = (await Promise.all(corporateIds.map(id => this.getAllCorporateTaxes(id)))).flat();

    return taxRates.map(({ date, corporateId }) => {
      const time = new Date(date).getTime();
      return rates
        .filter(rate => rate.corporate_id === corporateId)
        .sort((a, b) => b.date.getTime() - a.date.getTime())
        .find(rate => rate.date.getTime() <= time);
    });
  }

  public getCorporateTaxesByDateLoader = new DataLoader(
    async (dates: readonly TimelessDateString[]) => {
      const businessId = await this.getBusinessId();
      return this.batchCorporateTaxesByDates(
        dates.map(date => ({ date, corporateId: businessId })),
      );
    },
  );

  private async batchCorporateTaxesByCorporateIds(corporateIds: readonly string[]) {
    const taxes = await getCorporateTaxesByCorporateIds.run({ corporateIds }, this.db);
    return corporateIds.map(id => taxes.filter(expense => expense.corporate_id === id));
  }

  public getCorporateTaxesByCorporateIdLoader = new DataLoader((corporateIds: readonly string[]) =>
    this.batchCorporateTaxesByCorporateIds(corporateIds),
  );

  public updateCorporateTax(params: IUpdateCorporateTaxParams) {
    this.allCorporateTaxesCache = new Map();
    return updateCorporateTax.run(params, this.db);
  }

  public async insertCorporateTax(params: IInsertCorporateTaxParams) {
    this.allCorporateTaxesCache = new Map();
    const ownerId = params.ownerId ?? (await this.getBusinessId());
    return insertCorporateTax.run(
      {
        ...params,
        ownerId,
      },
      this.db,
    );
  }

  public deleteCorporateTax(params: IDeleteCorporateTaxParams) {
    this.allCorporateTaxesCache = new Map();
    return deleteCorporateTax.run(params, this.db);
  }
}
