'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, 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 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;
  initialSessionId?: string;
  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;
  children?: ReactNode;
}

export function ChatProvider({
  transport,
  config = {},
  initialSessionId,
  autoCreateSession,
  streaming,
  audio,
  debug,
  blockRegistry,
  onBeforeSend,
  getDynamicMetadata,
  autoFocusOnStreamEnd = 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,
    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(),
  });

  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);
}
