import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';

import {
  type LibrariesTreeItem,
  type SidebarContextValue,
  type SidebarProviderProps,
  type SortedItem,
} from '@composed-components/LibrariesSidebar/types';
import { toLibrariesTreeItem } from '@composed-components/LibrariesSidebar/utils';

const EMPTY_ITEMS: LibrariesTreeItem[] = [];

const SidebarContext = createContext<SidebarContextValue | undefined>(
  undefined,
);

/**
 * Access the sidebar context. Throws if used outside {@link SidebarProvider},
 * so consumers never have to handle an `undefined` value.
 */
export const useSidebarContext = () => {
  const context = useContext(SidebarContext);
  if (!context) {
    throw new Error('useSidebarContext must be used within SidebarProvider');
  }
  return context;
};

/**
 * Single owner of all sidebar state, exposed to every row/header through one
 * memoized context value. Responsibilities:
 *
 * - **Item index** — builds `itemsById` (O(1) lookup via `getTreeItem`) and
 *   `childrenByParent` from the flat `items` prop, both memoized on `items`.
 * - **Sort mode** — `isSortMode` toggle and `showSortButton` (on when `onSort`
 *   is provided).
 * - **Pending order** — while sorting, drag results are staged per parent in
 *   `pendingSortByParent`; `getOrderedItems` returns the pending order when set,
 *   else the natural order. On Save the order is emitted via `onSort` and kept
 *   applied until the refetched `items` reflect it (an effect clears the staged
 *   order whenever `items` change outside sort mode).
 * - **Row expansion** — tracked in a `Set` ref (`expandedIds`) so toggling a
 *   row never re-renders the tree and the open/closed state survives remounts.
 * - **Consumer callbacks** — forwards `onItemClick` / `onAddMouseEnter`.
 *
 * @param model - Normalized sidebar nodes from the consumer adapter layer.
 * @param capabilities - Presentation flags (create, sort, status badge).
 * @param onSort - Called on Save with the reordered items grouped by parent.
 * @param onItemClick - Called when a row is clicked (outside sort mode).
 * @param onAddMouseEnter - Called when hovering an add button (e.g. to prefetch).
 */
export const SidebarProvider = ({
  model,
  capabilities,
  children,
  onSort,
  onItemClick,
  onAddMouseEnter,
}: SidebarProviderProps) => {
  const items = useMemo(
    () => model.nodes.map(node => toLibrariesTreeItem(node, capabilities)),
    [model.nodes, capabilities],
  );
  const [isSortMode, setIsSortMode] = useState(false);
  const [pendingSortByParent, setPendingSortByParent] = useState(
    () => new Map<number | null, LibrariesTreeItem[]>(),
  );
  const expandedIdsRef = useRef(new Set<number>());

  const isSortModeRef = useRef(isSortMode);
  isSortModeRef.current = isSortMode;

  const nodesSignature = useMemo(
    () => model.nodes.map(node => `${node.id}:${node.position}`).join(','),
    [model.nodes],
  );
  const prevNodesSignatureRef = useRef(nodesSignature);

  useEffect(() => {
    if (prevNodesSignatureRef.current === nodesSignature) return;
    prevNodesSignatureRef.current = nodesSignature;
    if (!isSortModeRef.current) setPendingSortByParent(new Map());
  }, [nodesSignature]);

  const { itemsById, childrenByParent } = useMemo(() => {
    const byId = new Map<number, LibrariesTreeItem>();
    const byParent = new Map<number | null, LibrariesTreeItem[]>();

    for (const item of items) {
      byId.set(item.id, item);
      const key = item.parentId ?? null;
      const siblings = byParent.get(key);
      if (siblings) siblings.push(item);
      else byParent.set(key, [item]);
    }

    return { itemsById: byId, childrenByParent: byParent };
  }, [items]);

  const getTreeItem = useCallback(
    (id: number) => itemsById.get(id),
    [itemsById],
  );

  const getOrderedItems = useCallback(
    (parentId?: number): LibrariesTreeItem[] => {
      const key = parentId ?? null;
      const pending = pendingSortByParent.get(key);
      if (pending !== undefined) return pending;
      return childrenByParent.get(key) ?? EMPTY_ITEMS;
    },
    [pendingSortByParent, childrenByParent],
  );

  const setPendingSort = useCallback(
    (nextItems: LibrariesTreeItem[], parentId?: number) => {
      const key = parentId ?? null;
      setPendingSortByParent(prev => {
        const next = new Map(prev);
        next.set(key, nextItems);
        return next;
      });
    },
    [],
  );

  const setRowExpanded = useCallback((id: number, expanded: boolean) => {
    if (expanded) expandedIdsRef.current.add(id);
    else expandedIdsRef.current.delete(id);
  }, []);

  const handleRestoreSortableItems = useCallback(
    () => setPendingSortByParent(new Map()),
    [],
  );
  const handleToggleSortMode = useCallback(
    () => setIsSortMode(prev => !prev),
    [],
  );

  const handleSaveSort = useCallback(() => {
    if (onSort && pendingSortByParent.size > 0) {
      const newItems: SortedItem[] = Array.from(
        pendingSortByParent.entries(),
      ).map(([key, sortedItems]) => ({
        items: sortedItems,
        parentId: typeof key === 'number' ? key : null,
      }));

      onSort(newItems);
    }
    // Keep the pending order applied until the refetched `items` reflect it
    // (cleared by the effect above); only leave sort mode here.
    setIsSortMode(false);
  }, [onSort, pendingSortByParent]);

  const value = useMemo<SidebarContextValue>(
    () => ({
      getTreeItem,
      onItemClick,
      onAddMouseEnter,
      isSortMode,
      showSortButton: Boolean(onSort),
      getOrderedItems,
      setPendingSort,
      handleToggleSortMode,
      handleRestoreSortableItems,
      handleSaveSort,
      expandedIds: expandedIdsRef.current,
      setRowExpanded,
    }),
    [
      getTreeItem,
      onItemClick,
      onAddMouseEnter,
      isSortMode,
      onSort,
      getOrderedItems,
      setPendingSort,
      handleToggleSortMode,
      handleRestoreSortableItems,
      handleSaveSort,
      setRowExpanded,
    ],
  );

  return (
    <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>
  );
};
