'use client';

import { type ReactNode, useMemo, useRef, useState } from 'react';

import { cn } from '@djangocfg/ui-core/lib';

import type { ChatAttachment, ChatConfig, ChatMessage, ChatTransport } from '../types';
import type { ChatAudioConfig } from '../core/audio/types';
import {
  ChatProvider,
  useChatContext,
  useChatContextOptional,
  type ChatContextValue,
} from '../context';
import { useChatComposer, type UseChatComposerReturn } from '../hooks/useChatComposer';
import { useFocusOnEmptyClick } from '../hooks/useFocusOnEmptyClick';
import { Composer } from '../composer/Composer';
import type {
  ComposerAppearance,
  ComposerFooterProps,
  ComposerLayout,
  ComposerSize,
  ComposerSlots,
} from '../composer/types';
import { EmptyState } from './EmptyState';
import { ErrorBanner } from './ErrorBanner';
import { JumpToLatest } from '../messages/JumpToLatest';
import { MessageBubble } from '../messages/MessageBubble';
import type { BubbleMenuItem } from '../messages/BubbleContextMenu';
import { MessageList, type MessageListHandle } from '../messages/MessageList';
import type { AttachmentRendererMap } from '../messages/Attachments';
import type { BlockRegistry } from '../messages/blocks';
import type { ToolCallsProps } from '../messages/ToolCalls';

/** Empty-state render API — seeds + focuses the composer from prompt chips. */
export interface ChatEmptyApi {
  setValue: (v: string) => void;
  focus: () => void;
}

/** Full-replacement render API for the composer slot — the live composer
 *  hook plus the resolved {@link ChatComposerConfig} that produced it.
 *  One source of truth; no hand-mirrored field list. */
export interface ChatComposerRenderApi {
  composer: UseChatComposerReturn;
  config: ChatComposerConfig;
}

/** Composer-region configuration for `<ChatRoot composer={…}>`. Groups
 *  every knob that forwards into the built-in `<Composer>`. */
export interface ChatComposerConfig {
  /** Visual size variant. Default `md`. */
  size?: ComposerSize;
  /** Input-surface layout. Default `stacked` (`sm` → `inline`). */
  layout?: ComposerLayout;
  /** Extra className on the `<Composer>` wrapper. */
  className?: string;
  /** Hide the composer entirely (e.g. while waiting for human approval). */
  hidden?: boolean;
  /** Show the paperclip "attach" button. */
  showAttachmentButton?: boolean;
  /** Called when the user clicks the attach button. */
  onPickFiles?: () => void;
  /** Tier A declarative slots — action clusters + raw-node escape hatches. */
  slots?: ComposerSlots;
  /** Footer toolbar below the input surface. `false` hides it. */
  footer?: ComposerFooterProps | false;
  /** Full replacement — receives the live composer hook + this config. */
  render?: (api: ChatComposerRenderApi) => ReactNode;
}

/** Named ReactNode slots for `<ChatRoot slots={…}>`. The `header` / `empty`
 *  slots accept either a node or a render fn — no separate `renderX` prop. */
export interface ChatSlots {
  /** Sticky banner above the message list (e.g. quota warning). */
  banner?: ReactNode;
  /** Header row below the banner — node, or a fn given the chat context. */
  header?: ReactNode | ((ctx: ChatContextValue) => ReactNode);
  /** Replaces the default `<EmptyState>` — node, or a fn given a seed API. */
  empty?: ReactNode | ((api: ChatEmptyApi) => ReactNode);
  /** Replaces the default `<JumpToLatest>` floating pill. */
  jumpToLatest?: ReactNode;
}

/** Message-rendering configuration for `<ChatRoot messages={…}>`. */
export interface ChatMessagesConfig {
  /** Replace `<MessageBubble>` per message. */
  render?: (m: ChatMessage, i: number) => ReactNode;
  /**
   * Render arbitrary content beneath every default `<MessageBubble>`
   * (not invoked when `render` is set — the host owns layout then).
   */
  renderAfter?: (m: ChatMessage) => ReactNode;
  /** Forwarded into `<MessageBubble toolCallsProps>` to swap payload renderers. */
  toolCallsProps?: Omit<ToolCallsProps, 'calls'>;
  /** Per-type attachment renderers — `{ image, audio, video, file, default }`. */
  attachmentRenderers?: AttachmentRendererMap;
  /** Called when an attachment tile is clicked (e.g. open lightbox). */
  onAttachmentOpen?: (attachment: ChatAttachment) => void;
  /**
   * Registry of `kind` → renderer for `message.blocks`. Merge host
   * overrides over the defaults with `createBlockRegistry({ … })`.
   * Omit to use `BUILTIN_BLOCK_REGISTRY`.
   */
  blockRegistry?: BlockRegistry;
  /**
   * Host-injected EXTRA right-click / long-press menu items per bubble,
   * merged after the built-in Copy / Regenerate / Delete defaults.
   * Forwarded to `<MessageBubble bubbleMenuItems>`.
   */
  bubbleMenuItems?: BubbleMenuItem[];
  /**
   * Full control over each bubble's context menu — receives the message
   * and the built-in defaults, returns the final ordered list. Beats
   * `bubbleMenuItems`. Forwarded to `<MessageBubble getBubbleMenuItems>`.
   */
  getBubbleMenuItems?: (message: ChatMessage, defaults: BubbleMenuItem[]) => BubbleMenuItem[];
  /**
   * Host handler for the bubble "Edit" menu item (user messages only — the
   * builtin builder user-gates it). The host loads the message's content into
   * its composer for editing and re-runs the turn on submit via
   * `chat.editMessage` (engine truncate+resend). When omitted the "Edit" item
   * never appears. This is intentionally a host hook, not an inline editor: the
   * edit happens in the real composer, not the bubble.
   */
  onEdit?: (message: ChatMessage) => void;
}

export interface ChatRootProps {
  // ---- core wiring -------------------------------------------------------
  /**
   * Transport. Required UNLESS `<ChatRoot>` is rendered inside an
   * existing `<ChatProvider>` (e.g. mounted by `<ChatLauncher>`), in
   * which case the ambient provider is reused and `transport` is
   * ignored.
   */
  transport?: ChatTransport;
  config?: ChatConfig;
  /** Session wiring — pre-existing id to attach, auto-create toggle. */
  session?: { initialId?: string; autoCreate?: boolean };
  streaming?: boolean;
  /** Audio-trigger configuration. Off by default (no `sounds` map). */
  audio?: ChatAudioConfig;
  /**
   * Verbose dev-mode logging via `consola` (namespace `chat:*`).
   * Defaults to `isDev` from `@djangocfg/ui-core/lib`.
   */
  debug?: boolean;

  // ---- presentation ------------------------------------------------------
  /** Spaciousness of the whole chat — scales bubbles + composer.
   *  `compact` (default) for embedded chat, `full` for full-page. */
  appearance?: ComposerAppearance;
  /** Extra className on the `<ChatRoot>` wrapper. */
  className?: string;
  /** Extra className forwarded to the `<MessageList>` scroll container. */
  listClassName?: string;

  // ---- composition (grouped) --------------------------------------------
  /** Named ReactNode slots — banner, header, empty, jumpToLatest. */
  slots?: ChatSlots;
  /** Composer-region configuration. */
  composer?: ChatComposerConfig;
  /** Message-rendering configuration. */
  messages?: ChatMessagesConfig;

  // ---- behavior ----------------------------------------------------------
  /**
   * Click in the message area → focus the composer (Slack / ChatGPT).
   * @default true
   */
  focusOnEmptyClick?: boolean;
  /**
   * Contribute extra transport metadata, computed fresh per send.
   * Forwarded to the `<ChatProvider>` this component creates — ignored
   * when mounted under an ambient provider (set it on that provider
   * instead). Used to attach the page-context snapshot.
   */
  getDynamicMetadata?: () => Record<string, unknown> | undefined;
}

export function ChatRoot(props: ChatRootProps) {
  const {
    transport,
    config,
    session,
    streaming,
    audio,
    debug,
    className,
    listClassName,
    getDynamicMetadata,
    ...rest
  } = props;
  // When mounted under a launcher-owned `<ChatProvider>`, reuse that
  // provider instead of wrapping in a second one. This lets host code
  // freely nest `<ChatRoot>` inside `<ChatLauncher>` without losing
  // `headerSlots` access to the same session / clearMessages / audio.
  const ambient = useChatContextOptional();
  if (ambient) {
    return <ChatRootShell className={className} listClassName={listClassName} parts={rest} />;
  }
  if (!transport) {
    throw new Error(
      '<ChatRoot> requires `transport` when mounted outside a <ChatProvider>.',
    );
  }
  return (
    <ChatProvider
      transport={transport}
      config={config}
      initialSessionId={session?.initialId}
      autoCreateSession={session?.autoCreate}
      streaming={streaming}
      audio={audio}
      debug={debug}
      blockRegistry={rest.messages?.blockRegistry}
      getDynamicMetadata={getDynamicMetadata}
    >
      <ChatRootShell className={className} listClassName={listClassName} parts={rest} />
    </ChatProvider>
  );
}

type ChatRootParts = Pick<
  ChatRootProps,
  'appearance' | 'slots' | 'composer' | 'messages' | 'focusOnEmptyClick'
>;

interface ChatRootShellProps {
  className?: string;
  listClassName?: string;
  parts: ChatRootParts;
}

function ChatRootShell({ className, listClassName, parts }: ChatRootShellProps) {
  const { slots = {}, composer: composerConfig = {}, messages = {} } = parts;
  const chat = useChatContext();
  const composer = useChatComposer({
    onSubmit: (content, attachments) => chat.sendMessage(content, attachments),
    disabled: chat.isStreaming,
  });
  const onMessagesMouseUp = useFocusOnEmptyClick({
    enabled: parts.focusOnEmptyClick !== false,
  });
  // Stream-end composer re-focus is handled by `<ChatProvider>` — it
  // covers every usage pattern, not just `ChatRoot`.
  // MessageList (virtuoso) owns the scroll viewport. We talk to it via
  // the imperative handle (scrollToBottom) and the `onAtBottomChange`
  // callback (drives the pill).
  const listRef = useRef<MessageListHandle | null>(null);
  const [isAtBottom, setIsAtBottom] = useState(true);
  // The id of the most-recent user-sent message — passed as
  // `scrollAnchorId` so every send re-anchors the viewport.
  const lastUserMessageId = useMemo(() => {
    const msgs = chat.messages;
    for (let i = msgs.length - 1; i >= 0; i -= 1) {
      if (msgs[i].role === 'user') return msgs[i].id;
    }
    return null;
  }, [chat.messages]);
  const handleStartReached = chat.hasMore && !chat.isLoadingMore
    ? () => void chat.loadMore()
    : undefined;

  const greeting = chat.config.greeting ?? 'How can I help?';
  const description = chat.config.description;
  const suggestions = chat.config.suggestions;

  const headerNode = typeof slots.header === 'function'
    ? slots.header(chat)
    : slots.header;

  const emptyNode = typeof slots.empty === 'function'
    ? slots.empty({ setValue: composer.setValue, focus: composer.focus })
    : (slots.empty ?? (
        <EmptyState
          greeting={greeting}
          description={description}
          suggestions={suggestions}
          onPickSuggestion={(prompt) => {
            composer.setValue(prompt);
            composer.focus();
          }}
        />
      ));

  // `appearance` scales the whole chat (bubbles + composer). Default
  // `compact` keeps every embedded chat pixel-identical.
  const appearance: ComposerAppearance = parts.appearance ?? 'compact';

  const renderItem = messages.render
    ?? ((m: ChatMessage) => (
      <MessageBubble
        key={m.id}
        message={m}
        appearance={appearance}
        toolCallsProps={messages.toolCallsProps}
        attachmentRenderers={messages.attachmentRenderers}
        blockRegistry={messages.blockRegistry}
        onAttachmentOpen={messages.onAttachmentOpen}
        renderAfterMessage={messages.renderAfter}
        bubbleMenuItems={messages.bubbleMenuItems}
        getBubbleMenuItems={messages.getBubbleMenuItems}
        onCopy={() => copy(m.content)}
        onRegenerate={() => void chat.regenerate(m.id)}
        // Edit (user bubbles): hand the message to the host so it can load the
        // text into the real composer. The host owns the edit-mode UI + the
        // submit branch (chat.editMessage). Omitted handler → no Edit item.
        // No in-bubble editor: editing happens in the composer, not the bubble.
        onEdit={messages.onEdit ? () => messages.onEdit?.(m) : undefined}
        onDelete={() => chat.deleteMessage(m.id)}
      />
    ));

  return (
    <div className={cn('relative flex h-full min-h-0 flex-col overflow-hidden', className)}>
      {slots.banner ?? null}
      {headerNode ?? null}
      <div className="relative flex min-h-0 flex-1 flex-col" onMouseUp={onMessagesMouseUp}>
        <ErrorBanner
          error={chat.error}
          onDismiss={chat.error ? () => chat.clearMessages() : undefined}
          onRetry={chat.error ? () => void chat.regenerate() : undefined}
        />
        <MessageList
          ref={listRef}
          renderItem={renderItem}
          appearance={appearance}
          renderEmpty={() => <>{emptyNode}</>}
          className={listClassName}
          onStartReached={handleStartReached}
          onAtBottomChange={setIsAtBottom}
          scrollAnchorId={lastUserMessageId}
        />
        <div className="pointer-events-none absolute inset-x-0 bottom-2 flex justify-center">
          {slots.jumpToLatest ?? (
            <JumpToLatest
              visible={!isAtBottom}
              onClick={() => listRef.current?.scrollToBottom(true)}
            />
          )}
        </div>
      </div>
      {!composerConfig.hidden && (
        composerConfig.render
          ? composerConfig.render({ composer, config: composerConfig })
          : (
              <Composer
                composer={composer}
                placeholder={chat.config.placeholder}
                showAttachmentButton={composerConfig.showAttachmentButton}
                onPickFiles={composerConfig.onPickFiles}
                size={composerConfig.size}
                appearance={appearance}
                layout={composerConfig.layout}
                className={composerConfig.className}
                composerSlots={composerConfig.slots}
                footer={composerConfig.footer ?? {}}
              />
            )
      )}
    </div>
  );
}

function copy(text: string) {
  if (typeof navigator !== 'undefined' && navigator.clipboard) {
    void navigator.clipboard.writeText(text);
  }
}

// re-export for convenience: composer hook return is a common slot dependency
export type { UseChatComposerReturn };
