import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';
import type { Command } from './types.js';

interface RegistryState {
  commands: Map<string, Command>;
  register: (commands: Command[]) => () => void;
  unregister: (ids: string[]) => void;
  clear: () => void;
}

const RegistryContext = createContext<RegistryState | null>(null);

export interface CommandRegistryProviderProps {
  /** Optional seed of commands always available (app-level chrome). */
  initial?: Command[];
  children: ReactNode;
}

/**
 * Provides a shared command registry to descendants. Wrap your app once
 * (typically next to `<DashboardShell>`) and any nested component can
 * call `useRegisterCommands(...)` to contribute commands for as long as
 * it stays mounted.
 */
export function CommandRegistryProvider({
  initial = [],
  children,
}: CommandRegistryProviderProps) {
  const [commands, setCommands] = useState<Map<string, Command>>(() => {
    const m = new Map<string, Command>();
    for (const c of initial) m.set(c.id, c);
    return m;
  });

  const register = useCallback<RegistryState['register']>(next => {
    setCommands(prev => {
      const m = new Map(prev);
      for (const c of next) m.set(c.id, c);
      return m;
    });
    return () => {
      setCommands(prev => {
        const m = new Map(prev);
        for (const c of next) m.delete(c.id);
        return m;
      });
    };
  }, []);

  const unregister = useCallback<RegistryState['unregister']>(ids => {
    setCommands(prev => {
      const m = new Map(prev);
      for (const id of ids) m.delete(id);
      return m;
    });
  }, []);

  const clear = useCallback(() => setCommands(new Map()), []);

  const value = useMemo<RegistryState>(
    () => ({ commands, register, unregister, clear }),
    [commands, register, unregister, clear],
  );

  return <RegistryContext.Provider value={value}>{children}</RegistryContext.Provider>;
}

function useRegistry(): RegistryState {
  const ctx = useContext(RegistryContext);
  if (!ctx) {
    throw new Error(
      '@bytelyst/command-palette: useRegisterCommands / useCommands must be used within <CommandRegistryProvider>',
    );
  }
  return ctx;
}

/**
 * Register commands for the lifetime of the calling component. Returns a
 * stable function callers can ignore — registration unwinds automatically
 * on unmount via the returned effect cleanup.
 *
 * @example
 * ```tsx
 * useRegisterCommands([
 *   { id: 'new-task', label: 'New task', icon: '+', run: openNewTask },
 *   { id: 'goto-billing', label: 'Billing', mode: 'navigate', href: '/billing' },
 * ]);
 * ```
 */
export function useRegisterCommands(commands: Command[]): void {
  const { register } = useRegistry();
  // Stable ref to the most-recent commands list. We intentionally avoid
  // re-registering on every keystroke / state tick — callers usually
  // pass an inline array. The serialization key below keeps it cheap.
  const key = useMemo(
    () =>
      commands
        .map(c => `${c.id}|${c.label}|${c.mode ?? ''}|${c.section ?? ''}`)
        .join('\n'),
    [commands],
  );

  useEffect(() => {
    const off = register(commands);
    return off;
    // eslint-disable-next-line react-hooks/exhaustive-deps -- key is the canonical identity
  }, [key, register]);
}

/** Read the current registry snapshot. */
export function useCommands(): Command[] {
  const { commands } = useRegistry();
  return useMemo(() => Array.from(commands.values()), [commands]);
}

/** Imperative access (e.g. for tests or shell-level wiring). */
export function useCommandRegistry(): RegistryState {
  return useRegistry();
}
