'use client';

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

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.
 */
export interface ComposerHandle {
  focus: () => void;
  /** Move the caret to the very end of the input. */
  moveCursorToEnd?: () => void;
  /** Read the current draft text. Needed by voice dictation to anchor
   *  partial transcripts onto the user's already-typed prefix. */
  getValue?: () => string;
  /** Replace the current draft text. Voice dictation uses this to push
   *  interim + final transcripts into the composer without owning a
   *  controlled binding. */
  setValue?: (value: string) => void;
}

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;
  /** Composer registry. The built-in `<Composer>` calls
   *  `registerComposer({ focus })` on mount; custom composers (e.g.
   *  cmdop's MarkdownEditor wrapper) do the same via the
   *  `useRegisterComposer` helper. Read it via `composer?.focus()` —
   *  null until any composer has mounted. Plan64 follow-up. */
  composer: ComposerHandle | null;
  registerComposer: (handle: ComposerHandle | null) => void;
  /** 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);

// ─── Dual-bundle guard ────────────────────────────────────────────────────
//
// `@djangocfg/ui-tools` ships its root export through `dist/` (compiled) but
// most subpaths resolve to raw `src/`. If a consumer mixes them — e.g.
// `ChatRoot` from the root barrel and `VoiceComposerSlot` from
// `@djangocfg/ui-tools/speech-recognition` — `createContext(...)` runs twice
// and produces two distinct context instances. The Composer registers its
// handle on one; `useChatContextOptional()` in the voice slot reads from the
// other, sees `null`, and silently drops every transcript.
//
// We tag the global with our module identity. When more than one tag is
// observed the consumer is told exactly what to do.
type GuardSlot = { stamps: Set<symbol>; warned: boolean };
const GLOBAL_KEY = '__djangocfg_chat_ctx_stamps__';
const stamp = Symbol('djangocfg.chat.ctx');
function markCtxLoad(): void {
  if (typeof globalThis === 'undefined') return;
  const g = globalThis as unknown as Record<string, GuardSlot | undefined>;
  let slot = g[GLOBAL_KEY];
  if (!slot) {
    slot = { stamps: new Set(), warned: false };
    g[GLOBAL_KEY] = slot;
  }
  slot.stamps.add(stamp);
  if (slot.stamps.size > 1 && !slot.warned && process.env.NODE_ENV !== 'production') {
    slot.warned = true;
    // eslint-disable-next-line no-console
    console.warn(
      '[@djangocfg/ui-tools/chat] Two ChatProvider context instances detected — ' +
        'this means `@djangocfg/ui-tools` was loaded twice (one bundle via the root ' +
        '`.` export → `dist/`, another via a `./chat` / `./speech-recognition` subpath → `src/`). ' +
        'Symptom: `useChatContextOptional()` returns `null` for descendants of `<ChatProvider>`, ' +
        'so VoiceComposerSlot drops transcripts, useChatReset silently no-ops, etc. ' +
        '\n\nFix: import every Chat surface from the SAME subpath. Recommended:\n' +
        "  import { ChatRoot, ChatLauncher, useChatContextOptional, … } from '@djangocfg/ui-tools/chat';\n" +
        "  import { VoiceComposerSlot } from '@djangocfg/ui-tools/speech-recognition';\n" +
        '\n(See packages/ui-tools/src/tools/Chat/README.md → "Anti-patterns".)',
    );
  }
}
markCtxLoad();

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

  // Composer registry — kept in state (not a ref) so consumers
  // observing `ctx.composer` re-render when a composer mounts /
  // unmounts. The setter is the API surface; pass it to the composer
  // on mount and call with `null` on unmount.
  const [composer, setComposer] = useState<ComposerHandle | null>(null);
  const registerComposer = useCallback((handle: ComposerHandle | null) => {
    setComposer(handle);
  }, []);

  // 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.
  const composerRef = useRef<ComposerHandle | null>(composer);
  composerRef.current = composer;
  useStreamEndFocus({
    isStreaming: chat.isStreaming,
    enabled: autoFocusOnStreamEnd,
    delayMs: 0,
    resolveTarget: () => composerRef.current,
  });

  const value = useMemo<ChatContextValue>(
    () => ({
      ...chat,
      layout,
      config,
      labels,
      audio: audioApi,
      hasAudio,
      composer,
      registerComposer,
      blockRegistry: blockRegistry ?? null,
    }),
    [chat, layout, config, labels, audioApi, hasAudio, composer, registerComposer, 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);
}
