/**
 * @file FolderTree.tsx - Hierarchical document tree component that displays a top-level folder node (representing the work item) with nested document nodes.
 * Supports inline renaming of both folder and document nodes, paginated document listing via TanStack Table, split-document hierarchy sorting, and node selection with unsaved changes guards (manual or auto-save).
 * Uses useNamedAnchorRemainingHeight for dynamic scroll-area sizing.
 */

import {
  type Dispatch,
  type FC,
  Fragment,
  type SetStateAction,
  useEffect,
  useRef,
  useState,
} from 'react';
import {
  type Cell,
  getCoreRowModel,
  getPaginationRowModel,
  type PaginationState,
  type RowSelectionState,
  useReactTable,
} from '@tanstack/react-table';
import {
  Badge,
  Button,
  Form,
  ListGroup,
  OverlayTrigger,
  Tooltip,
} from 'react-bootstrap';
import { Icon, type IconName } from '../../../../Icon.tsx';
import { CustomPaginationControls } from '../../../../shared/CustomPaginationControls.tsx';
import { toast } from 'react-toastify';
import { renameWorkItem } from '../../../../../api/workItem.ts';
import { useProcessPermissions } from '../../../../../contexts/ProcessPermissionsContext.tsx';
import { AtAllowRights } from '../../../../../types/atAllowRights.ts';
import { useDocument } from '../../../../../contexts/DocumentContext.tsx';
import { useProcessingUI } from '../../../../../contexts/ProcessingUIContext.tsx';
import { getStoredSaveMode } from '../../../../../utils/localStorage.ts';
import { renameDocument } from '../../../../../api/document.ts';
import { useWorkItem } from '../../../../../contexts/WorkItemContext.tsx';
import { useNamedAnchorRemainingHeight } from '../../../../../hooks/useNamedAnchorRemainingHeight.ts';
import { useFolderTreeHotkeys } from '../../../../../hooks/hotkeys/useFolderTreeHotkeys.ts';
import { UAParser } from 'ua-parser-js';
import {
  MAX_FILE_NAME_LENGTH,
  MAX_WORK_ITEM_NAME_LENGTH,
} from '../../../../../constants.ts';
import { useTranslation } from 'react-i18next';

/**
 * Represents a document node in the folder tree.
 */
export interface DocumentNode {
  /** ID of document */
  documentId: number;

  /** Name of document */
  documentName: string;
}

/**
 * Represents the top-level folder node (work item) in the folder tree.
 */
export interface FolderNode {
  /** Folder ID (work item reference ID) */
  folderId: number;

  /** Folder name (work item name/description) */
  folderName: string;
}

/**
 * Union type for any node in the folder tree (either a folder or document).
 */
export type TreeNode = FolderNode | DocumentNode;

/**
 * Props for the FolderTree component.
 */
interface TreeProps {
  /** Top-level folder node representing the work item */
  folderNode: FolderNode;

  /** Callback to propagate folder (work item) name changes to parent */
  updateFolderName: (newName: string) => void;

  /** Array of document nodes to display under the folder */
  documentNodes: DocumentNode[];

  /** Optional callback to propagate document name changes to parent */
  updateDocumentNames?: (newDocumentNodes: DocumentNode[]) => void;

  /** Optional callback providing the internal selection setter for external control */
  onNodeSelectionSetterReady?: (
    setter: Dispatch<SetStateAction<RowSelectionState>>
  ) => void;
}

/**
 * Returns a Bootstrap icon with name and color strings matching the document extension of the supplied DocumentNode object.
 *
 * @param node A DocumentNode object
 * @returns A Bootstrap icon IconName and color class
 */
export function getDocumentIcon(node: DocumentNode): {
  iconName: IconName;
  colorClass?: string;
} {
  const split = node.documentName.split('.');

  const extension = split[split.length - 1].toLowerCase();
  switch (extension) {
    case 'pdf':
      return { iconName: 'FileEarmarkPdfFill', colorClass: 'text-danger' };
    default:
      return { iconName: 'FileEarmarkFill' };
  }
}

/**
 * Tree component that displays documents under a top-level folder and allows selecting/renaming tree nodes.
 */
export const FolderTree: FC<TreeProps> = ({
  folderNode,
  updateFolderName,
  documentNodes,
  updateDocumentNames,
  onNodeSelectionSetterReady,
}) => {
  // Required contexts for work item processing
  const { process } = useWorkItem();
  const {
    selectedDocumentId,
    selectedDocumentContentHasChanged,
    pdfViewerRef,
    documentIsLoading,
  } = useDocument();
  const {
    selectedNode,
    setSelectedNode,
    selectedNodeFieldsHaveChanged,
    unsavedChangesModalShown,
    showUnsavedChangesModal,
    saveWorkItem,
    setSaveWorkItem,
    autoSavingNextNode,
    setAutoSavingNextNode,
    isMobile,
  } = useProcessingUI();
  const { hasPermission } = useProcessPermissions();

  const pageSize = 10;

  // Keeps track of the current pagination state
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: pageSize,
  });

  // Keeps track of the current node selection state
  const [nodeSelection, setNodeSelection] = useState<RowSelectionState>({});
  // Expose the setter to parent component
  useEffect(() => {
    if (onNodeSelectionSetterReady) {
      onNodeSelectionSetterReady(setNodeSelection);
    }
  }, [onNodeSelectionSetterReady]);
  // Pass selected node to calling component
  useEffect(() => {
    const selectedNodes = table
      .getSelectedRowModel()
      .rows.map((row) => row.original);

    setSelectedNode(selectedNodes[0]);
  }, [nodeSelection]);

  /**
   * Handles folder node selection with unsaved changes guard.
   * If auto-save is enabled, triggers save then queues the node switch; otherwise shows the modal.
   */
  const selectFolderNode = () => {
    if (
      selectedNodeFieldsHaveChanged &&
      (selectedNode as FolderNode)?.folderId !== folderNode.folderId
    ) {
      if (getStoredSaveMode() === 'auto') {
        setSaveWorkItem(true);
        setAutoSavingNextNode(folderNode);
      } else {
        showUnsavedChangesModal(folderNode);
      }
    } else {
      setEditingDocumentNameCells(new Set());
      setSelectedNode(folderNode);
    }
  };

  /**
   * Handles document node selection with unsaved changes guard (field changes or document content changes).
   *
   * @param document Document node to select
   */
  const selectDocumentNode = (document: DocumentNode) => {
    if (
      (selectedNodeFieldsHaveChanged ||
        (selectedDocumentContentHasChanged &&
          selectedDocumentId !== document.documentId)) &&
      (selectedNode as DocumentNode)?.documentId !== document.documentId
    ) {
      if (getStoredSaveMode() === 'auto') {
        setSaveWorkItem(true);
        setAutoSavingNextNode(document);
      } else {
        showUnsavedChangesModal(document);
      }
    } else {
      setFolderNodeIsEditing(false);
      setSelectedNode(document);
    }
  };

  const [folderNodeIsEditing, setFolderNodeIsEditing] = useState(false);
  const folderNameInputRef = useRef<HTMLInputElement | null>(null);

  /**
   * Toggles inline editing for the folder node name, or commits the rename via API if already editing.
   */
  const renameFolderNode = () => {
    if (
      !selectedNodeFieldsHaveChanged ||
      (selectedNode as FolderNode)?.folderId === folderNode.folderId
    ) {
      if (folderNodeIsEditing) {
        const newValue = folderNameInputRef.current?.value as string;
        if (!newValue) {
          toast.error(
            t('functions.rename_folder_node.errors.missing_work_item_name')
          );
          return;
        }

        if (newValue === folderNode.folderName) {
          setFolderNodeIsEditing(false);
          return;
        }

        // Update folder node with new name
        renameWorkItem(folderNode.folderId, newValue)
          .then(() => {
            updateFolderName(newValue);
            setFolderNodeIsEditing(false);
            toast.success(t('functions.rename_folder_node.success'));
          })
          .catch(() => {
            toast.error(
              t('functions.rename_folder_node.errors.failed_to_rename')
            );
          });
      } else {
        setFolderNodeIsEditing(true);
      }
    }
  };

  // Keeps track of individual cell values (state necessary for tracking changes when updating values)
  // For the purposes of FolderTree, this is just the values of the "name" column for documents
  const [documentNameCellValues, setDocumentNameCellValues] = useState<
    Map<string, string>
  >(new Map());
  // Function rebuilds cellValues state map
  const updateDocumentNameCellValues = (sourceData: DocumentNode[]) => {
    const newCellValues = new Map<string, string>();

    sourceData.forEach((row, index) => {
      Object.keys(row).forEach((columnKey) => {
        const cellId = `${index}_${columnKey}`;
        newCellValues.set(cellId, String(row[columnKey as keyof DocumentNode]));
      });
    });

    setDocumentNameCellValues(newCellValues);
  };
  // Populate cellValues when documents change
  useEffect(() => {
    updateDocumentNameCellValues(documentNodes);
  }, [documentNodes]);

  // Keeps track of the cells (document names) being edited
  const [editingDocumentNameCells, setEditingDocumentNameCells] = useState<
    Set<string>
  >(new Set());
  // Toggles the editing state for a document name cell
  // Ensures only one cell is editable at a time
  const updateDocumentNameEditingStates = (
    cellId: string,
    isEditing: boolean
  ) => {
    const newEditingCells = new Set<string>();

    if (isEditing) {
      // Get all the cell IDs for the current row
      const rowCells = Array.from(editingDocumentNameCells).filter(
        (_cellId) => {
          if (_cellId.split('_')[0] === cellId.split('_')[0]) {
            return _cellId;
          }
        }
      );

      // Remove editing state for all cells in row
      rowCells.forEach((_cellId) => {
        newEditingCells.delete(_cellId as string);
      });
    } else {
      newEditingCells.add(cellId);
    }

    setEditingDocumentNameCells(newEditingCells);
  };

  /**
   * Toggles inline editing for a document node name, or commits the rename via API if already editing.
   * Appends the original file extension back to the new name before sending to the API.
   *
   * @param document Document node being renamed
   * @param cell Cell containing document node being renamed
   * @param cellIsEditing Whether the cell is being edited
   * @param extension File extension of document being renamed
   */
  const renameDocumentNode = (
    document: DocumentNode,
    cell: Cell<DocumentNode, unknown>,
    cellIsEditing: boolean,
    extension: string
  ) => {
    if (
      !selectedNodeFieldsHaveChanged ||
      (selectedNode as DocumentNode)?.documentId === document.documentId
    ) {
      if (cellIsEditing && updateDocumentNames) {
        const newValue = documentNameInputRefs.current.get(cell.id)
          ?.value as string;
        if (!newValue) {
          toast.error(
            t('functions.rename_document_node.errors.missing_document_name')
          );
          return;
        }

        renameDocument(
          process?.id as number,
          document.documentId,
          newValue + extension
        )
          .then(() => {
            // Update documents, replacing the documentName where appropriate
            updateDocumentNames(
              documentNodes.map((_document) => {
                if (_document.documentId === document.documentId) {
                  return {
                    ..._document,
                    documentName: newValue + extension,
                  };
                }
                return _document;
              })
            );
            toast.success(t('functions.rename_document_node.success'));
          })
          .catch(() => {
            toast.error(
              t('functions.rename_document_node.errors.failed_to_rename')
            );
          });
      }

      updateDocumentNameEditingStates(cell.id, cellIsEditing);
    }
  };

  // Reset all name editing states after "Discard Changes" clicked in "Unsaved Changes" modal
  useEffect(() => {
    if (!unsavedChangesModalShown) {
      setFolderNodeIsEditing(false);
      setEditingDocumentNameCells(new Set());
    }
  }, [unsavedChangesModalShown]);

  // Dynamic refs for inputs used when editing document names
  const documentNameInputRefs = useRef<Map<string, HTMLInputElement>>(
    new Map()
  );

  // After auto-save completes (saveWorkItem resets to false), switch to the queued next node
  useEffect(() => {
    if (!saveWorkItem && autoSavingNextNode) {
      if ((autoSavingNextNode as FolderNode)?.folderId) {
        setEditingDocumentNameCells(new Set());
        setSelectedNode(autoSavingNextNode as FolderNode);
      } else {
        setFolderNodeIsEditing(false);
        setSelectedNode(autoSavingNextNode as DocumentNode);
      }
      setAutoSavingNextNode(undefined);
    }
  }, [saveWorkItem]);

  // Configure TanStack Table for paginated document listing with single-row selection
  const table = useReactTable({
    data: documentNodes,
    columns: [
      {
        accessorKey: 'documentName',
      },
    ],
    state: { pagination, rowSelection: nodeSelection },
    getCoreRowModel: getCoreRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onPaginationChange: setPagination,
    onRowSelectionChange: setNodeSelection,
    autoResetPageIndex: false,
  });

  const totalPages = table.getPageCount();

  const { registerAnchor, maxHeight } = useNamedAnchorRemainingHeight('tree');

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

  // Call custom hook for FolderTree hotkeys
  useFolderTreeHotkeys({
    documentNodes,
    selectedDocumentId,
    documentIsLoading,
    selectDocumentNode,
    os,
  });

  const { t } = useTranslation(undefined, {
    keyPrefix:
      'components.processing.details_folder_panel.sections.folder_section.components.folder_tree',
  });

  return (
    <>
      {/* Pagination controls */}
      <div className={'d-flex justify-content-end mb-2'}>
        <CustomPaginationControls
          table={table}
          totalPages={totalPages}
          isMobile={true}
        />
      </div>

      <div ref={registerAnchor('tree')} />
      <div
        className={'overflow-y-auto'}
        style={{ maxHeight: isMobile ? undefined : maxHeight }}
      >
        <ListGroup>
          {/* Top-level folder item */}
          <ListGroup.Item
            as={'div'}
            role={'button'}
            key={folderNode.folderId}
            action
            className={'d-flex justify-content-between align-items-center'}
            active={
              (selectedNode as FolderNode)?.folderId === folderNode.folderId
            }
            onClick={selectFolderNode}
            disabled={autoSavingNextNode !== undefined}
          >
            <div className={'d-flex gap-2 align-items-center'}>
              <span className={'d-flex align-items-center'}>
                <Icon
                  iconName={'FolderFill'}
                  size={20}
                  className={'text-warning'}
                />
              </span>
              {folderNodeIsEditing ? (
                <Form.Control
                  ref={folderNameInputRef}
                  placeholder={folderNode.folderName}
                  defaultValue={folderNode.folderName}
                  value={
                    folderNodeIsEditing ? undefined : folderNode.folderName
                  }
                  size={'sm'}
                  style={{ maxWidth: '25ch' }}
                  maxLength={Number(MAX_WORK_ITEM_NAME_LENGTH)}
                />
              ) : (
                <div
                  className={
                    'd-flex justify-content-between align-items-center gap-2'
                  }
                >
                  <span className={'text-break'}>{folderNode.folderName}</span>
                  <Badge
                    bg={
                      (selectedNode as FolderNode)?.folderId ===
                      folderNode.folderId
                        ? 'light'
                        : 'primary'
                    }
                    className={
                      (selectedNode as FolderNode)?.folderId ===
                      folderNode.folderId
                        ? 'text-primary'
                        : 'text-light'
                    }
                  >
                    {documentNodes.length}
                  </Badge>
                </div>
              )}
            </div>
            {hasPermission(AtAllowRights.CreateWorkitem) && (
              <OverlayTrigger
                placement={'top'}
                overlay={
                  <Tooltip className={'position-fixed'}>
                    {t('buttons.rename_work_item.tooltip')}
                  </Tooltip>
                }
              >
                <Button
                  variant={
                    (selectedNode as FolderNode)?.folderId ===
                    folderNode.folderId
                      ? 'outline-light'
                      : 'outline-primary'
                  }
                  size={'sm'}
                  className={'ms-2'}
                  onClick={renameFolderNode}
                >
                  <div className={'d-flex align-items-center'}>
                    <Icon
                      iconName={folderNodeIsEditing ? 'CheckLg' : 'Pencil'}
                      size={16}
                    />
                  </div>
                </Button>
              </OverlayTrigger>
            )}
          </ListGroup.Item>

          {/* Nested document items */}
          {table.getRowModel().rows.map((row) => {
            const document = row.original;
            const icon = getDocumentIcon(document);

            return (
              <ListGroup.Item
                as={'div'}
                role={'button'}
                key={document.documentId}
                action
                className={`d-flex justify-content-between align-items-center ${
                  selectedDocumentId === document.documentId &&
                  (selectedNode as FolderNode)?.folderId === folderNode.folderId
                    ? 'semi-primary'
                    : ''
                }`}
                active={
                  (selectedNode as DocumentNode)?.documentId ===
                  document.documentId
                }
                onClick={() => selectDocumentNode(document)}
                disabled={
                  (documentIsLoading || autoSavingNextNode !== undefined) &&
                  document.documentId !== selectedDocumentId
                }
              >
                {row.getVisibleCells().map((cell) => {
                  const cellIsEditing = editingDocumentNameCells.has(cell.id);

                  // Format value to display
                  const cellValue = String(documentNameCellValues.get(cell.id));
                  const cellValueNoExt = cellValue.substring(
                    0,
                    cellValue.lastIndexOf('.')
                  );
                  const extension = cellValue.substring(
                    cellValue.lastIndexOf('.')
                  );

                  return (
                    <Fragment key={cell.id}>
                      <div className={'ms-2 d-flex gap-2 align-items-center'}>
                        <span className={'d-flex align-items-center'}>
                          <Icon
                            iconName={icon.iconName}
                            size={20}
                            className={icon.colorClass}
                          />
                        </span>
                        {updateDocumentNames && cellIsEditing ? (
                          <Form.Control
                            ref={(element) => {
                              if (element) {
                                documentNameInputRefs.current.set(
                                  cell.id,
                                  element
                                );
                              } else {
                                documentNameInputRefs.current.delete(cell.id);
                              }
                            }}
                            placeholder={cellValueNoExt}
                            defaultValue={cellValueNoExt}
                            value={cellIsEditing ? undefined : cellValueNoExt}
                            size={'sm'}
                            style={{ maxWidth: '25ch' }}
                            maxLength={
                              Number(MAX_FILE_NAME_LENGTH) - extension.length
                            }
                          />
                        ) : (
                          <div
                            className={
                              'd-flex justify-content-between align-items-center gap-2'
                            }
                          >
                            <span className={'text-break'}>
                              <abbr
                                title={`${cellValue} (${document.documentId.toString()})`}
                                style={{ cursor: 'pointer' }}
                              >
                                {cellValue}
                              </abbr>
                            </span>
                            <Badge
                              bg={
                                (selectedNode as DocumentNode)?.documentId ===
                                document.documentId
                                  ? 'light'
                                  : 'primary'
                              }
                              className={`${
                                (selectedNode as DocumentNode)?.documentId ===
                                document.documentId
                                  ? 'text-primary'
                                  : 'text-light'
                              } ${selectedDocumentId === document.documentId && pdfViewerRef.current?.pageCount ? '' : 'd-none'}`}
                            >
                              {pdfViewerRef.current?.pageCount}
                            </Badge>
                          </div>
                        )}
                      </div>
                      {updateDocumentNames &&
                        hasPermission(AtAllowRights.EditDocument) && (
                          <OverlayTrigger
                            placement={'top'}
                            overlay={
                              <Tooltip className={'position-fixed'}>
                                {t('buttons.rename_document.tooltip')}
                              </Tooltip>
                            }
                          >
                            <Button
                              variant={
                                (selectedNode as DocumentNode)?.documentId ===
                                document.documentId
                                  ? 'outline-light'
                                  : 'outline-primary'
                              }
                              size={'sm'}
                              className={'ms-2'}
                              onClick={() =>
                                renameDocumentNode(
                                  document,
                                  cell,
                                  cellIsEditing,
                                  extension
                                )
                              }
                            >
                              <div className={'d-flex align-items-center'}>
                                <Icon
                                  iconName={
                                    cellIsEditing ? 'CheckLg' : 'Pencil'
                                  }
                                  size={16}
                                />
                              </div>
                            </Button>
                          </OverlayTrigger>
                        )}
                    </Fragment>
                  );
                })}
              </ListGroup.Item>
            );
          })}
        </ListGroup>
      </div>
    </>
  );
};
