/**
 * @file ScanInsertModal.tsx - Modal for inserting scanned pages into the currently viewed PDF document via a WebSocket connection to the RWP Companion desktop application.
 *
 * Supports three insertion modes:
 * - "before" (before current page)
 * - "after" (after current page)
 * - "end" (append to document)
 *
 * After receiving scanned pages, uses pdf-lib to determine how many pages were added and updates the original page indices map so page revert still works correctly.
 */

import { type FC, useEffect, useRef, useState } from 'react';
import { Alert, Modal, Spinner } from 'react-bootstrap';
import { RWP_COMPANION_WEBSOCKET_ADDRESS } from '../../../../constants.ts';
import {
  type ScanInsertMode,
  type CompanionRequest,
  type CompanionResponse,
  type ScanInsertStatus,
} from '../../../../types/companionTool.ts';
import { useDocument } from '../../../../contexts/DocumentContext.tsx';
import { toast } from 'react-toastify';
import { insertPageIndexPairs } from '../../../../utils/pageIndexing.ts';
import { PDFDocument } from 'pdf-lib';
import { useScanInsertHotkeys } from '../../../../hooks/hotkeys/useScanInsertHotkeys.ts';
import { UAParser } from 'ua-parser-js';
import { useTranslation } from 'react-i18next';

/**
 * Props for the ScanInsertModal component.
 */
interface ScanInsertModalProps {
  /** Whether the modal is currently visible */
  show: boolean;

  /** Setter to control modal visibility (used by automatic scan hotkeys) */
  setShow: (show: boolean) => void;

  /** Callback to close the modal */
  onHide: () => void;

  /** Insertion mode: 'before', 'after', or 'end' relative to the current page */
  mode: ScanInsertMode;

  /** Setter to preserve the current page number across document reloads */
  setPreservedPageNum: (pageNum: number) => void;

  /** Map tracking original page indices for page revert functionality */
  originalPageIndices: Map<number, number | undefined>;

  /** Setter to update the original page indices map after insertion */
  setOriginalPageIndices: (newMap: Map<number, number | undefined>) => void;
}

/**
 * Scan Insert modal that manages the WebSocket lifecycle to the RWP Companion application.
 * Displays connection status, processing state, and error/success alerts.
 */
export const ScanInsertModal: FC<ScanInsertModalProps> = ({
  show,
  setShow,
  onHide,
  mode,
  setPreservedPageNum,
  originalPageIndices,
  setOriginalPageIndices,
}) => {
  const { pdfViewerRef, setSelectedDocumentContentHasChanged } = useDocument();

  // Persistent WebSocket ref across renders; managed by the show effect
  const webSocket = useRef<WebSocket>(null);
  // Tracks the current WebSocket/operation lifecycle state for UI rendering
  const [status, setStatus] = useState<
    'initial' | 'connected' | ScanInsertStatus | 'error'
  >('initial');
  // When true, triggers an automatic scan operation via hotkey (bypasses companion UI)
  const [auto, setAuto] = useState(false);

  const { os } = UAParser(navigator.userAgent);

  // Call custom hook for ScanInsertModal hotkeys
  useScanInsertHotkeys({
    setAuto,
    setShow,
    isWindows: os.is('Windows'),
  });

  /**
   * Calculates the insertion index based on the current mode (before/after/end)
   *
   * @param currentPageNumber Current page number
   * @param totalPages Number of total pages
   */
  const getPageIndex = (currentPageNumber: number, totalPages: number) => {
    return mode === 'before'
      ? currentPageNumber - 1
      : mode === 'after'
        ? currentPageNumber
        : totalPages;
  };

  // WebSocket lifecycle: opens connection when modal is shown, closes when hidden
  // On open: converts current PDF to base64 blob and sends to companion with insertion index
  // On message: receives PDF with inserted scans, reloads in viewer, updates page index map
  // On close (code 1006): companion app is not running, show error state
  useEffect(() => {
    if (show) {
      setStatus('initial');

      // Establish WebSocket connection to RWP Companion application
      webSocket.current = new WebSocket(RWP_COMPANION_WEBSOCKET_ADDRESS);

      // Handle response from companion: reload document with inserted scans
      webSocket.current.onmessage = async (event) => {
        const message: CompanionResponse = JSON.parse(event.data);
        setStatus(message.status as ScanInsertStatus);

        const prevNumPages = pdfViewerRef.current?.pageCount as number;
        const prevPageNum = pdfViewerRef.current?.currentPageNumber as number;
        setPreservedPageNum(prevPageNum);

        const documentPath =
          'data:application/pdf;base64,' +
          (message as CompanionResponse).pdfBase64;
        pdfViewerRef.current?.load(documentPath, '');
        setSelectedDocumentContentHasChanged(true);

        // Use pdf-lib to count pages in the new document and calculate how many were added
        const newNumPages = (
          await PDFDocument.load((message as CompanionResponse).pdfBase64)
        ).getPageCount();
        const numPagesAdded = newNumPages - prevNumPages;
        // Update the original page indices map so page revert still tracks correctly
        setOriginalPageIndices(
          insertPageIndexPairs(
            originalPageIndices,
            getPageIndex(prevPageNum, prevNumPages),
            numPagesAdded
          )
        );

        webSocket.current?.close();
      };

      // On connection established: extract PDF as blob, convert to base64, send to companion
      webSocket.current.onopen = () => {
        setStatus('connected');

        // Save PDF as blob and send to companion app
        pdfViewerRef.current
          ?.saveAsBlob()
          .then((blobData) => {
            const reader = new FileReader();

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

              // Send scan insert request with page index and optional auto-scan flag
              webSocket.current?.send(
                JSON.stringify({
                  pdfBase64: base64,
                  pageIndex: getPageIndex(
                    pdfViewerRef.current?.currentPageNumber as number,
                    pdfViewerRef.current?.pageCount as number
                  ),
                  operationMode: 'scan',
                  automaticOperation: auto ? 'scan' : undefined,
                } as CompanionRequest)
              );

              if (auto) {
                setAuto(false);
              }
            };

            reader.onerror = () => {
              toast.error(t('effects.show.errors.failed_to_read'));
            };

            reader.readAsDataURL(blobData);
          })
          .catch(() => {
            toast.error(t('effects.show.errors.failed_to_save'));
          });
      };

      // Handle WebSocket close: code 1006 indicates abnormal closure (companion not running)
      webSocket.current.onclose = (event) => {
        // Handle unexpected closure (including companion not running)
        if (event.code === 1006) {
          setStatus('error');
          return;
        }

        onHide();
      };
    } else {
      // Clean up WebSocket when modal is hidden
      webSocket.current?.close();
      webSocket.current = null;
    }
  }, [show]);

  const { t } = useTranslation(undefined, {
    keyPrefix: 'components.processing.document_editor_panel.modals.scan_insert',
  });

  return (
    <Modal show={show} onHide={onHide} backdrop={'static'} centered>
      <Modal.Header closeButton>
        <Modal.Title>{t('modal.header.title')}</Modal.Title>
      </Modal.Header>
      <Modal.Body>
        <div>
          {t('modal.body.paragraph_text')}
          <br />
          <br />
          <i>{t('modal.body.italics_text')}</i>
        </div>
        <hr />
        {status === 'error' ? (
          <Alert variant={'danger'} className={'text-center mb-0'}>
            {t('modal.body.alerts.error')}
          </Alert>
        ) : status === 'Scans inserted' ? (
          <Alert variant={'success'} className={'text-center mb-0'}>
            {t('modal.body.alerts.scans_inserted')}
          </Alert>
        ) : (
          <div
            className={`d-flex flex-column gap-2 justify-content-center align-items-center`}
          >
            <Spinner animation={'border'} role={'status'} variant={'primary'}>
              <span className={'visually-hidden'}>
                {t('modal.body.loading.spinner_text')}...
              </span>
            </Spinner>
            <div>
              <i>
                {status === 'connected'
                  ? t('modal.body.loading.status_messages.connected')
                  : t('modal.body.loading.status_messages.default')}
              </i>
            </div>
          </div>
        )}
      </Modal.Body>
    </Modal>
  );
};
