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 {
  IDeleteIssuedDocumentParams,
  IDeleteIssuedDocumentQuery,
  IGetAllIssuedDocumentsParams,
  IGetAllIssuedDocumentsQuery,
  IGetAllIssuedDocumentsResult,
  IGetIssuedDocumentsByClientIdsQuery,
  IGetIssuedDocumentsByExternalIdsQuery,
  IGetIssuedDocumentsByIdsQuery,
  IGetIssuedDocumentsByTypeParams,
  IGetIssuedDocumentsByTypeQuery,
  IGetIssuedDocumentsStatusByChargeIdsQuery,
  IInsertIssuedDocumentsParams,
  IInsertIssuedDocumentsQuery,
  IUpdateIssuedDocumentByExternalIdParams,
  IUpdateIssuedDocumentByExternalIdQuery,
  IUpdateIssuedDocumentParams,
  IUpdateIssuedDocumentQuery,
} from '../types.js';

const getAllIssuedDocuments = sql<IGetAllIssuedDocumentsQuery>`
  SELECT *
  FROM accounter_schema.documents_issued;
`;

const getIssuedDocumentsByIds = sql<IGetIssuedDocumentsByIdsQuery>`
  SELECT *
  FROM accounter_schema.documents_issued
  WHERE id IN $$Ids;
`;

const getIssuedDocumentsByExternalIds = sql<IGetIssuedDocumentsByExternalIdsQuery>`
  SELECT *
  FROM accounter_schema.documents_issued
  WHERE external_id IN $$externalIds;
`;

const getIssuedDocumentsByClientIds = sql<IGetIssuedDocumentsByClientIdsQuery>`
  SELECT d.*, di.external_id, di.status, di.linked_document_ids
  FROM accounter_schema.documents_issued di
  LEFT JOIN accounter_schema.documents d
    ON di.id = d.id
  WHERE d.debtor_id IN $$clientIds OR d.creditor_id IN $$clientIds;
`;

const getIssuedDocumentsStatusByChargeIds = sql<IGetIssuedDocumentsStatusByChargeIdsQuery>`
  SELECT
    documents.charge_id,
    COALESCE(
      bool_or(
        documents_issued.status = 'OPEN'::accounter_schema.document_status
      ),
      false
    ) AS open_docs_flag
  FROM
    accounter_schema.documents_issued
    LEFT JOIN accounter_schema.documents ON documents.id = documents_issued.id
  WHERE
    documents.charge_id in $$chargeIds
  GROUP BY
    documents.charge_id
`;

const getIssuedDocumentsByType = sql<IGetIssuedDocumentsByTypeQuery>`
  SELECT d.*, di.external_id, di.status, di.linked_document_ids
  FROM accounter_schema.documents_issued di
  LEFT JOIN accounter_schema.documents d
    ON di.id = d.id
  WHERE d.type = $type
  ORDER BY d.serial_number DESC
  LIMIT $limit;
`;

const updateIssuedDocument = sql<IUpdateIssuedDocumentQuery>`
  UPDATE accounter_schema.documents_issued
  SET
  external_id = COALESCE(
    $externalId,
    external_id
  ),
  status = COALESCE(
    $status,
    status
  ),
  linked_document_ids = COALESCE(
    $linkedDocumentIds,
    linked_document_ids
  )
  WHERE
    id = $documentId
  RETURNING *;
`;

const updateIssuedDocumentByExternalId = sql<IUpdateIssuedDocumentByExternalIdQuery>`
  UPDATE accounter_schema.documents_issued
  SET
  id = COALESCE(
    $documentId,
    id
  ),
  status = COALESCE(
    $status,
    status
  ),
  linked_document_ids = COALESCE(
    $linkedDocumentIds,
    linked_document_ids
  )
  WHERE
    external_id = $externalId
  RETURNING *;
`;

const deleteIssuedDocument = sql<IDeleteIssuedDocumentQuery>`
  DELETE FROM accounter_schema.documents_issued
  WHERE id = $documentId
  RETURNING id;
`;

const insertIssuedDocuments = sql<IInsertIssuedDocumentsQuery>`
    INSERT INTO accounter_schema.documents_issued (
      id,
      external_id,
      status,
      linked_document_ids,
      owner_id
    )
    VALUES $$issuedDocuments(
      id,
      externalId,
      status,
      linkedDocumentIds,
      ownerId
    )
    RETURNING *;`;

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

  private allIssuedDocumentsCache: Promise<IGetAllIssuedDocumentsResult[]> | null = null;
  public async getAllIssuedDocuments(params: IGetAllIssuedDocumentsParams) {
    if (this.allIssuedDocumentsCache) {
      return this.allIssuedDocumentsCache;
    }
    this.allIssuedDocumentsCache = getAllIssuedDocuments.run(params, this.db).then(docs => {
      docs.map(doc => {
        this.getIssuedDocumentsByIdLoader.prime(doc.id, doc);
      });
      return docs;
    });
    return this.allIssuedDocumentsCache;
  }

  private async batchIssuedDocumentsByIds(ids: readonly string[]) {
    const uniqueIDs = [...new Set(ids)];
    try {
      const docs = await getIssuedDocumentsByIds.run({ Ids: uniqueIDs }, this.db);

      return ids.map(id => docs.find(doc => doc.id === id));
    } catch (e) {
      console.error(e);
      return ids.map(() => null);
    }
  }

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

  private async batchIssuedDocumentsByExternalIds(externalIds: readonly string[]) {
    const uniqueExternalIDs = [...new Set(externalIds)];
    try {
      const docs = await getIssuedDocumentsByExternalIds.run(
        { externalIds: uniqueExternalIDs },
        this.db,
      );

      return externalIds.map(id => docs.find(doc => doc.external_id === id));
    } catch (e) {
      console.error(e);
      return externalIds.map(() => null);
    }
  }

  public getIssuedDocumentsByExternalIdLoader = new DataLoader((externalIds: readonly string[]) =>
    this.batchIssuedDocumentsByExternalIds(externalIds),
  );

  private async batchIssuedDocumentsByClientIds(clientIds: readonly string[]) {
    const uniqueClientIDs = [...new Set(clientIds)];
    try {
      const docs = await getIssuedDocumentsByClientIds.run({ clientIds: uniqueClientIDs }, this.db);

      return clientIds.map(id =>
        docs.filter(doc => {
          this.getIssuedDocumentsByIdLoader.prime(doc.id, doc);
          return doc.creditor_id === id || doc.debtor_id === id;
        }),
      );
    } catch (e) {
      const message = 'Error fetching issued documents by client IDs';
      console.error(message, e);
      throw new Error(message, { cause: e });
    }
  }

  public getIssuedDocumentsByClientIdLoader = new DataLoader(
    (clientIds: readonly string[]) => this.batchIssuedDocumentsByClientIds(clientIds),
    {
      cache: false,
    },
  );

  private async batchIssuedDocumentsStatusByChargeIds(chargeIds: readonly string[]) {
    const uniqueClientIDs = [...new Set(chargeIds)];
    try {
      const statuses = await getIssuedDocumentsStatusByChargeIds.run(
        { chargeIds: uniqueClientIDs },
        this.db,
      );

      return chargeIds.map(id =>
        statuses.find(status => {
          return status.charge_id === id;
        }),
      );
    } catch (e) {
      const message = 'Error fetching issued documents status by charge IDs';
      console.error(message, e);
      throw new Error(message, { cause: e });
    }
  }

  public getIssuedDocumentsStatusByChargeIdLoader = new DataLoader(
    (chargeIds: readonly string[]) => this.batchIssuedDocumentsStatusByChargeIds(chargeIds),
    {
      cache: false,
    },
  );

  public async getIssuedDocumentsByType(params: IGetIssuedDocumentsByTypeParams) {
    return getIssuedDocumentsByType.run(params, this.db).then(docs => {
      docs.map(doc => {
        this.getIssuedDocumentsByIdLoader.prime(doc.id, doc);
      });
      return docs;
    });
  }

  public async updateIssuedDocument(params: IUpdateIssuedDocumentParams) {
    if (params.documentId) {
      this.invalidateById(params.documentId);
    }
    return updateIssuedDocument.run(params, this.db);
  }

  public async updateIssuedDocumentByExternalId(params: IUpdateIssuedDocumentByExternalIdParams) {
    return updateIssuedDocumentByExternalId.run(params, this.db).then(res => {
      if (res[0]?.id) {
        this.invalidateById(res[0].id);
      }
      return res;
    });
  }

  public async deleteIssuedDocument(params: IDeleteIssuedDocumentParams) {
    if (params.documentId) {
      this.invalidateById(params.documentId);
    }
    return deleteIssuedDocument.run(params, this.db);
  }

  public async insertIssuedDocuments(params: IInsertIssuedDocumentsParams) {
    this.allIssuedDocumentsCache = null;
    const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
    const issuedDocumentsWithOwnerId = params.issuedDocuments.map(doc =>
      reassureOwnerIdExists(doc, ownerId),
    );
    return insertIssuedDocuments.run({ issuedDocuments: issuedDocumentsWithOwnerId }, this.db);
  }

  public async invalidateById(id: string) {
    this.getIssuedDocumentsByIdLoader.clear(id);
    this.allIssuedDocumentsCache = null;
    this.getIssuedDocumentsStatusByChargeIdLoader.clearAll();
  }

  public clearCache() {
    this.getIssuedDocumentsByIdLoader.clearAll();
    this.getIssuedDocumentsByExternalIdLoader.clearAll();
    this.getIssuedDocumentsByClientIdLoader.clearAll();
    this.getIssuedDocumentsStatusByChargeIdLoader.clearAll();
    this.allIssuedDocumentsCache = null;
  }
}
