import { useState, useMemo, Fragment, useEffect, useRef } from 'react';
import {
  Table,
  Dropdown,
  Form,
  Pagination,
  Collapse,
  InputGroup,
  Button,
  OverlayTrigger,
  Tooltip,
  Modal,
} from 'react-bootstrap';
import {
  useReactTable,
  getCoreRowModel,
  getSortedRowModel,
  getFilteredRowModel,
  SortingState,
  getPaginationRowModel,
  PaginationState,
} from '@tanstack/react-table';
import {
  ArrowDown,
  ArrowUp,
  CornerDownRight,
  Filter,
  FoldVertical,
  UnfoldVertical,
} from 'lucide-react';
import Fuse from 'fuse.js';
import * as linkify from 'linkifyjs';
import Linkify from 'linkify-react';
import {
  APIColumnMeta,
  TableData,
  TablePatch,
  TableProps,
  getTypeOfT,
  handleCellUpdate,
  renderExternalLink,
} from '../../utils/TableHelper';
import { useAuth } from '../../auth/AuthProvider';
import { getFormattedTeamName, getTeam, Team } from '../../api/Teams';
import { createNotification } from '../../api/Notifications';
import { getRequest, Request, RequestTableData } from '../../api/Requests';
import { useToast } from '../Toast/ToastContext';
import { useMediaQuery } from 'react-responsive';
import { breakpoints } from '../../utils/BootstrapHelper';

// DataTable component
// Requires type TableData (any API middleware type)
// IN: { columns : ColumnDef[], use default[Type]Column export from API middleware
//       data : TableData[], i.e. Order[], Part[], etc. (single records must be wrapped in arrays)
//       setData : React useState setter function reference for the data variable (i.e., setOrders)
//       padding : option to pad table with extra rows to match pagination size (maintain consistent height), default false
//       initialIdSort : option to sort the table data by 'ascending' or 'descending', default 'descending'
//       expandable : option to allow rows to be expanded when clicked (creates collapse element below each row), default false
//       collapseData : array of foreign key data, like all Requests when data is Order[] (single records must be wrapped in arrays)
//       collapseDataKey : object property name used to match foreign key references between data and collapseData
//       children : function that returns a ReactNode for each item in collapseData
//      }
// OUT: Full table component with Bootstrap styling, sorting, filtering, searching, and pagination
export function DataTable<T extends TableData, P extends TablePatch, R = any>({
  columns,
  data,
  setData,
  padding = false,
  initialIdSort = 'descending',
  expandable = false,
  collapseData = [],
  collapseDataKey,
  children = undefined,
}: TableProps<T, P, R>) {
  const { user } = useAuth();
  const { addToast } = useToast();

  const pageSize = 10;

  // Keeps track of individual cell values (state necessary for tracking changes when updating values)
  const [cellValues, setCellValues] = useState<Map<string, string>>(new Map());
  // Keeps track of the currently sorted column and direction
  const [sorting, setSorting] = useState<SortingState>([
    {
      // @ts-expect-error
      id: columns[0].accessorKey as string,
      // Initial sorting direction for "ID" column
      desc: initialIdSort === 'descending',
    },
  ]);
  // Keeps track of the currently selected filters for each column
  const [filters, setFilters] = useState<Record<keyof T, Set<any>>>(
    {} as Record<keyof T, Set<any>>
  );
  // Keeps track of the current pagination state
  const [pagination, setPagination] = useState<PaginationState>({
    pageIndex: 0,
    pageSize: pageSize,
  });
  // Keeps track of the global search query
  const [searchQuery, setSearchQuery] = useState<string>('');
  // Keeps track of the expanded table rows (and their associated collapse rows)
  const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
  const [shownCollapseRows, setShownCollapseRows] = useState<Set<string>>(
    new Set()
  );
  // Keeps track of the cells being edited
  const [editingCells, setEditingCells] = useState<Set<string>>(new Set());

  // Function rebuilds cellValues state map
  const updateCellValues = (sourceData: T[]) => {
    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 T]));
      });
    });

    setCellValues(newCellValues);
  };

  // Populate cellValues and reset filters when data changes
  useEffect(() => {
    updateCellValues(data);

    setFilters((prevFilters) => {
      const newFilters = {} as Record<keyof T, Set<any>>;

      // Get the new available column keys
      const availableKeys = new Set(Object.keys(data[0] || {}));

      Object.keys(prevFilters).forEach((key) => {
        const typedKey = key as keyof T;

        // Only retain filters for columns that still exist
        if (availableKeys.has(key)) {
          const availableValues = new Set(data.map((row) => row[typedKey]));

          // Retain only filter values that exist in the new data
          newFilters[typedKey] = new Set(
            [...prevFilters[typedKey]].filter((value) =>
              availableValues.has(value)
            )
          );

          // Remove filter if no values remain
          if (newFilters[typedKey].size === 0) {
            delete newFilters[typedKey];
          }
        }
      });

      return newFilters;
    });
  }, [data]);

  // Compute the sets of unique values for each column for filtering
  // Recalculates on change to data (which may never happen)
  const uniqueValues = useMemo(() => {
    // Record<[key in TableData allowed types], Set<any type of object data>>
    const values: Record<keyof T, Set<any>> = {} as Record<keyof T, Set<any>>;
    data.forEach((row) => {
      Object.keys(row).forEach((column) => {
        const key = column as keyof T;
        if (!values[key]) {
          values[key] = new Set();
        }
        values[key].add(row[key]);
      });
    });
    return values;
  }, [data]);

  // Function to filter and sort the data based on the current filters and sorting state
  const refreshFilteredData = (
    data: T[],
    filters: Record<keyof T, Set<any>>,
    sorting: SortingState
  ) => {
    // Apply filters
    const filteredData = data.filter((row) =>
      // For each filter (filtering by column), ensure the value is in the allowed set
      (Object.entries(filters) as [keyof T, Set<any>][]).every(
        ([key, allowedValues]) => allowedValues.has(row[key])
      )
    );

    // Apply sorting to the filtered data
    const sortedData = [...filteredData].sort((a, b) => {
      for (const { id, desc } of sorting) {
        const aValue = a[id as keyof T];
        const bValue = b[id as keyof T];

        // Compare the values for sorting order
        if (aValue < bValue) {
          return desc ? 1 : -1;
        }
        if (aValue > bValue) {
          return desc ? -1 : 1;
        }
      }

      // If no sorting applied, maintain original order
      return 0;
    });

    return sortedData;
  };

  // Update filteredData whenever sorting or filters change
  const filteredData = useMemo(
    () => refreshFilteredData(data, filters, sorting),
    [data, filters, sorting]
  );

  // Toggle a filter's selected state when a checkbox is toggled in the dropdown
  const toggleFilter = (columnId: keyof T, value: any) => {
    setFilters((prev) => {
      const newFilters = { ...prev };
      if (!newFilters[columnId]) {
        newFilters[columnId] = new Set(uniqueValues[columnId]);
      }

      // Add or remove the value from the set of allowed values for this column
      if (newFilters[columnId].has(value)) {
        newFilters[columnId].delete(value);
      } else {
        newFilters[columnId].add(value);
      }

      // Remove empty filters
      if (newFilters[columnId].size === 0) {
        delete newFilters[columnId];
      }

      return { ...newFilters };
    });
  };

  // Toggle filter multi-toggle button state
  // Can be "checked" (Select All), "indeterminate" (only some selected), or "unchecked" (Unselect All)
  const getFilterMultiToggleState = (columnId: keyof T) => {
    // No filters exist or all filters selected
    if (
      !filters[columnId] ||
      filters[columnId].size === uniqueValues[columnId]?.size
    ) {
      return 'checked';
    }

    // Filters exists but no items means none selected
    if (!filters[columnId].size) {
      return 'unchecked';
    }

    return 'indeterminate';
  };

  // Setup Fuse.js for searching
  const fuse = useMemo(() => {
    return new Fuse(filteredData, {
      // Search across all columns
      // @ts-expect-error
      keys: columns.map((column) => column.accessorKey as string),
      // Strict matching
      threshold: 0,
      ignoreLocation: true,
      includeScore: true,
    });
  }, [filteredData, columns]);

  // Apply global search using Fuse.js
  const searchedData = useMemo(() => {
    let searchedData;
    if (searchQuery) {
      // Find data matching search query
      searchedData = fuse.search(searchQuery).map((result) => result.item);
    } else {
      searchedData = filteredData;
    }

    // Reset row expansion states
    setExpandedRows(new Set());
    setShownCollapseRows(new Set());

    updateCellValues(searchedData);

    return searchedData;
  }, [filteredData, searchQuery, fuse]);

  // Function updates the states of expanded rows and shown associated collapse rows
  const updateExpansionStates = (
    rowId: string,
    rowIsExpanded: boolean,
    isCollapseRow: boolean
  ) => {
    const newExpandedRows = new Set(expandedRows);
    const newShownCollapseRows = new Set(shownCollapseRows);

    if (!isCollapseRow) {
      if (rowIsExpanded) {
        newExpandedRows.delete(rowId);
      } else {
        newExpandedRows.add(rowId);
        newShownCollapseRows.add(rowId);
      }

      setExpandedRows(newExpandedRows);
    } else {
      newShownCollapseRows.delete(rowId);
    }

    setShownCollapseRows(newShownCollapseRows);
  };

  // Function updates the states of cells being edited
  const updateEditingStates = (cellId: string, isEditing: boolean) => {
    const newEditingCells = new Set(editingCells);

    if (isEditing) {
      // Get all of the cell IDs for the current row
      const rowCells = Array.from(editingCells).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);
    }

    setEditingCells(newEditingCells);
  };

  // Setup TanStack Table
  // This is where the magic happens
  const table = useReactTable({
    data: searchedData,
    columns,
    state: { sorting, pagination },
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
    getFilteredRowModel: getFilteredRowModel(),
    getPaginationRowModel: getPaginationRowModel(),
    onSortingChange: setSorting,
    onPaginationChange: setPagination,
    // Enforce sorting by at least one column
    enableSortingRemoval: false,
    autoResetPageIndex: false,
  });

  const totalPages = table.getPageCount();
  const isEmpty = table.getRowModel().rows.length === 0;
  const isMobile = useMediaQuery({ maxWidth: breakpoints.md - 1 });

  // State variables and functions for "Send Additional Notification" modal
  const [
    sendAdditionalNotificationModalShown,
    setSendAdditionalNotificationModalShown,
  ] = useState(false);
  const showSendAdditionalNotificationModal = async (request_id: number) => {
    associatedRequest.current = await getRequest(request_id);
    teamToNotify.current = await getTeam(
      associatedRequest.current.team_id as number
    );

    setSendAdditionalNotificationModalShown(true);
  };
  const hideSendAdditionalNotificationModal = () =>
    setSendAdditionalNotificationModalShown(false);
  const teamToNotify = useRef<Team>();
  const associatedRequest = useRef<Request>();
  const additionalNotificationMessage = useRef<HTMLTextAreaElement | null>(
    null
  );
  // Function sends an additional notification to a team
  const sendRequestStatusNotification = () => {
    createNotification({
      request_id: associatedRequest.current?.request_id,
      team_id: teamToNotify.current?.team_id as number,
      generated_message: `Message from ${user?.first_name} ${user?.last_name} about request #${associatedRequest.current?.request_id}'s status:`,
      free_text: additionalNotificationMessage.current?.value,
      for_instructor: false,
    });

    addToast('Request status notification sent.', 'success');

    hideSendAdditionalNotificationModal();
  };

  // Return a beautiful Bootstrap table
  // There's a ton of TanStack Table API usage here and there's no sense in me trying to explain it all
  // Become familiar with: https://tanstack.com/table/latest/docs/introduction
  return (
    <Fragment>
      {/* Send Additional Notification modal (used when instructors update request status) */}
      <Modal
        show={sendAdditionalNotificationModalShown}
        onHide={hideSendAdditionalNotificationModal}
      >
        <Form
          onSubmit={(event) => {
            event.preventDefault();

            if (event.currentTarget.checkValidity()) {
              sendRequestStatusNotification();
            } else {
              event.currentTarget.classList.add('was-validated');
            }
          }}
          noValidate
        >
          <Modal.Header closeButton>
            <Modal.Title>Send Additional Notification</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <p className='mb-2'>
              If you would like to send an additional notification to{' '}
              <b>
                {teamToNotify.current &&
                  getFormattedTeamName(teamToNotify.current)}
              </b>{' '}
              to elaborate on your change in request status, you can do so
              below. If not, you can close this modal.
            </p>
            {associatedRequest.current?.status === 'delivered' &&
              teamToNotify.current?.locker_num && (
                <p className='mb-2'>
                  For reference,{' '}
                  <b>
                    {teamToNotify.current &&
                      getFormattedTeamName(teamToNotify.current)}
                  </b>{' '}
                  is assigned locker <b>#{teamToNotify.current.locker_num}</b>.
                </p>
              )}
            <div className='mb-2'>
              <Form.Control
                ref={additionalNotificationMessage}
                as='textarea'
                rows={3}
                placeholder='Enter notification message...'
                required
              />
              <Form.Control.Feedback type='invalid'>
                Please provide a message.
              </Form.Control.Feedback>
            </div>
          </Modal.Body>
          <Modal.Footer>
            <Button
              variant='secondary'
              onClick={hideSendAdditionalNotificationModal}
            >
              Cancel
            </Button>
            <Button type='submit' variant='primary'>
              Send Notification
            </Button>
          </Modal.Footer>
        </Form>
      </Modal>

      {/* Search bar and pagination controls */}
      <div className='d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 gap-md-3 mb-3'>
        <Form.Control
          type='text'
          placeholder='Search...'
          className='flex-grow-1'
          value={searchQuery}
          onChange={(event) => setSearchQuery(event.target.value)}
        ></Form.Control>

        <div className='d-flex justify-content-end'>
          <Pagination className={`mb-0 ${isMobile ? 'overflow-x-scroll' : ''}`}>
            <Pagination.First
              onClick={() => table.firstPage()}
              disabled={!table.getCanPreviousPage()}
            />
            <Pagination.Prev
              onClick={() => table.previousPage()}
              disabled={!table.getCanPreviousPage()}
            />

            {
              // Show all pages if three or fewer
              totalPages <= 3 ? (
                Array.from({ length: totalPages }).map((_, index) => (
                  <Pagination.Item
                    key={index}
                    active={index === table.getState().pagination.pageIndex}
                    onClick={() => table.setPageIndex(index)}
                  >
                    {index + 1}
                  </Pagination.Item>
                ))
              ) : (
                // Show first page, current page with neighbors, and last page with ellipsis
                <Fragment>
                  {/* Always show first page */}
                  <Pagination.Item
                    active={0 === table.getState().pagination.pageIndex}
                    onClick={() => table.setPageIndex(0)}
                  >
                    1
                  </Pagination.Item>

                  {/* Show ellipsis after first page if current page is not adjacent */}
                  {table.getState().pagination.pageIndex > 2 && (
                    <Pagination.Ellipsis disabled />
                  )}

                  {/* Show current page and one neighbor on each side if not first/last */}
                  {[
                    table.getState().pagination.pageIndex - 1,
                    table.getState().pagination.pageIndex,
                    table.getState().pagination.pageIndex + 1,
                  ]
                    .filter((index) => index > 0 && index < totalPages - 1)
                    .map((index) => (
                      <Pagination.Item
                        key={index}
                        active={index === table.getState().pagination.pageIndex}
                        onClick={() => table.setPageIndex(index)}
                      >
                        {index + 1}
                      </Pagination.Item>
                    ))}

                  {/* Show ellipsis before last page if current page is not adjacent */}
                  {table.getState().pagination.pageIndex < totalPages - 3 && (
                    <Pagination.Ellipsis disabled />
                  )}

                  {/* Always show last page */}
                  <Pagination.Item
                    active={
                      totalPages - 1 === table.getState().pagination.pageIndex
                    }
                    onClick={() => table.setPageIndex(totalPages - 1)}
                  >
                    {totalPages}
                  </Pagination.Item>
                </Fragment>
              )
            }

            <Pagination.Next
              onClick={() => table.nextPage()}
              disabled={!table.getCanNextPage()}
            />
            <Pagination.Last
              onClick={() => table.setPageIndex(totalPages - 1)}
              disabled={!table.getCanNextPage()}
            />
          </Pagination>
        </div>
      </div>

      {/* Table */}
      <Table striped bordered hover responsive className='mb-0'>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {
                // Show extra column for row expansion
                expandable && (
                  <th
                    className='align-middle'
                    style={{ whiteSpace: 'nowrap', minWidth: 'fit-content' }}
                  >
                    <OverlayTrigger
                      placement='top'
                      overlay={
                        <Tooltip>
                          Click on the <UnfoldVertical size={16} /> icon to
                          expand a row.
                        </Tooltip>
                      }
                    >
                      <span className='d-flex justify-content-center align-items-center'>
                        <CornerDownRight size={16} />
                      </span>
                    </OverlayTrigger>
                  </th>
                )
              }
              {headerGroup.headers.map((header) => {
                const column = header.column;
                const columnId = column.id as keyof T;

                const multiToggleState = getFilterMultiToggleState(columnId);

                return (
                  <th
                    key={header.id}
                    className='align-middle'
                    // Prevent column header from breaking to new lines to fit content, extend width instead
                    style={{ whiteSpace: 'nowrap', minWidth: 'fit-content' }}
                  >
                    <div className='d-flex justify-content-between align-items-center'>
                      <span
                        onClick={
                          isEmpty ? undefined : column.getToggleSortingHandler()
                        }
                        role={isEmpty ? undefined : 'button'} // pointer: cursor
                        className={
                          'align-middle ' +
                          (column.getCanFilter() ? 'me-3' : null)
                        }
                      >
                        {column.columnDef.header as string}{' '}
                        {
                          // Only show sort buttons if not explicitly denied in ColumnDef[]
                          column.getCanSort() && (
                            <span>
                              <ArrowUp
                                size={16}
                                className={
                                  column.getIsSorted() === 'asc'
                                    ? 'opacity-100'
                                    : 'opacity-50'
                                }
                              />
                              <ArrowDown
                                size={16}
                                className={
                                  column.getIsSorted() === 'desc'
                                    ? 'opacity-100'
                                    : 'opacity-50'
                                }
                              />
                            </span>
                          )
                        }
                      </span>
                      {
                        // Only show filter button if explicitly allowed in ColumnDef[]
                        column.getCanFilter() && (
                          <span className='float-end' role='button'>
                            <Dropdown align='end' autoClose='outside'>
                              {/* Show a recognizable filter icon */}
                              <Dropdown.Toggle
                                variant='outline-dark'
                                size='sm'
                                disabled={
                                  isEmpty && !(filters[columnId]?.size === 0)
                                }
                              >
                                <Filter size={16} />
                              </Dropdown.Toggle>
                              <Dropdown.Menu
                                renderOnMount
                                popperConfig={{ strategy: 'fixed' }}
                              >
                                <Dropdown.Item
                                  as='div'
                                  disabled={
                                    uniqueValues &&
                                    uniqueValues[columnId] &&
                                    uniqueValues[columnId].size === 1
                                  }
                                  onClick={() => {
                                    // If all filters are selected, unselect all
                                    if (multiToggleState === 'checked') {
                                      setFilters((prev) => ({
                                        ...prev,
                                        [columnId]: new Set(),
                                      }));
                                    } else {
                                      // Otherwise select all
                                      setFilters((prev) => ({
                                        ...prev,
                                        [columnId]: new Set(
                                          uniqueValues[columnId]
                                        ),
                                      }));
                                    }
                                  }}
                                >
                                  <Form.Check
                                    type='checkbox'
                                    label={
                                      multiToggleState === 'checked'
                                        ? 'Unselect All'
                                        : 'Select All'
                                    }
                                    // Indeterminate state configuration
                                    checked={multiToggleState === 'checked'}
                                    ref={(element) => {
                                      if (element) {
                                        element.indeterminate =
                                          multiToggleState === 'indeterminate';
                                      }
                                    }}
                                    readOnly
                                  />
                                </Dropdown.Item>
                                <Dropdown.Divider />
                                <div
                                  style={{
                                    maxHeight: '22.5rem',
                                    overflowY: 'scroll',
                                  }}
                                >
                                  {
                                    // For each unique value in the column, create a checkbox menu item
                                    Array.from(
                                      uniqueValues[columnId] || []
                                    ).map((value) => (
                                      <Dropdown.Item
                                        key={value}
                                        as='div'
                                        // Toggle filter when clicked
                                        onClick={() =>
                                          toggleFilter(columnId, value)
                                        }
                                      >
                                        <Form.Check
                                          type='checkbox'
                                          label={
                                            String(value) === 'null'
                                              ? 'N/A'
                                              : String(value)
                                          }
                                          // Automatically update checked state
                                          checked={
                                            filters[columnId]?.has(value) ??
                                            true
                                          }
                                          // Dropdown.Item element handles clicking
                                          readOnly
                                        />
                                      </Dropdown.Item>
                                    ))
                                  }
                                </div>
                              </Dropdown.Menu>
                            </Dropdown>
                          </span>
                        )
                      }
                    </div>
                  </th>
                );
              })}
            </tr>
          ))}
        </thead>
        <tbody>
          {!isEmpty ? (
            <Fragment>
              {
                // Return the row data (updates automatically)
                table.getRowModel().rows.map((row, index, array) => {
                  const rowData = row.original;
                  // Find the items in collapseData where foreign key references exist
                  const matchingCollapseRows = collapseDataKey
                    ? collapseData.filter(
                        (collapseRow) =>
                          // @ts-expect-error
                          collapseRow[collapseDataKey as keyof R] ===
                          rowData[collapseDataKey as keyof T]
                      )
                    : [];

                  // Find the number of cells in the current row that are expanded
                  const rowExpandedCells = row
                    .getVisibleCells()
                    .filter((cell) => editingCells.has(cell.id));

                  const rowCanExpand =
                    expandable &&
                    matchingCollapseRows.length &&
                    !rowExpandedCells.length;
                  const rowIsExpanded = expandedRows.has(row.id);
                  const showCollapseRow = shownCollapseRows.has(row.id);

                  // Functionality and styling changes if table is marked expandable
                  return (
                    <Fragment key={row.id}>
                      <tr
                        style={
                          expandable
                            ? {
                                borderBottomWidth:
                                  !rowIsExpanded && index !== array.length - 1
                                    ? 0
                                    : 1,
                              }
                            : undefined
                        }
                      >
                        {expandable && (
                          <td
                            className='align-middle'
                            onClick={
                              rowCanExpand
                                ? () =>
                                    updateExpansionStates(
                                      row.id,
                                      rowIsExpanded,
                                      false
                                    )
                                : undefined
                            }
                            role={rowCanExpand ? 'button' : undefined}
                          >
                            <span className='d-flex justify-content-center align-items-center'>
                              {rowIsExpanded ? (
                                <FoldVertical size={16} />
                              ) : (
                                <UnfoldVertical
                                  size={16}
                                  className={
                                    rowCanExpand ? undefined : 'opacity-50'
                                  }
                                />
                              )}
                            </span>
                          </td>
                        )}
                        {row.getVisibleCells().map((cell) => {
                          const cellIsEditing = editingCells.has(cell.id);

                          // Format value to display
                          const cellValue =
                            String(cellValues.get(cell.id)).length &&
                            String(cellValues.get(cell.id)) !== 'null'
                              ? String(cellValues.get(cell.id))
                              : 'N/A';

                          const column = cell.column;
                          const columnMeta = column.columnDef.meta as
                            | APIColumnMeta<T, P>
                            | undefined;

                          // Determine whether cell can be edited
                          // Includes complex logic for handling editing of requests by students
                          const canEditCell =
                            setData &&
                            columnMeta &&
                            columnMeta.allowedUserTypes?.includes(
                              user?.type as string
                            ) &&
                            (user?.type === 'student'
                              ? columnMeta.restrictedValues?.includes(cellValue)
                              : true) &&
                            columnMeta.element;
                          const values =
                            user?.type === 'student' || user?.type === 'ta'
                              ? columnMeta?.restrictedValues
                              : columnMeta?.values;

                          return (
                            <td key={cell.id} className='align-middle'>
                              {canEditCell ? (
                                <InputGroup className='flex-nowrap'>
                                  {
                                    // Select element for updatable cells with multiple options for value
                                    columnMeta.element === 'select' &&
                                    values ? (
                                      <Form.Select
                                        value={
                                          !cellIsEditing ? cellValue : undefined
                                        }
                                        disabled={!cellIsEditing}
                                        style={{ minWidth: '15ch' }}
                                      >
                                        {
                                          // Values can either be an array or dict
                                          (Array.isArray(values)
                                            ? values
                                            : Object.values(values)
                                          ).map((value) => (
                                            <option key={value}>{value}</option>
                                          ))
                                        }
                                      </Form.Select>
                                    ) : columnMeta.element === 'date' ? (
                                      <Form.Control
                                        type='date'
                                        disabled={!cellIsEditing}
                                        // Value must be output with 'en-CA' locale to work with date picker
                                        value={
                                          cellIsEditing
                                            ? undefined
                                            : new Date(
                                                cellValue
                                              ).toLocaleDateString('en-CA', {
                                                timeZone: 'UTC',
                                              })
                                        }
                                        style={{ minWidth: '15ch' }}
                                      />
                                    ) : (
                                      // Input element for all others
                                      <Form.Control
                                        disabled={!cellIsEditing}
                                        placeholder={cellValue}
                                        // Using undefined here throws an error in the browser, but it can be ignored
                                        value={
                                          cellIsEditing ? undefined : cellValue
                                        }
                                        style={{ minWidth: '15ch' }}
                                      />
                                    )
                                  }
                                  <Button
                                    variant='outline-primary'
                                    onClick={(event) => {
                                      if (rowIsExpanded) {
                                        updateExpansionStates(
                                          row.id,
                                          rowIsExpanded,
                                          false
                                        );
                                      }

                                      // prettier-ignore
                                      const newValue =
                                        // @ts-expect-error
                                        event.currentTarget.previousSibling?.value;

                                      if (cellIsEditing) {
                                        handleCellUpdate(
                                          columnMeta,
                                          rowData,
                                          column,
                                          newValue
                                        )
                                          .then((response) => {
                                            addToast(
                                              getTypeOfT(rowData) +
                                                ' ' +
                                                // @ts-expect-error
                                                column.columnDef.accessorKey +
                                                ' updated to ' +
                                                newValue +
                                                '.',
                                              'success'
                                            );

                                            // Update data using state update function
                                            setData(
                                              data.map((dataRow) => {
                                                // Merge changes in row data (handles cases where updating one cell value triggers another update)
                                                if (
                                                  response &&
                                                  dataRow === rowData
                                                ) {
                                                  Object.keys(dataRow).forEach(
                                                    (key) => {
                                                      const typedKey =
                                                        key as keyof T;
                                                      // Get the transformation function corresponding to the current key
                                                      const transform =
                                                        columns.find(
                                                          (column) =>
                                                            // @ts-expect-error
                                                            column.accessorKey ===
                                                            key
                                                        )?.meta?.transform;

                                                      // Transform value if possible
                                                      if (transform) {
                                                        dataRow[typedKey] =
                                                          transform(
                                                            response[typedKey]
                                                          ) as T[keyof T];
                                                      } else if (
                                                        typeof dataRow[
                                                          typedKey
                                                        ] ===
                                                          typeof response[
                                                            typedKey
                                                          ] &&
                                                        dataRow[typedKey] !==
                                                          response[typedKey]
                                                      ) {
                                                        dataRow[typedKey] =
                                                          response[typedKey];
                                                      } else if (
                                                        response[typedKey] ===
                                                        null
                                                      ) {
                                                        dataRow[typedKey] =
                                                          'N/A' as T[keyof T];
                                                      }
                                                    }
                                                  );
                                                }

                                                return dataRow;
                                              })
                                            );

                                            setCellValues(
                                              new Map(cellValues).set(
                                                cell.id,
                                                newValue
                                              )
                                            );

                                            // Determine whether an additional notification should be sent after change
                                            // Only applies to request status
                                            const shouldSendNotification =
                                              user?.type !== 'student' &&
                                              getTypeOfT(rowData) ===
                                                'Request' &&
                                              // @ts-expect-error
                                              column.columnDef.accessorKey ===
                                                'status' &&
                                              [
                                                'delivered',
                                                'delayed',
                                                'reordered',
                                                'on hand',
                                                'cancelled',
                                                'other',
                                              ].includes(newValue);
                                            if (shouldSendNotification) {
                                              showSendAdditionalNotificationModal(
                                                (rowData as RequestTableData)
                                                  .request_id as number
                                              );
                                            }
                                          })
                                          .catch(() => {
                                            addToast(
                                              'Failed to update ' +
                                                getTypeOfT(rowData) +
                                                ' ' +
                                                // @ts-expect-error
                                                column.columnDef.accessorKey +
                                                '.',
                                              'danger'
                                            );
                                          });
                                      }

                                      updateEditingStates(
                                        cell.id,
                                        cellIsEditing
                                      );
                                    }}
                                  >
                                    {cellIsEditing ? 'Update' : 'Edit'}
                                  </Button>
                                </InputGroup>
                              ) : (
                                <Fragment>
                                  {
                                    // This is used when displaying part data inside a Request table
                                    // Shows part description and URL separately
                                    linkify.find(cellValue).length &&
                                    cellValue.split('\u001E').length == 2 ? (
                                      <div>
                                        {cellValue
                                          .split('\u001E')[0]
                                          .substring(0, 50) +
                                          (cellValue.split('\u001E')[0].length >
                                          50
                                            ? '...'
                                            : '')}
                                        <Linkify
                                          options={{
                                            render: renderExternalLink,
                                            truncate: 25,
                                          }}
                                        >
                                          <div>
                                            {cellValue.split('\u001E')[1]}
                                          </div>
                                        </Linkify>
                                      </div>
                                    ) : (
                                      <div>{cellValue}</div>
                                    )
                                  }
                                </Fragment>
                              )}
                            </td>
                          );
                        })}
                      </tr>
                      {expandable && (
                        // Collapse row plus extra hidden row (to maintain proper striped table)
                        <Fragment>
                          <tr
                            className={
                              rowIsExpanded || showCollapseRow
                                ? undefined
                                : 'd-none'
                            }
                            id={'collapse-row-' + index}
                            style={{
                              borderTopWidth: 0,
                              borderBottomWidth: 0,
                              backgroundColor: 'transparent',
                            }}
                          >
                            <td
                              colSpan={columns.length + (expandable ? 1 : 0)}
                              className='p-0 w-100'
                              style={{ maxWidth: 0 }}
                            >
                              <Collapse
                                in={rowIsExpanded}
                                className='bg-white'
                                onExited={() =>
                                  updateExpansionStates(
                                    row.id,
                                    rowIsExpanded,
                                    true
                                  )
                                }
                              >
                                <div>
                                  <div className='p-3'>
                                    {children && children(matchingCollapseRows)}
                                  </div>
                                </div>
                              </Collapse>
                            </td>
                          </tr>
                          <tr className='d-none'></tr>
                        </Fragment>
                      )}
                    </Fragment>
                  );
                })
              }
              {
                // Add empty rows to maintain consistent table height
                padding &&
                  table.getRowModel().rows.length < pageSize &&
                  Array.from({
                    length: pageSize - table.getRowModel().rows.length,
                  }).map((_, index) => (
                    <tr key={`empty-row-${index}`}>
                      {table.getAllColumns().map((column) => (
                        <td key={column.id}>&nbsp;</td>
                      ))}
                      {expandable && <td>&nbsp;</td>}
                    </tr>
                  ))
              }
            </Fragment>
          ) : (
            <Fragment>
              {padding ? (
                // Add empty rows to maintain consistent table height
                Array.from({
                  length: pageSize,
                }).map((_, index) => (
                  <tr key={`empty-row-${index}`}>
                    {table.getAllColumns().map((column) => (
                      <td key={column.id}>&nbsp;</td>
                    ))}
                    {expandable && <td>&nbsp;</td>}
                  </tr>
                ))
              ) : (
                <tr>
                  <td
                    colSpan={columns.length + (expandable ? 1 : 0)}
                    className='text-center p-3'
                  >
                    No data to display.
                  </td>
                </tr>
              )}
            </Fragment>
          )}
        </tbody>
      </Table>
    </Fragment>
  );
}
