import {
  createContext,
  useContext,
  useCallback,
  useRef,
  useMemo,
  useState,
  type ReactNode,
} from 'react';
import type { EditorCommand } from '../types';

interface UndoRedoContextValue {
  /** Execute a command and push it onto the undo stack. */
  execute(command: EditorCommand): void;
  /** Undo the last command. */
  undo(): void;
  /** Redo the last undone command. */
  redo(): void;
  /** Whether there are commands to undo. */
  readonly canUndo: boolean;
  /** Whether there are commands to redo. */
  readonly canRedo: boolean;
}

const UndoRedoContext = createContext<UndoRedoContextValue | null>(null);

const MAX_UNDO_STACK = 100;

interface UndoRedoProviderProps {
  readonly children: ReactNode;
  /** Called whenever the stack changes so the parent can re-render. */
  readonly onStackChange?: () => void;
}

export function UndoRedoProvider({ children, onStackChange }: UndoRedoProviderProps) {
  const undoStackRef = useRef<EditorCommand[]>([]);
  const redoStackRef = useRef<EditorCommand[]>([]);
  // State counter to trigger re-renders when stack changes
  const [version, setVersion] = useState(0);

  const notifyChange = useCallback(() => {
    setVersion((v) => v + 1);
    onStackChange?.();
  }, [onStackChange]);

  const execute = useCallback(
    (command: EditorCommand) => {
      command.execute();
      undoStackRef.current = [...undoStackRef.current, command].slice(
        -MAX_UNDO_STACK,
      );
      redoStackRef.current = [];
      notifyChange();
    },
    [notifyChange],
  );

  const undo = useCallback(() => {
    const stack = undoStackRef.current;
    if (stack.length === 0) return;

    const command = stack[stack.length - 1];
    command.undo();
    undoStackRef.current = stack.slice(0, -1);
    redoStackRef.current = [...redoStackRef.current, command];
    notifyChange();
  }, [notifyChange]);

  const redo = useCallback(() => {
    const stack = redoStackRef.current;
    if (stack.length === 0) return;

    const command = stack[stack.length - 1];
    command.execute();
    redoStackRef.current = stack.slice(0, -1);
    undoStackRef.current = [...undoStackRef.current, command];
    notifyChange();
  }, [notifyChange]);

  const canUndo = undoStackRef.current.length > 0;
  const canRedo = redoStackRef.current.length > 0;

  const value = useMemo<UndoRedoContextValue>(
    () => ({
      execute,
      undo,
      redo,
      canUndo,
      canRedo,
    }),
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [execute, undo, redo, version],
  );

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

export function useUndoRedo(): UndoRedoContextValue {
  const ctx = useContext(UndoRedoContext);
  if (!ctx) {
    throw new Error('useUndoRedo must be used within an UndoRedoProvider');
  }
  return ctx;
}
