import DataLoader from 'dataloader';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { getCacheInstance } from '../../../shared/helpers/index.js';
import type { Optional, TimelessDateString } from '../../../shared/types/index.js';
import { DBProvider } from '../../app-providers/db.provider.js';
import type {
  accountant_status,
  IBatchUpdateChargesParams,
  IBatchUpdateChargesQuery,
  IDeleteChargesByIdsParams,
  IDeleteChargesByIdsQuery,
  IGenerateChargeParams,
  IGenerateChargeQuery,
  IGetChargesByFiltersParams,
  IGetChargesByFiltersQuery,
  IGetChargesByFiltersResult,
  IGetChargesByIdsQuery,
  IGetChargesByMissingRequiredInfoQuery,
  IGetChargesByTransactionIdsQuery,
  IGetSimilarChargesParams,
  IGetSimilarChargesQuery,
  IUpdateAccountantApprovalParams,
  IUpdateAccountantApprovalQuery,
  IUpdateChargeParams,
  IUpdateChargeQuery,
} from '../types.js';

export type ChargeRequiredWrapper<
  T extends {
    id: unknown;
    owner_id: unknown;
    is_property: unknown;
    accountant_status: unknown;
    updated_at: unknown;
    created_at: unknown;
    documents_optional_flag: unknown;
    optional_vat: unknown;
  },
> = Omit<
  T,
  | 'id'
  | 'owner_id'
  | 'is_property'
  | 'accountant_status'
  | 'updated_at'
  | 'created_at'
  | 'documents_optional_flag'
  | 'optional_vat'
> & {
  id: NonNullable<T['id']>;
  owner_id: NonNullable<T['owner_id']>;
  is_property: NonNullable<T['is_property']>;
  accountant_status: NonNullable<T['accountant_status']>;
  updated_at: NonNullable<T['updated_at']>;
  created_at: NonNullable<T['created_at']>;
  documents_optional_flag: NonNullable<T['documents_optional_flag']>;
  optional_vat: NonNullable<T['optional_vat']>;
};

const getChargesByIds = sql<IGetChargesByIdsQuery>`
    SELECT *
    FROM accounter_schema.charges
    WHERE id IN $$chargeIds;`;

const getChargesByTransactionIds = sql<IGetChargesByTransactionIdsQuery>`
    SELECT t.id AS transaction_id, c.* FROM accounter_schema.transactions t
    LEFT JOIN accounter_schema.charges c
      ON t.charge_id = c.id
    WHERE t.id IN $$transactionIds;`;

const getChargesByMissingRequiredInfo = sql<IGetChargesByMissingRequiredInfoQuery>`
    SELECT c.*
    FROM accounter_schema.charges c
    LEFT JOIN accounter_schema.charge_tags t
      ON t.charge_id = c.id
    WHERE c.user_description IS NULL
    OR t.tag_id IS NULL;`;

const updateCharge = sql<IUpdateChargeQuery>`
  UPDATE accounter_schema.charges
  SET
  owner_id = COALESCE(
    $ownerId,
    owner_id
  ),
  user_description = COALESCE(
    $userDescription,
    user_description
  ),
  type = COALESCE(
    $type,
    type
  ),
  invoice_payment_currency_diff = COALESCE(
    $isInvoicePaymentDifferentCurrency,
    invoice_payment_currency_diff
  ),
  accountant_status = COALESCE(
    $accountantStatus,
    accountant_status
  ),
  tax_category_id = COALESCE(
    $taxCategoryId,
    tax_category_id
  ),
  optional_vat = COALESCE(
    $optionalVAT,
    optional_vat
  ),
  documents_optional_flag = COALESCE(
    $optionalDocuments,
    documents_optional_flag
  ),
  is_property = COALESCE(
    $isProperty,
    is_property
  )
  WHERE
    id = $chargeId
  RETURNING *;
`;

const batchUpdateCharges = sql<IBatchUpdateChargesQuery>`
  UPDATE accounter_schema.charges
  SET
  owner_id = COALESCE(
    $ownerId,
    owner_id
  ),
  user_description = COALESCE(
    $userDescription,
    user_description
  ),
  type = COALESCE(
    $type,
    type
  ),
  invoice_payment_currency_diff = COALESCE(
    $isInvoicePaymentDifferentCurrency,
    invoice_payment_currency_diff
  ),
  accountant_status = COALESCE(
    $accountantStatus,
    accountant_status
  ),
  tax_category_id = COALESCE(
    $taxCategoryId,
    tax_category_id
  ),
  optional_vat = COALESCE(
    $optionalVAT,
    optional_vat
  ),
  documents_optional_flag = COALESCE(
    $optionalDocuments,
    documents_optional_flag
  ),
  is_property = COALESCE(
    $isProperty,
    is_property
  )
  WHERE
    id in $$chargeIds
  RETURNING *;
`;

const updateAccountantApproval = sql<IUpdateAccountantApprovalQuery>`
  UPDATE accounter_schema.charges
  SET
    accountant_status = $accountantStatus
  WHERE
    id = $chargeId
  RETURNING *;
`;

const generateCharge = sql<IGenerateChargeQuery>`
  INSERT INTO accounter_schema.charges (owner_id, type, accountant_status, user_description, tax_category_id, optional_vat, documents_optional_flag, is_property)
  VALUES ($ownerId, $type, $accountantStatus, $userDescription, $taxCategoryId, $optionalVAT, $optionalDocuments, $isProperty)
  RETURNING *;
`;

const getChargesByFilters = sql<IGetChargesByFiltersQuery>`
  SELECT
    ec.*,
    ABS(ec.event_amount) as abs_event_amount
  FROM accounter_schema.charges c
  LEFT JOIN accounter_schema.extended_charges ec
    ON c.id = ec.id
  WHERE 
  ($isIDs = 0 OR c.id IN $$IDs)
  AND ($isOwnerIds = 0 OR c.owner_id IN $$ownerIds)
  AND ($isBusinessIds = 0 OR ec.business_array && $businessIds)
  AND ($fromDate ::TEXT IS NULL OR COALESCE(ec.documents_min_date, ec.transactions_min_event_date)::TEXT::DATE >= date_trunc('day', $fromDate ::DATE))
  AND ($fromAnyDate ::TEXT IS NULL OR GREATEST(ec.documents_max_date, ec.transactions_max_event_date, ec.transactions_max_debit_date, ec.ledger_max_invoice_date, ec.ledger_max_value_date)::TEXT::DATE >= date_trunc('day', $fromAnyDate ::DATE))
  AND ($toDate ::TEXT IS NULL OR COALESCE(ec.documents_max_date, ec.transactions_max_event_date)::TEXT::DATE <= date_trunc('day', $toDate ::DATE))
  AND ($toAnyDate ::TEXT IS NULL OR LEAST(ec.documents_min_date, ec.transactions_min_event_date, ec.transactions_min_debit_date, ec.ledger_min_invoice_date, ec.ledger_min_value_date)::TEXT::DATE <= date_trunc('day', $toAnyDate ::DATE))
  AND ($chargeType = 'ALL' OR ($chargeType = 'INCOME' AND ec.transactions_event_amount > 0) OR ($chargeType = 'EXPENSE' AND ec.transactions_event_amount <= 0))
  AND ($withoutInvoice = FALSE OR COALESCE(ec.invoices_count, 0) = 0)
  AND ($withoutReceipt = FALSE OR (COALESCE(ec.receipts_count, 0) = 0 AND (ec.no_invoices_required IS FALSE)))
  AND ($withoutDocuments = FALSE OR COALESCE(ec.documents_count, 0) = 0)
  AND ($withoutTransactions = FALSE OR COALESCE(ec.transactions_count, 0) = 0)
  AND ($withOpenDocuments = FALSE OR ec.open_docs_flag IS TRUE)
  AND ($withoutLedger = FALSE OR COALESCE(ec.ledger_count, 0) = 0)
  AND ($isAccountantStatuses = 0 OR ec.accountant_status = ANY ($accountantStatuses::accounter_schema.accountant_status[]))
  AND ($isTags = 0 OR ec.tags && $tags)
  ORDER BY
  CASE WHEN $asc = true AND $sortColumn = 'event_date' THEN (COALESCE(ec.transactions_min_debit_date, ec.transactions_min_event_date, ec.documents_min_date, ec.ledger_min_value_date, ec.ledger_min_invoice_date), COALESCE(ec.documents_min_date, ec.transactions_min_event_date), ec.id)  END ASC,
  CASE WHEN $asc = false AND $sortColumn = 'event_date'  THEN (COALESCE(ec.transactions_min_debit_date, ec.transactions_min_event_date, ec.documents_min_date, ec.ledger_min_value_date, ec.ledger_min_invoice_date), COALESCE(ec.documents_min_date, ec.transactions_min_event_date), ec.id)  END DESC,
  CASE WHEN $asc = true AND $sortColumn = 'event_amount' THEN (ec.event_amount, ec.id) END ASC,
  CASE WHEN $asc = false AND $sortColumn = 'event_amount'  THEN (ec.event_amount, ec.id) END DESC,
  CASE WHEN $asc = true AND $sortColumn = 'abs_event_amount' THEN ABS(cast(ec.event_amount as DECIMAL)) END ASC,
  CASE WHEN $asc = false AND $sortColumn = 'abs_event_amount'  THEN ABS(cast(ec.event_amount as DECIMAL)) END DESC, ID;
  `;

const getSimilarCharges = sql<IGetSimilarChargesQuery>`
      SELECT *
      FROM accounter_schema.extended_charges
      WHERE (CASE WHEN $withMissingTags IS TRUE THEN
        tags IS NULL
     ELSE
        TRUE
     END)
        AND (CASE WHEN $withMissingDescription IS TRUE THEN
        user_description IS NULL
     ELSE
        TRUE
     END)
        AND (CASE WHEN $tagsDifferentThan::UUID[] IS NOT NULL THEN
        NOT((tags @> $tagsDifferentThan) AND (tags <@ $tagsDifferentThan))
     ELSE
        TRUE
     END)
        AND (CASE WHEN $descriptionDifferentThan::TEXT IS NOT NULL THEN
        NOT(user_description = $descriptionDifferentThan)
     ELSE
        TRUE
     END)
        AND (
          (business_id IS NOT NULL AND business_id = $businessId)
          OR (business_array IS NOT NULL AND business_array @> $businessArray AND business_array <@ $businessArray)
        ) AND owner_id = $ownerId
     ORDER BY (COALESCE(documents_min_date, transactions_min_debit_date, transactions_min_event_date, ledger_min_value_date, ledger_min_invoice_date), COALESCE(transactions_min_event_date, documents_min_date), id) DESC;`;

type IGetAdjustedChargesByFiltersParams = Optional<
  Omit<
    IGetChargesByFiltersParams,
    'isOwnerIds' | 'isBusinessIds' | 'businessIds' | 'isIDs' | 'isTags' | 'tags'
  >,
  'ownerIds' | 'IDs' | 'asc' | 'sortColumn' | 'toDate' | 'fromDate'
> & {
  toDate?: TimelessDateString | null;
  fromDate?: TimelessDateString | null;
  tags?: readonly string[] | null;
  businessIds?: readonly string[] | null;
};

const deleteChargesByIds = sql<IDeleteChargesByIdsQuery>`
    DELETE FROM accounter_schema.charges
    WHERE id IN $$chargeIds;`;
@Injectable({
  scope: Scope.Singleton,
  global: true,
})
export class ChargesProvider {
  cache = getCacheInstance({
    stdTTL: 60 * 60, // 1 hours
  });

  constructor(private dbProvider: DBProvider) {}

  private async batchChargesByIds(ids: readonly string[]) {
    const charges = await getChargesByIds.run(
      {
        chargeIds: ids,
      },
      this.dbProvider,
    );
    return ids.map(id => charges.find(charge => charge.id === id));
  }

  public getChargeByIdLoader = new DataLoader(
    (keys: readonly string[]) => this.batchChargesByIds(keys),
    {
      cacheKeyFn: id => `charge-${id}`,
      cacheMap: this.cache,
    },
  );

  private async batchChargesByTransactionIds(transactionIds: readonly string[]) {
    const charges = await getChargesByTransactionIds.run(
      {
        transactionIds,
      },
      this.dbProvider,
    );
    charges.map(c => this.getChargeByIdLoader.prime(c.id, c));
    return transactionIds.map(id => charges.find(charge => charge.transaction_id === id));
  }

  public getChargeByTransactionIdLoader = new DataLoader(
    (transactionIds: readonly string[]) => this.batchChargesByTransactionIds(transactionIds),
    { cache: false },
  );

  public async getChargesByMissingRequiredInfo() {
    return getChargesByMissingRequiredInfo.run(undefined, this.dbProvider).then(charges =>
      charges.map(c => {
        this.getChargeByIdLoader.prime(c.id, c);
        return c;
      }),
    );
  }

  public updateCharge(params: IUpdateChargeParams) {
    return updateCharge.run(params, this.dbProvider).then(([newCharge]) => {
      if (newCharge) {
        this.invalidateCharge(newCharge.id);
        this.getChargeByIdLoader.prime(newCharge.id, newCharge);
      }
      return newCharge;
    });
  }

  public batchUpdateCharges(params: IBatchUpdateChargesParams) {
    return batchUpdateCharges.run(params, this.dbProvider).then(charges => {
      charges.map(charge => this.getChargeByIdLoader.prime(charge.id, charge));
      return charges;
    });
  }

  public updateAccountantApproval(params: IUpdateAccountantApprovalParams) {
    return updateAccountantApproval.run(params, this.dbProvider).then(([newCharge]) => {
      if (newCharge) {
        this.getChargeByIdLoader.prime(newCharge.id, newCharge);
      }
      return newCharge;
    });
  }

  public generateCharge(params: IGenerateChargeParams) {
    const fullParams = {
      isProperty: false,
      userDescription: null,
      optionalVAT: false,
      optionalDocuments: false,
      accountantStatus: 'UNAPPROVED' as accountant_status,
      ...params,
    };
    return generateCharge.run(fullParams, this.dbProvider).then(([newCharge]) => {
      if (newCharge) {
        this.getChargeByIdLoader.prime(newCharge.id, newCharge);
      }
      return newCharge;
    });
  }

  public getChargesByFilters(params: IGetAdjustedChargesByFiltersParams) {
    const isOwnerIds = !!params?.ownerIds?.filter(Boolean).length;
    const isBusinessIds = !!params?.businessIds?.filter(Boolean).length;
    const isIDs = !!params?.IDs?.length;
    const isTags = !!params?.tags?.length;
    const isAccountantStatuses = !!params?.accountantStatuses?.length;

    const defaults = {
      asc: false,
      sortColumn: 'event_date',
    };

    const fullParams: IGetChargesByFiltersParams = {
      ...defaults,
      isOwnerIds: isOwnerIds ? 1 : 0,
      isBusinessIds: isBusinessIds ? 1 : 0,
      isIDs: isIDs ? 1 : 0,
      isTags: isTags ? 1 : 0,
      isAccountantStatuses: isAccountantStatuses ? 1 : 0,
      ...params,
      fromDate: params.fromDate ?? null,
      toDate: params.toDate ?? null,
      ownerIds: isOwnerIds ? params.ownerIds! : [null],
      businessIds: isBusinessIds ? (params.businessIds! as string[]) : null,
      IDs: isIDs ? params.IDs! : [null],
      tags: isTags ? (params.tags! as string[]) : null,
      chargeType: params.chargeType ?? 'ALL',
      withoutInvoice: params.withoutInvoice ?? false,
      withoutReceipt: params.withoutReceipt ?? false,
      withoutDocuments: params.withoutDocuments ?? false,
      withOpenDocuments: params.withOpenDocuments ?? false,
      withoutTransactions: params.withoutTransactions ?? false,
      withoutLedger: params.withoutLedger ?? false,
      accountantStatuses: isAccountantStatuses ? params.accountantStatuses! : null,
    };
    return getChargesByFilters.run(fullParams, this.dbProvider) as Promise<
      IGetChargesByFiltersResult[]
    >;
  }

  public async getSimilarCharges(params: IGetSimilarChargesParams) {
    try {
      return getSimilarCharges.run(params, this.dbProvider) as Promise<
        IGetChargesByFiltersResult[]
      >;
    } catch (error) {
      const message = 'Failed to fetch similar charges';
      console.error(message, error);
      throw new Error(message);
    }
  }

  public deleteChargesByIds(params: IDeleteChargesByIdsParams) {
    return deleteChargesByIds.run(params, this.dbProvider);
  }

  public async invalidateCharge(chargeId: string) {
    this.getChargeByIdLoader.clear(chargeId);
  }

  public clearCache() {
    this.cache.clear();
  }
}
