/**
 * @file documentLevelActions.ts - Functions for performing document-level operations on work item documents.
 * Includes uploading, duplicating, rotating, merging with previous, printing, and deleting documents.
 * Each action interacts with the IWS5 API and updates application state (documents list, PDF viewer, UI controls) accordingly.
 * Uses the pdf-lib library for PDF manipulation and the Syncfusion PDF viewer for rendering.
 */

import {
  type Document,
  addDocument,
  deleteDocument as apiDeleteDocument,
  getDocumentContent,
  updateDocumentContent,
} from '../api/document';
import type { Process } from '../api/process.ts';
import type { WorkItem } from '../api/workItem.ts';
import type { DocumentTemplate } from '../api/documentTemplate.ts';
import {
  DEFAULT_DOCUMENT_TEMPLATE_ID,
  DEFAULT_DOCUMENT_TEMPLATE_NAME,
  MAX_FILE_NAME_LENGTH,
  MAX_FILE_SIZE,
} from '../constants';
import { toast } from 'react-toastify';
import type { RefObject } from 'react';
import type { PdfViewerComponent } from '@syncfusion/ej2-react-pdfviewer';
import { degrees, PDFDocument } from 'pdf-lib';
import type {
  DocumentNode,
  FolderNode,
  TreeNode,
} from '../components/Processing/DetailsFolderPanel/sections/FolderSection/FolderTree.tsx';
import i18next from 'i18next';

/** Shared props required by all document-level action functions. */
export interface DocumentLevelActionsProps {
  // Work item properties
  process: Process | undefined;
  workItem: WorkItem | undefined;
  documents: Document[];
  setDocuments: (documents: Document[]) => void;
  documentTemplates: DocumentTemplate[];

  // Related to FolderSection/FolderTree
  folderNode: FolderNode;
  documentNodes: DocumentNode[];
  selectedNode: TreeNode | undefined;
  setSelectedNode: (node: TreeNode) => void;

  // Selected document properties
  selectedDocumentId: number | undefined;
  setSelectedDocumentContentHasChanged: (hasChanged: boolean) => void;

  // Related to Syncfusion PDF Viewer
  pdfViewerRef: RefObject<PdfViewerComponent>;

  // Other view state properties
  setUploadDocumentProcessing: (processing: boolean) => void;
  setDocumentIsLoading: (loading: boolean) => void;
  setDocumentActionButtonsDisabled: (disabled: boolean) => void;
  setDeleteDocumentButtonDisabled: (disabled: boolean) => void;

  // Related to deleting documents
  setDocumentIdToDelete: (documentId: number) => void;
  setDeleteDocumentProcessing: (processing: boolean) => void;
  hideDeleteDocumentModal: () => void;
}

/**
 * Reads an uploaded file's content and creates a new document for the current work item.
 *
 * @param props Document-level action props
 * @param file Uploaded file
 */
export const uploadDocument = async (
  props: DocumentLevelActionsProps,
  file: File
) => {
  const {
    process,
    workItem,
    documents,
    setDocuments,
    documentTemplates,
    setUploadDocumentProcessing,
  } = props;

  const tKey = 'utils.document_level_actions.functions.upload_document';

  const reader = new FileReader();

  reader.onload = () => {
    const result = reader.result as string;
    const base64 = result.split(',')[1];

    const processDefaultTemplate = documentTemplates.find(
      (template) => template.isDefault
    );
    addDocument(
      process?.id as number,
      workItem?.workItemReferenceId as number,
      processDefaultTemplate?.templateId || DEFAULT_DOCUMENT_TEMPLATE_ID,
      processDefaultTemplate?.templateName || DEFAULT_DOCUMENT_TEMPLATE_NAME,
      file.name,
      base64
    )
      .then((newDocument) => {
        setDocuments(documents.concat(newDocument));

        toast.success(`${i18next.t(tKey + '.success')}: ` + file.name);
      })
      .catch(() => {
        toast.error(
          `${i18next.t(tKey + '.errors.failed_to_upload')}: ` + file.name
        );
      })
      .finally(() => {
        setUploadDocumentProcessing(false);
      });
  };

  reader.onerror = () => {
    toast.error(`${i18next.t(tKey + '.errors.failed_to_read')}: ` + file.name);
    setUploadDocumentProcessing(false);
  };

  // Guard against large file names and file sizes
  if (file.name.length > MAX_FILE_NAME_LENGTH) {
    toast.error(
      i18next.t(tKey + '.errors.file_name_too_long', {
        max_length: MAX_FILE_NAME_LENGTH,
      })
    );
  } else if (file.size > MAX_FILE_SIZE) {
    toast.error(
      i18next.t(tKey + '.errors.file_size_too_large', {
        max_size: MAX_FILE_SIZE / 1024 / 1024,
      })
    );
  } else {
    setUploadDocumentProcessing(true);
    reader.readAsDataURL(file);
  }
};

/**
 * Duplicates the currently selected document.
 *
 * @param props Document-level action props
 */
export const duplicateDocument = async (props: DocumentLevelActionsProps) => {
  const {
    process,
    workItem,
    documents,
    setDocuments,
    selectedDocumentId,
    pdfViewerRef,
  } = props;

  const tKey = 'utils.document_level_actions.functions.duplicate_document';

  if (selectedDocumentId) {
    const document = documents.find(
      (_document) => _document.documentId === selectedDocumentId
    );

    // Get PDF viewer document as blob
    pdfViewerRef.current
      ?.saveAsBlob()
      .then((blobData) => {
        const reader = new FileReader();

        reader.onload = () => {
          const result = reader.result as string;
          const base64 = result.split(',')[1];

          toast.info(
            `${i18next.t(tKey + '.info')}: ` + document?.documentName,
            {
              toastId: 'duplicating-document-' + document?.documentId,
              autoClose: false,
            }
          );

          addDocument(
            process?.id as number,
            workItem?.workItemReferenceId as number,
            document?.documentTemplateId as number,
            document?.documentTemplateName as string,
            document?.documentName as string,
            base64
          )
            .then((newDocument) => {
              setDocuments(documents.concat(newDocument));

              toast.success(
                `${i18next.t(tKey + '.success')}: ` + document?.documentName
              );
            })
            .catch(() => {
              toast.error(
                `${i18next.t(tKey + '.errors.failed_to_duplicate')}: ` +
                  document?.documentName
              );
            })
            .finally(() => {
              toast.dismiss('duplicating-document-' + document?.documentId);
            });
        };

        reader.onerror = () => {
          toast.error(
            `${i18next.t(tKey + '.errors.failed_to_read')}: ` +
              document?.documentName
          );
        };

        reader.readAsDataURL(blobData);
      })
      .catch(() => {
        toast.error(i18next.t(tKey + '.errors.failed_to_save'));
      });
  }
};

/**
 * Rotates all the pages in the currently selected document by 90 degrees.
 *
 * @param props Document-level action props
 */
export const rotateDocument = (props: DocumentLevelActionsProps) => {
  const {
    documents,
    selectedDocumentId,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.document_level_actions.functions.rotate_document';

  if (selectedDocumentId) {
    const currentDocument = documents.find(
      (document) => document.documentId === selectedDocumentId
    );

    // Get PDF viewer document as blob
    pdfViewerRef.current
      ?.saveAsBlob()
      .then(async (blobData) => {
        setDocumentIsLoading(true);
        setDocumentActionButtonsDisabled(true);
        setDeleteDocumentButtonDisabled(true);

        // Load document in pdf-lib
        const pdfBytes = await blobData.arrayBuffer();
        const pdf = await PDFDocument.load(pdfBytes);

        toast.info(
          `${i18next.t(tKey + '.info')}: ` + currentDocument?.documentName,
          {
            toastId: 'rotating-document-' + currentDocument?.documentId,
            autoClose: false,
          }
        );

        // Rotate all pages 90 degrees from current rotation angle
        for (let i = 0; i < pdf.getPageCount(); i++) {
          const currentPage = pdf.getPage(i);
          currentPage.setRotation(
            degrees(currentPage.getRotation().angle + 90)
          );
        }

        pdfViewerRef.current?.load(await pdf.save(), '');
        setSelectedDocumentContentHasChanged(true);

        toast.success(
          `${i18next.t(tKey + '.success')}: ` + currentDocument?.documentName
        );
      })
      .catch((error: Error) => {
        if (error.message.includes('encrypted')) {
          toast.error(i18next.t(tKey + '.errors.pdf_encrypted'));
          return;
        }
        toast.error(i18next.t(tKey + '.errors.failed_to_save'));
      })
      .finally(() => {
        setDocumentIsLoading(false);
        setDocumentActionButtonsDisabled(false);
        setDeleteDocumentButtonDisabled(false);
        toast.dismiss('rotating-document-' + currentDocument?.documentId);
      });
  }
};

/**
 * Merges the currently selected document with the previous document in the folder tree.
 *
 * @param props Document-level action props
 */
export const mergeDocumentWithPrevious = (props: DocumentLevelActionsProps) => {
  const {
    process,
    workItem,
    documents,
    documentNodes,
    selectedDocumentId,
    selectedNode,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
  } = props;

  const tKey =
    'utils.document_level_actions.functions.merge_document_with_previous';

  if (selectedDocumentId) {
    const currentDocument = documents.find(
      (document) => document.documentId === selectedDocumentId
    );

    const cleanUp = () => {
      setDocumentIsLoading(false);
      setDocumentActionButtonsDisabled(false);
      toast.dismiss('merging-document-' + currentDocument?.documentId);
    };

    // Get PDF viewer document as blob
    pdfViewerRef.current
      ?.saveAsBlob()
      .then(async (blobData) => {
        setDocumentIsLoading(true);
        setDocumentActionButtonsDisabled(true);
        setDeleteDocumentButtonDisabled(true);

        // Load document in pdf-lib
        const originalBytes = await blobData.arrayBuffer();
        const originalPdf = await PDFDocument.load(originalBytes);

        toast.info(
          `${i18next.t(tKey + '.info')}: ` + currentDocument?.documentName,
          {
            toastId: 'merging-document-' + currentDocument?.documentId,
            autoClose: false,
          }
        );

        // Retrieve previous document content
        const previousNode =
          documentNodes[
            documentNodes.indexOf(selectedNode as DocumentNode) - 1
          ];
        getDocumentContent(previousNode.documentId)
          .then(async (content) => {
            const previousPdf = await PDFDocument.load(content);

            // Copy pages from current to previous
            const pageIndices = Array.from(
              { length: originalPdf.getPageCount() },
              (_, i) => i
            );
            const copiedPages = await previousPdf.copyPages(
              originalPdf,
              pageIndices
            );
            copiedPages.forEach((page) => previousPdf.addPage(page));

            // Update previous document content
            const previousPdfBase64 = await previousPdf.saveAsBase64({
              objectsPerTick: 50,
              updateFieldAppearances: false,
            });
            updateDocumentContent(
              process?.id as number,
              workItem?.workItemReferenceId as number,
              previousNode.documentId,
              previousPdfBase64
            )
              .then(() => {
                // Delete current document
                deleteDocument(props)
                  .then(() => {
                    toast.success(i18next.t(tKey + '.success'));
                  })
                  .catch(() => {
                    toast.error(i18next.t(tKey + '.errors.failed_to_delete'));
                  })
                  .finally(() => {
                    setDocumentIsLoading(false);
                    setDocumentActionButtonsDisabled(false);
                    setDeleteDocumentButtonDisabled(false);
                    toast.dismiss(
                      'merging-document-' + currentDocument?.documentId
                    );
                  });
              })
              .catch(() => {
                toast.error(i18next.t(tKey + '.errors.failed_to_update'));
                cleanUp();
              });
          })
          .catch(() => {
            toast.error(i18next.t(tKey + '.errors.failed_to_retrieve'));
            cleanUp();
          });
      })
      .catch((error: Error) => {
        if (error.message.includes('encrypted')) {
          toast.error(i18next.t(tKey + '.errors.pdf_encrypted'));
        } else {
          toast.error(i18next.t(tKey + '.errors.failed_to_save'));
        }
        cleanUp();
      });
  }
};

/**
 * Prints the currently selected document.
 *
 * @param props Document-level action props
 */
export const printDocument = async (props: DocumentLevelActionsProps) => {
  const { selectedDocumentId, pdfViewerRef } = props;

  const tKey = 'utils.document_level_actions.functions.print_document';

  if (selectedDocumentId) {
    // Get PDF viewer document as blob
    pdfViewerRef.current
      ?.saveAsBlob()
      .then((blobData) => {
        const blobUrl = URL.createObjectURL(blobData);

        // Create embedded iframe to hold blob content
        const iframe = document.createElement('iframe');
        iframe.style.display = 'none';
        iframe.src = blobUrl;

        const cleanUp = () => {
          iframe.remove();
          URL.revokeObjectURL(blobUrl);
        };

        iframe.onload = () => {
          try {
            // Give control to the iframe, and call print
            iframe.contentWindow?.focus();

            // requestAnimationFrame is used twice to act as a delay after focus
            requestAnimationFrame(() => {
              requestAnimationFrame(() => {
                iframe.contentWindow?.print();

                // Clean up after print dialog
                const listener = () => {
                  cleanUp();
                  window.removeEventListener('afterprint', listener);
                };
                window.addEventListener('afterprint', listener);
              });
            });
          } catch {
            cleanUp();
          }
        };

        document.body.appendChild(iframe);
      })
      .catch(() => {
        toast.error(i18next.t(tKey + '.errors.failed_to_save'));
      });
  }
};

/**
 * Deletes the currently selected document.
 *
 * @param props Document-level action props
 */
export const deleteDocument = async (props: DocumentLevelActionsProps) => {
  const {
    selectedDocumentId,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    setDeleteDocumentProcessing,
    setDocumentIdToDelete,
    folderNode,
    setSelectedNode,
    hideDeleteDocumentModal,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.document_level_actions.functions.delete_document';

  if (selectedDocumentId) {
    setDeleteDocumentProcessing(true);

    apiDeleteDocument(selectedDocumentId)
      .then(() => {
        setDocumentIdToDelete(selectedDocumentId);
        setSelectedDocumentContentHasChanged(false);
        setDocumentActionButtonsDisabled(true);
        setDeleteDocumentButtonDisabled(true);
        setSelectedNode(folderNode);

        toast.success(i18next.t(tKey + '.success'));
        hideDeleteDocumentModal();
      })
      .catch(() => {
        toast.error(i18next.t(tKey + '.errors.failed_to_delete'));
      })
      .finally(() => {
        setDeleteDocumentProcessing(false);
      });
  }
};
