import { useCallback, useReducer, useRef } from "react";

// A generic, reusable undo/redo container for a single piece of state.
//
// It keeps a past/present/future snapshot stack: `set` records the current value into the
// past and clears the redo stack; `undo`/`redo` walk the stack. Every value change (set,
// undo, redo) calls `onApply(value)` — the side effect that makes the change real (e.g.
// persist to the server). `reset` installs a fresh value and clears history WITHOUT calling
// onApply (use it when the underlying data is reloaded from the source of truth).
//
// Not playlist-specific: any component with an undoable value (sort order, a draft, a set
// of filters…) can use it. State lives in a ref so the callbacks are stable and never read
// a stale snapshot; a reducer-based force-render keeps the view in sync.
export interface Undoable<T> {
  value: T;
  set: (next: T) => void;
  reset: (value: T) => void;
  rebase: (map: (value: T) => T) => void;
  undo: () => void;
  redo: () => void;
  canUndo: boolean;
  canRedo: boolean;
}

interface History<T> {
  past: T[];
  present: T;
  future: T[];
}

export function useUndoable<T>(
  initial: T,
  opts?: { onApply?: (value: T) => void; limit?: number },
): Undoable<T> {
  const [, force] = useReducer((n: number) => n + 1, 0);
  const ref = useRef<History<T>>({ past: [], present: initial, future: [] });
  const onApplyRef = useRef(opts?.onApply);
  onApplyRef.current = opts?.onApply;
  const limit = opts?.limit ?? 100;

  const set = useCallback(
    (next: T) => {
      const s = ref.current;
      if (Object.is(next, s.present)) return;
      const past = [...s.past, s.present];
      if (past.length > limit) past.shift();
      ref.current = { past, present: next, future: [] };
      force();
      onApplyRef.current?.(next);
    },
    [limit],
  );

  const undo = useCallback(() => {
    const s = ref.current;
    if (!s.past.length) return;
    const prev = s.past[s.past.length - 1]!; // past is non-empty (guarded above)
    ref.current = {
      past: s.past.slice(0, -1),
      present: prev,
      future: [s.present, ...s.future],
    };
    force();
    onApplyRef.current?.(prev);
  }, []);

  const redo = useCallback(() => {
    const s = ref.current;
    if (!s.future.length) return;
    const next = s.future[0]!; // future is non-empty (guarded above)
    const rest = s.future.slice(1);
    ref.current = { past: [...s.past, s.present], present: next, future: rest };
    force();
    onApplyRef.current?.(next);
  }, []);

  const reset = useCallback((value: T) => {
    ref.current = { past: [], present: value, future: [] };
    force();
  }, []);

  // Apply the same transformation to EVERY snapshot — past, present and future — and keep the
  // history. For changes that are outside the undoable dimension but invalidate the snapshots:
  // removing a playlist item is not itself undoable, yet dropping it from each stored order keeps
  // the user's reorder history valid and usable instead of throwing it away (which is what a
  // `reset` does). No `onApply`: the caller is already persisting the change by its own route.
  const rebase = useCallback((map: (value: T) => T) => {
    const s = ref.current;
    ref.current = {
      past: s.past.map(map),
      present: map(s.present),
      future: s.future.map(map),
    };
    force();
  }, []);

  const h = ref.current;
  return {
    value: h.present,
    set,
    reset,
    rebase,
    undo,
    redo,
    canUndo: h.past.length > 0,
    canRedo: h.future.length > 0,
  };
}
