'use client';

import { type RefObject, useEffect, useRef } from 'react';

import { useChatContextOptional, type ComposerHandle } from '../context';
import { useStreamEndFocus, type Focusable } from './useStreamEndFocus';

export type { Focusable } from './useStreamEndFocus';

export interface UseAutoFocusOnStreamEndOptions {
  /** True while an assistant reply is streaming. The hook fires the
   *  focus() call on the true → false transition.
   *
   *  When omitted, the hook reads `isStreaming` from `useChatContext`.
   *  Pass it explicitly only if you're driving stream state from your
   *  own store (cmdop's Wails event bus, for example). */
  isStreaming?: boolean;
  /** Ref / handle to focus when the reply lands.
   *
   *  When omitted, the hook uses the composer handle registered in
   *  the chat context — the built-in `<Composer>` registers itself
   *  automatically, custom composers can opt in via
   *  `useRegisterComposer`. Pass `targetRef` only when you need to
   *  focus something other than the composer (e.g. an "approve"
   *  button, a quick-reply chip). */
  targetRef?: RefObject<Focusable | HTMLElement | null>;
  /** Opt-out. Default true. Pass false to disable without unmounting
   *  the hook (e.g. user preference). */
  enabled?: boolean;
  /** Delay the focus call by this many ms. Default 0 = next animation
   *  frame, which lets the streaming bubble's final commit settle
   *  before focus pulls scroll. Bump to 50-150ms for layouts that
   *  re-mount the composer after the final chunk. */
  delayMs?: number;
}

/**
 * Refocus the chat composer the moment the assistant reply finishes
 * streaming. Standard chat UX: the user types → sends → reads the
 * reply → starts typing again without reaching for the mouse.
 *
 * **You usually do NOT need to call this.** `<ChatProvider>` runs the
 * stream-end focus internally, so every chat — `ChatRoot`, a hand-rolled
 * `ChatProvider` + `Composer` layout, or a headless setup with a
 * registered composer — gets it for free. Disable it provider-wide with
 * `<ChatProvider autoFocusOnStreamEnd={false}>`.
 *
 * Call this hook directly only for advanced cases the provider can't
 * cover:
 *
 *   // focus something OTHER than the composer (an approve button):
 *   const ref = useRef<{ focus: () => void } | null>(null);
 *   useAutoFocusOnStreamEnd({ targetRef: ref });
 *
 *   // drive stream state from your own store:
 *   useAutoFocusOnStreamEnd({ isStreaming: myExternalStreaming });
 */
export function useAutoFocusOnStreamEnd(
  options: UseAutoFocusOnStreamEndOptions = {},
): void {
  const { isStreaming: isStreamingProp, targetRef, enabled = true, delayMs = 0 } = options;
  const ctx = useChatContextOptional();
  // Prefer the prop (caller knows best), fall back to context.
  const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;

  // Keep latest ctx-composer in a ref so target resolution always sees
  // the freshest registered handle.
  const composerHandleRef = useRef<ComposerHandle | null>(null);
  composerHandleRef.current = ctx?.composer ?? null;

  useStreamEndFocus({
    isStreaming,
    enabled,
    delayMs,
    // Resolve in priority order: explicit ref > registered composer.
    resolveTarget: () =>
      (targetRef?.current as Focusable | null) ?? composerHandleRef.current,
  });
}

/**
 * Helper for custom composers (anything that's NOT the built-in
 * `<Composer>`) to register their focus() with the chat context so
 * `useAutoFocusOnStreamEnd()` and other consumers work without
 * prop-drilling.
 *
 * Usage inside your custom composer:
 *
 *   const focus = useCallback(() => {
 *     myEditorRef.current?.commands.focus();
 *   }, []);
 *   useRegisterComposer(focus);
 *
 * No-op when called outside a `<ChatProvider>`.
 */
export function useRegisterComposer(handle: ComposerHandle): void {
  const ctx = useChatContextOptional();
  const register = ctx?.registerComposer;
  const focus = handle.focus;
  const moveCursorToEnd = handle.moveCursorToEnd;
  // Forward `getValue/setValue` too — voice dictation reads/writes the
  // draft through them, so dropping them silently broke dictation for
  // custom composers (e.g. the TipTap MarkdownEditor wrapper).
  const getValue = handle.getValue;
  const setValue = handle.setValue;
  useEffect(() => {
    if (!register) return;
    register({ focus, moveCursorToEnd, getValue, setValue });
    return () => register(null);
  }, [register, focus, moveCursorToEnd, getValue, setValue]);
}
