'use client';

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

import { getActiveComposer } from '@djangocfg/ui-tools/composer-registry';

import type { ChatConfig, ChatLabels, ChatMessage, ChatTransport } from '../types';
import { DEFAULT_LABELS } from '../types';
import type { BlockRegistry } from '../messages/blocks';

import { setBridgeSender } from '../../../lib/browser-bridge';
import { useChat, type UseChatReturn } from '../hooks/useChat';
import { useChatLayout, type UseChatLayoutReturn } from '../hooks/useChatLayout';
import { useChatAudio } from '../hooks/useChatAudio';
import { useStreamEndFocus } from '../hooks/useStreamEndFocus';
import { useAutoFocusOnMount } from '../hooks/useAutoFocusOnStreamEnd';
import type { ChatAudioConfig, UseChatAudioReturn } from '../core/audio/types';

/** Imperative handle a composer (built-in or custom) registers so
 *  other parts of the chat tree can drive it without prop-drilling a
 *  ref. `focus()` is the baseline; the rest is optional so non-textarea
 *  hosts can keep returning `{ focus }` only.
 *
 *  Implemented by:
 *   - built-in `<Composer>` — backed by `useChatComposer.textareaRef`.
 *   - `@djangocfg/ui-tools/markdown-editor` — backed by the TipTap
 *     editor (`editor.commands.focus('end')`).
 *  Consumed by `VoiceComposerSlot` for the focus / move-caret behaviour
 *  during live dictation.
 */
// `ComposerHandle` lives in `@djangocfg/ui-tools/composer-registry` —
// the cross-tool registry shared by chat (producer) and
// speech-recognition (consumer). Re-export here so existing call sites
// `import { ComposerHandle } from '@djangocfg/ui-tools/chat'` keep
// working unchanged.
export type { ComposerHandle } from '@djangocfg/ui-tools/composer-registry';

export interface ChatContextValue extends UseChatReturn {
  layout: UseChatLayoutReturn;
  config: ChatConfig;
  labels: ChatLabels;
  audio: UseChatAudioReturn;
  /** True iff the host wired at least one ``audio.sounds[event]`` URL.
   * Components like ``AudioToggle`` use this to auto-hide when there
   * is nothing to mute. */
  hasAudio: boolean;
  /** Registry of `kind` → renderer for `message.blocks`. `null` when the
   *  host wired none — `<MessageBubble>` then uses `BUILTIN_BLOCK_REGISTRY`. */
  blockRegistry: BlockRegistry | null;
}

const Ctx = createContext<ChatContextValue | null>(null);

export interface ChatProviderProps {
  transport: ChatTransport;
  config?: ChatConfig;
  /**
   * The session to resume on mount. Changing it AFTER mount performs an
   * in-place session switch (atomic `SESSION_SWITCH` + background history
   * revalidate) — the modern alternative to remounting the whole tree via
   * `key={sessionId}`. Pair with a fresh `initialMessages` for the new room
   * to make the swap paint instantly from the host's cache. The legacy
   * remount pattern remains fully supported.
   */
  initialSessionId?: string;
  /**
   * Warm transcript rendered instantly on mount while the resume-path
   * `loadHistory` revalidates in the background (see `useChat`). Pair with
   * `initialSessionId`: the host caches the last-seen transcript per room and
   * passes it here so returning to a conversation shows content immediately
   * instead of a cold refetch Spinner. The server read remains the source of
   * truth — this is a visual head-start, replaced by the loaded page.
   */
  initialMessages?: readonly ChatMessage[];
  autoCreateSession?: boolean;
  streaming?: boolean;
  /** Audio-trigger configuration. Off by default (no `sounds` map). */
  audio?: ChatAudioConfig;
  /** Enable verbose dev logging via consola. Defaults to `isDev`. */
  debug?: boolean;
  /** Registry of `kind` → renderer for `message.blocks`. Stored in
   *  context so nested `<ChatRoot>` / `<MessageBubble>` pick it up. */
  blockRegistry?: BlockRegistry;
  /**
   * Rewrite outgoing message content before it hits the transport.
   * History bubble keeps the original; useful for stripping rich-
   * display chips so the LLM sees plain text. See `useChat`.
   */
  onBeforeSend?: (content: string) => string | Promise<string>;
  /**
   * Contribute extra transport metadata, computed fresh per send.
   * Merged over the static metadata right before `transport.stream/send`.
   * Keeps `Chat` decoupled from any specific metadata source — the host
   * supplies the function (e.g. the page-context snapshot from
   * `usePageSnapshot().getChatMetadata`).
   */
  getDynamicMetadata?: () => Record<string, unknown> | undefined;
  /**
   * Re-focus the registered composer on the streaming → idle edge —
   * the standard chat UX (type → send → read → keep typing without
   * reaching for the mouse). Default `true`. Works for every usage
   * pattern (`ChatRoot`, hand-rolled `Composer` layout, headless) as
   * long as a composer is registered. Set `false` to opt out.
   */
  autoFocusOnStreamEnd?: boolean;
  /**
   * Focus the registered composer when the chat mounts — caret already in
   * the input the instant the chat appears. With the standard
   * `key={conversationId}` engine remount, this also focuses on every
   * chat-switch (the tree remounts → mount focus re-fires) without any
   * id-tracking effect. Default `true`. Set `false` to opt out (e.g. an
   * embedded widget that shouldn't steal focus on load).
   */
  autoFocusOnMount?: boolean;
  children?: ReactNode;
}

export function ChatProvider({
  transport,
  config = {},
  initialSessionId,
  initialMessages,
  autoCreateSession,
  streaming,
  audio,
  debug,
  blockRegistry,
  onBeforeSend,
  getDynamicMetadata,
  autoFocusOnStreamEnd = true,
  autoFocusOnMount = true,
  children,
}: ChatProviderProps) {
  const audioApi = useChatAudio(audio ?? {});

  // Keep latest audio API in a ref so the chat-callback closures stay
  // referentially stable (don't re-mount transport on every audio change).
  const audioRef = useRef(audioApi);
  audioRef.current = audioApi;

  const onMessageSent = useCallback(() => audioRef.current.play('messageSent'), []);
  const onMessageEnd = useCallback(() => audioRef.current.play('messageReceived'), []);
  const onStreamStart = useCallback(() => audioRef.current.play('streamStart'), []);
  const onError = useCallback(() => audioRef.current.play('error'), []);

  const chat = useChat({
    transport,
    initialSessionId,
    initialMessages,
    autoCreateSession,
    streaming,
    debug,
    metadata: {
      locale: config.prefs?.locale,
      slug: config.slug,
    },
    userPersona: config.user,
    onMessageSent,
    onMessageEnd,
    onStreamStart,
    onError,
    onBeforeSend,
    getDynamicMetadata,
  });
  const layout = useChatLayout({ defaultMode: 'embedded' });

  // Auto-unlock audio on the first user gesture inside the provider.
  const rootRef = useRef<HTMLDivElement | null>(null);
  useEffect(() => {
    if (audioApi.isUnlocked) return;
    const root = rootRef.current;
    if (!root) return;
    const handler = () => {
      audioApi.unlock();
    };
    root.addEventListener('pointerdown', handler, { once: true, capture: true });
    root.addEventListener('keydown', handler, { once: true, capture: true });
    return () => {
      root.removeEventListener('pointerdown', handler, { capture: true });
      root.removeEventListener('keydown', handler, { capture: true });
    };
  }, [audioApi]);

  // Publish `sendMessage` to the browser bridge so the dev-only
  // `window.__chatBridge.sendMessage(...)` can drive the chat from the
  // console. `setBridgeSender` no-ops outside dev; cleared on unmount.
  useEffect(() => {
    setBridgeSender(chat.sendMessage);
    return () => setBridgeSender(null);
  }, [chat.sendMessage]);

  const labels = useMemo<ChatLabels>(
    () => ({ ...DEFAULT_LABELS, ...(config.labels ?? {}) }),
    [config.labels],
  );

  // True when audio is wired up — drives the auto-injected mute toggle.
  // `audio={{}}` counts: `useChatAudio` falls back to the bundled
  // DEFAULT_CHAT_SOUNDS, so an empty config still has sounds to mute.
  // Only an absent `audio` prop or `silenced` means "nothing to mute".
  const hasAudio = useMemo<boolean>(() => {
    if (!audio) return false;
    if (audio.silenced) return false;
    const sounds = audio.sounds;
    if (sounds === undefined) return true;
    return Object.values(sounds).some(
      (v) => typeof v === 'string' && v.length > 0,
    );
  }, [audio]);

  // Re-focus the composer on the streaming → idle edge. Lives here (not
  // in `ChatRoot`) so it works for *every* usage pattern — `ChatRoot`,
  // a hand-rolled `ChatProvider` + `Composer` layout, or headless — as
  // long as a composer registered its handle. The active handle lives
  // in `@djangocfg/ui-tools/composer-registry` — a single cross-tool
  // registry shared with `<VoiceComposerSlot>`.
  useStreamEndFocus({
    isStreaming: chat.isStreaming,
    enabled: autoFocusOnStreamEnd,
    delayMs: 0,
    resolveTarget: () => getActiveComposer(),
  });

  // Focus the composer on mount (and, via the host's `key={id}` engine
  // remount, on every chat-switch) so the caret is ready the moment the
  // chat appears. Pairs with the stream-end focus above.
  useAutoFocusOnMount({ enabled: autoFocusOnMount });

  // An IN-PLACE session switch (initialSessionId change, no remount) deserves
  // the same treatment: land the caret in the composer when the room changes,
  // exactly like the legacy remount did via the mount focus above.
  const prevSessionRef = useRef(chat.sessionId);
  useEffect(() => {
    if (prevSessionRef.current === chat.sessionId) return;
    prevSessionRef.current = chat.sessionId;
    if (!autoFocusOnMount) return;
    // Session switch is the provider's second and final automatic focus
    // intent (after mount). Wait one frame for a closing dialog / editor DOM
    // update, rather than competing with consumers through retry loops.
    const raf = requestAnimationFrame(() => getActiveComposer()?.focus());
    return () => cancelAnimationFrame(raf);
  }, [chat.sessionId, autoFocusOnMount]);

  const value = useMemo<ChatContextValue>(
    () => ({
      ...chat,
      layout,
      config,
      labels,
      audio: audioApi,
      hasAudio,
      blockRegistry: blockRegistry ?? null,
    }),
    [chat, layout, config, labels, audioApi, hasAudio, blockRegistry],
  );

  return (
    <Ctx.Provider value={value}>
      <div ref={rootRef} style={{ display: 'contents' }}>
        {children}
      </div>
    </Ctx.Provider>
  );
}

export function useChatContext(): ChatContextValue {
  const v = useContext(Ctx);
  if (!v) throw new Error('useChatContext must be used inside <ChatProvider>');
  return v;
}

export function useChatContextOptional(): ChatContextValue | null {
  return useContext(Ctx);
}
