/**
 * @file pageLevelActions - Functions for performing page-level operations within a single document in the Syncfusion PDF viewer.
 * Includes rotating, moving, duplicating, splitting, reverting, and deleting individual pages.
 * Each action uses the pdf-lib library for PDF manipulation and updates the page index tracking map to maintain the relationship between current and original page positions.
 */

import type { RefObject } from 'react';
import type { PdfViewerComponent } from '@syncfusion/ej2-react-pdfviewer';
import { degrees, PDFDocument } from 'pdf-lib';
import { toast } from 'react-toastify';
import {
  insertPageIndexPairs,
  removePageIndexPair,
  swapPageIndexPairs,
} from './pageIndexing.ts';
import { addDocument, type Document } from '../api/document.ts';
import { getNextSplitName } from './splitDocumentNames.ts';
import type { Process } from '../api/process.ts';
import type { WorkItem } from '../api/workItem.ts';
import type { RetrieveAndSaveDocumentProps } from './retrieveAndSaveDocument.ts';
import i18next from 'i18next';

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

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

  // Related to saving documents
  retrieveAndSaveDocumentProps: RetrieveAndSaveDocumentProps;
  saveDocument: (
    props: RetrieveAndSaveDocumentProps,
    newDocumentsArray: Document[]
  ) => void;

  // Related to Syncfusion PDF Viewer
  pdfViewerRef: RefObject<PdfViewerComponent>;
  originalPageIndices: Map<number, number | undefined>;
  setOriginalPageIndices: (newMap: Map<number, number | undefined>) => void;
  setPreservedPageNum: (pageNumber: number) => void;

  // Other view state properties
  setDocumentIsLoading: (isLoading: boolean) => void;
  setDocumentActionButtonsDisabled: (disabled: boolean) => void;
  setDeleteDocumentButtonDisabled: (disabled: boolean) => void;
}

/**
 * Rotates the page in the currently selected document at the specified index by the specified number of degrees.
 *
 * @param props Page-level action props
 * @param pageIndex Index of page to rotate
 * @param rotationDegrees Number of degrees to rotate by
 */
export const rotatePageAtIndex = (
  props: PageLevelActionsProps,
  pageIndex: number,
  rotationDegrees: number
) => {
  const {
    selectedDocumentId,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    setPreservedPageNum,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.page_level_actions.functions.rotate_page_at_index';

  const pageNumber = pageIndex + 1;

  if (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')}: ` + pageNumber, {
          toastId: 'rotating-page-' + pageNumber,
          autoClose: false,
        });

        // Rotate page
        const page = pdf.getPage(pageIndex);
        page.setRotation(degrees(page.getRotation().angle + rotationDegrees));

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

        toast.success(`${i18next.t(tKey + '.success')}: ` + pageNumber);
      })
      .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-page-' + pageNumber);
      });
  }
};

/**
 * Moves the page in the currently selected document at the specified index either up or down in the order by 1 place.
 *
 * @param props Page-level action props
 * @param pageIndex Index of page to move
 * @param up Whether to move the page up
 */
export const movePageAtIndex = (
  props: PageLevelActionsProps,
  pageIndex: number,
  up: boolean
) => {
  const {
    selectedDocumentId,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    originalPageIndices,
    setOriginalPageIndices,
    setPreservedPageNum,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.page_level_actions.functions.move_page_at_index';

  const pageNumber = pageIndex + 1;

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

        const movedPageIndex = pageIndex + (up ? -1 : 1);

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

        toast.info(`Moving page ${up ? 'up' : 'down'}: ` + pageNumber, {
          toastId: 'moving-page-' + pageNumber,
          autoClose: false,
        });

        // Reorder page
        const page = pdf.getPage(pageIndex);
        pdf.removePage(pageIndex);
        pdf.insertPage(movedPageIndex, page);
        setOriginalPageIndices(
          swapPageIndexPairs(originalPageIndices, pageIndex, movedPageIndex)
        );

        setPreservedPageNum(movedPageIndex + 1);
        pdfViewerRef.current?.load(await pdf.save(), '');
        setSelectedDocumentContentHasChanged(true);

        toast.success(`${i18next.t(tKey + '.success')}: ` + pageNumber);
      })
      .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('moving-page-' + pageNumber);
      });
  }
};

/**
 * Duplicates the page in the currently selected document at the specified index.
 *
 * @param props Page-level action props
 * @param pageIndex Index of page to duplicate
 */
export const duplicatePageAtIndex = (
  props: PageLevelActionsProps,
  pageIndex: number
) => {
  const {
    selectedDocumentId,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    originalPageIndices,
    setOriginalPageIndices,
    setPreservedPageNum,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.page_level_actions.functions.duplicate_page_at_index';

  const pageNumber = pageIndex + 1;

  if (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')}: ` + pageNumber, {
          toastId: 'duplicating-page-' + pageNumber,
          autoClose: false,
        });

        // Duplicate page
        const [duplicatedPage] = await pdf.copyPages(pdf, [pageIndex]);
        pdf.insertPage(pageIndex, duplicatedPage);
        setOriginalPageIndices(
          insertPageIndexPairs(originalPageIndices, pageIndex + 1, 1)
        );

        setPreservedPageNum(pageNumber + 1);
        pdfViewerRef.current?.load(await pdf.save(), '');
        setSelectedDocumentContentHasChanged(true);

        toast.success(`${i18next.t(tKey + '.success')}: ` + pageNumber);
      })
      .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('duplicating-page-' + pageNumber);
      });
  }
};

/**
 * Splits the currently selected document at the specified page index.
 * Creates a new document with ".split" appended to the file name and saves the current document with pages up to splitIndex.
 *
 * @param props Page-level action props
 * @param splitIndex Index of page to split at
 */
export const splitDocumentAtIndex = (
  props: PageLevelActionsProps,
  splitIndex: number
) => {
  const {
    process,
    workItem,
    documents,
    setDocuments,
    selectedDocumentId,
    retrieveAndSaveDocumentProps,
    saveDocument,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    setPreservedPageNum,
  } = props;

  const tKey = 'utils.page_level_actions.functions.split_document_at_index';

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

    const cleanUp = () => {
      setDocumentIsLoading(false);
      setDocumentActionButtonsDisabled(false);
      setDeleteDocumentButtonDisabled(false);
      toast.dismiss('splitting-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 splitBefore = await PDFDocument.load(originalBytes);
        const splitAfter = await PDFDocument.load(originalBytes);
        const totalPages = splitBefore.getPageCount();

        // Create before and after documents
        for (let i = totalPages - 1; i >= splitIndex; i--) {
          splitBefore.removePage(i);
        }
        for (let i = 0; i < splitIndex; i++) {
          splitAfter.removePage(0);
        }

        const splitBeforeBytes = await splitBefore.save();
        const splitAfterBase64 = await splitAfter.saveAsBase64();

        // Assemble new name for split document
        const splitAfterName = getNextSplitName(
          currentDocument as Document,
          documents
        );

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

        // Add new document with split pages first, then save current document with pages before split
        addDocument(
          process?.id as number,
          workItem?.workItemReferenceId as number,
          currentDocument?.documentTemplateId as number,
          currentDocument?.documentTemplateName as string,
          splitAfterName,
          splitAfterBase64
        )
          .then((newDocument) => {
            const docs = documents.concat(newDocument);
            setDocuments(docs);
            toast.success(`${i18next.t(tKey + '.success')}: ` + splitAfterName);

            setPreservedPageNum(
              pdfViewerRef.current?.currentPageNumber as number
            );
            pdfViewerRef.current?.load(splitBeforeBytes, '');
            saveDocument(retrieveAndSaveDocumentProps, docs);
          })
          .catch(() => {
            toast.error(
              `${i18next.t(tKey + '.errors.failed_to_add')}: ` + splitAfterName
            );
          })
          .finally(() => {
            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();
      });
  }
};

/**
 * Reverts the page in the currently selected document at the specified index to its original (last saved) state.
 *
 * @param props Page-level action props
 * @param pageIndex Index of page to revert
 */
export const revertPageAtIndex = (
  props: PageLevelActionsProps,
  pageIndex: number
) => {
  const {
    selectedDocumentId,
    selectedDocumentContent,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    originalPageIndices,
    setPreservedPageNum,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.page_level_actions.functions.revert_page_at_index';

  const pageNumber = pageIndex + 1;

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

        // Load original and modified documents in pdf-lib
        const documentPath =
          'data:application/pdf;base64,' + selectedDocumentContent;
        const originalPdf = await PDFDocument.load(documentPath);
        const modifiedPdfBytes = await blobData.arrayBuffer();
        const modifiedPdf = await PDFDocument.load(modifiedPdfBytes);

        toast.info(`${i18next.t(tKey + '.info')}: ` + pageNumber, {
          toastId: 'reverting-page-' + pageNumber,
          autoClose: false,
        });

        // Attempt to match page index to original page index
        const matchIndex = originalPageIndices.get(pageIndex);
        if ((matchIndex as number) >= 0) {
          // Revert page to original
          const [originalPage] = await modifiedPdf.copyPages(originalPdf, [
            matchIndex as number,
          ]);
          modifiedPdf.removePage(pageIndex);
          modifiedPdf.insertPage(pageIndex, originalPage);

          setPreservedPageNum(pageNumber);
          pdfViewerRef.current?.load(await modifiedPdf.save(), '');
          setSelectedDocumentContentHasChanged(true);

          toast.success(`${i18next.t(tKey + '.success')}: ` + pageNumber);
        } else {
          toast.error(i18next.t(tKey + '.errors.page_not_found'));
        }
      })
      .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('reverting-page-' + pageNumber);
      });
  }
};

/**
 * Deletes the page in the currently selected document at the specified index.
 *
 * @param props Page-level action props
 * @param pageIndex Index of page to delete
 */
export const deletePageAtIndex = (
  props: PageLevelActionsProps,
  pageIndex: number
) => {
  const {
    selectedDocumentId,
    pdfViewerRef,
    setDocumentIsLoading,
    setDocumentActionButtonsDisabled,
    setDeleteDocumentButtonDisabled,
    originalPageIndices,
    setOriginalPageIndices,
    setPreservedPageNum,
    setSelectedDocumentContentHasChanged,
  } = props;

  const tKey = 'utils.page_level_actions.functions.delete_page_at_index';

  const pageNumber = pageIndex + 1;

  if (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')}: ` + pageNumber, {
          toastId: 'deleting-page-' + pageNumber,
          autoClose: false,
        });

        // Delete page
        pdf.removePage(pageIndex);
        setOriginalPageIndices(
          removePageIndexPair(originalPageIndices, pageIndex)
        );

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

        toast.success(`${i18next.t(tKey + '.success')}: ` + pageNumber);
      })
      .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('deleting-page-' + pageNumber);
      });
  }
};
