'use client';

import { memo, type ReactNode } from 'react';

import { Avatar, AvatarFallback, AvatarImage } from '@djangocfg/ui-core/components';
import { cn } from '@djangocfg/ui-core/lib';

import { MarkdownMessage } from '../../MarkdownMessage';
import type {
  ChatAssistantContext,
  ChatAttachment,
  ChatMessage,
  ChatSource,
  ChatToolCall,
  ChatUserContext,
} from '../types';
import { resolvePersona, deriveInitials } from '../core/persona';
import { useChatContextOptional } from '../context';
import { useChatBubbleStyles } from '../styles';
import { StreamingIndicator } from './StreamingIndicator';
import { Sources } from './Sources';
import { ToolCalls } from './ToolCalls';
import {
  AttachmentsGrid,
  AttachmentsList,
  type AttachmentRendererMap,
} from './Attachments';
import { MessageActions } from './MessageActions';
import { MessageBlocks } from './blocks';
import type { BlockRegistry } from './blocks';
import type { ToolCallsProps } from './ToolCalls';
import type { ComposerAppearance } from '../composer/types';

export interface MessageBubbleProps {
  message: ChatMessage;
  isUser?: boolean;
  showAvatar?: boolean;
  /** Override avatar URL (skips persona resolution). */
  avatarSrc?: string;
  /** Override avatar fallback (skips persona resolution). */
  avatarFallback?: ReactNode;
  /** Personas — when provided, take precedence over context. */
  user?: ChatUserContext;
  assistant?: ChatAssistantContext;
  showTimestamp?: boolean;
  showActions?: boolean;
  isCompact?: boolean;
  /** Spaciousness of the bubble — `full` scales padding, text, avatar up
   * for full-page chat. `compact` (default) keeps the current geometry. */
  appearance?: ComposerAppearance;
  className?: string;
  beforeContent?: ReactNode;
  afterContent?: ReactNode;
  toolCallsRenderer?: (calls: ChatToolCall[]) => ReactNode;
  /** Forwarded to the default `<ToolCalls>` when `toolCallsRenderer` is not set. */
  toolCallsProps?: Omit<ToolCallsProps, 'calls'>;
  sourcesRenderer?: (sources: ChatSource[]) => ReactNode;
  attachmentsRenderer?: (atts: ChatAttachment[]) => ReactNode;
  /** Per-type attachment renderers forwarded to default `<Attachments>`. */
  attachmentRenderers?: AttachmentRendererMap;
  /**
   * Registry of `kind` → renderer for `message.blocks`. Prop beats
   * context; both fall back to `BUILTIN_BLOCK_REGISTRY`.
   */
  blockRegistry?: BlockRegistry;
  /** Click handler for attachment tiles (e.g. open lightbox). */
  onAttachmentOpen?: (a: ChatAttachment) => void;
  onCopy?: () => void;
  onRegenerate?: () => void;
  onEdit?: () => void;
  onDelete?: () => void;
  /**
   * Extra content rendered alongside the default copy/regenerate/edit/
   * delete actions (after them, on the same row). Hosts pass app-
   * specific affordances here — e.g. "Send to plan", "Add note",
   * "Open in inspector" — without having to fork `<MessageActions>`.
   * Receives the message so renderers can branch by id / role / etc.
   * Plan64.
   */
  messageActionsExtra?: (m: ChatMessage) => ReactNode;
  /**
   * Override the default streaming indicator (the dots / pulse + tool
   * activity label). Receives the message so the host can read
   * `toolActivity`, the running tool call name, etc., and render a
   * richer affordance ("Running cmd_execute on vps-audi…"). Plan64.
   *
   * Renders in two slots: as the in-bubble pre-token affordance, and
   * as the inline label above the bubble when `toolActivity` is set.
   * The render-prop is called for both — branch on `m.content` if
   * you need different behaviour per slot.
   */
  streamingIndicator?: (m: ChatMessage) => ReactNode;
  /**
   * Render arbitrary content beneath the message body, regardless of
   * whether the message carries tool calls. Used for product widgets
   * that ride a side channel (e.g. `ui_payload` SSE frames driving
   * vehicle cards, tax tables, charts) and therefore can't piggy-back
   * on the `toolCallsRenderer` slot — the latter is gated on the
   * message having a non-empty `toolCalls` array, which doesn't hold
   * when raw tool events are hidden from public clients.
   *
   * Receives the message so the host can scope the widget by id /
   * role / timing. Renders even on streaming messages, so progressive
   * UI hints (skeletons that fill in as payloads arrive) work as
   * expected.
   */
  renderAfterMessage?: (m: ChatMessage) => ReactNode;
}

/** Extra spaciousness layered on top of the default bubble geometry when
 *  `appearance="full"`. Empty strings for `compact` keep every pixel of the
 *  current geometry untouched (backward-compat). */
const APPEARANCE_CLASSES: Record<ComposerAppearance, {
  row: string;
  avatar: string;
  avatarFallback: string;
  bubble: string;
  meta: string;
}> = {
  compact: { row: '', avatar: '', avatarFallback: '', bubble: '', meta: '' },
  full: {
    row: 'gap-4',
    avatar: 'size-10',
    avatarFallback: 'text-sm',
    // Roomy ChatGPT-style bubble: larger text with generous line-height
    // and padding so a full-page chat reads spacious, not just bigger.
    bubble: 'rounded-3xl px-5 py-3.5 text-base leading-relaxed',
    meta: 'text-xs',
  },
};

const MessageBubbleInner = ({
  message,
  isUser: isUserProp,
  showAvatar = true,
  avatarSrc,
  avatarFallback,
  user,
  assistant,
  showTimestamp = false,
  showActions = true,
  isCompact = false,
  appearance = 'compact',
  className,
  beforeContent,
  afterContent,
  toolCallsRenderer,
  toolCallsProps,
  sourcesRenderer,
  attachmentsRenderer,
  attachmentRenderers,
  blockRegistry,
  onAttachmentOpen,
  onCopy,
  onRegenerate,
  onEdit,
  onDelete,
  messageActionsExtra,
  streamingIndicator,
  renderAfterMessage,
}: MessageBubbleProps) => {
  const isUser = isUserProp ?? message.role === 'user';
  const isStreaming = !!message.isStreaming;
  const isErr = !!message.isError;
  const { surface: bubbleSurface } = useChatBubbleStyles(
    isUser ? 'user' : 'assistant',
    isErr,
  );

  const ctx = useChatContextOptional();
  // Prop beats context; `<MessageBlocks>` falls back to the built-in
  // registry when both are absent.
  const resolvedBlockRegistry = blockRegistry ?? ctx?.blockRegistry;
  const persona = resolvePersona(
    message,
    user ?? ctx?.config.user,
    assistant ?? ctx?.config.assistant,
  );
  const initials = deriveInitials(persona, message.role);
  const personaName = persona.name ?? (isUser ? 'You' : 'Assistant');
  // `appearance` scales the whole bubble (padding, text, avatar) so a
  // full-page chat reads like ChatGPT/Gemini. `compact` = no changes.
  const ap = APPEARANCE_CLASSES[appearance];

  return (
    <div
      role="article"
      aria-label={`${personaName} said: ${message.content.slice(0, 80)}`}
      aria-busy={isStreaming || undefined}
      data-role={message.role}
      className={cn(
        'group/msg flex gap-2.5 px-2.5 py-2',
        ap.row,
        isUser ? 'flex-row-reverse' : 'flex-row',
        className,
      )}
    >
      {showAvatar ? (
        <Avatar
          className={cn('size-7 shrink-0', ap.avatar)}
          title={persona.description ?? personaName}
        >
          {avatarSrc || persona.avatarUrl ? (
            <AvatarImage src={avatarSrc ?? persona.avatarUrl} alt={personaName} />
          ) : null}
          <AvatarFallback className={cn('text-[10px]', ap.avatarFallback)}>
            {avatarFallback ?? initials}
          </AvatarFallback>
        </Avatar>
      ) : null}

      <div className={cn('min-w-0 flex-1', isUser && 'flex flex-col items-end')}>
        {beforeContent}
        {message.attachments?.length
          ? attachmentsRenderer
            ? attachmentsRenderer(message.attachments)
            : (
                <div className="mb-1.5 w-full">
                  {attachmentRenderers ? (
                    <AttachmentsList
                      attachments={message.attachments}
                      renderers={attachmentRenderers}
                      onClick={onAttachmentOpen}
                      className={isUser ? 'items-end' : undefined}
                    />
                  ) : (
                    <AttachmentsGrid
                      attachments={message.attachments}
                      onClick={onAttachmentOpen}
                      className={isUser ? 'justify-end' : undefined}
                    />
                  )}
                </div>
              )
          : null}

        <div
          className={cn(
            'inline-block max-w-full rounded-2xl px-3.5 py-2 text-sm',
            ap.bubble,
            bubbleSurface,
          )}
        >
          {isStreaming && message.toolActivity ? (
            <div className="mb-1.5">
              {streamingIndicator
                ? streamingIndicator(message)
                : <StreamingIndicator label={message.toolActivity} />}
            </div>
          ) : null}

          {message.content || !isStreaming ? (
            <MarkdownMessage
              content={message.content || (isErr ? '*Failed to generate a response.*' : '')}
              isUser={isUser}
              size={appearance === 'full' ? 'base' : isCompact ? 'xs' : 'sm'}
              plainText={isStreaming}
            />
          ) : (
            streamingIndicator ? streamingIndicator(message) : <StreamingIndicator />
          )}
        </div>

        {message.blocks?.length ? (
          <MessageBlocks
            blocks={message.blocks}
            registry={resolvedBlockRegistry}
            appearance={appearance}
            isUser={isUser}
          />
        ) : null}

        {message.toolCalls?.length
          ? toolCallsRenderer
            ? toolCallsRenderer(message.toolCalls)
            : <ToolCalls calls={message.toolCalls} {...toolCallsProps} />
          : null}

        {renderAfterMessage ? renderAfterMessage(message) : null}

        {message.sources?.length && !isStreaming
          ? sourcesRenderer
            ? sourcesRenderer(message.sources)
            : <Sources sources={message.sources} />
          : null}

        {showActions && !isStreaming ? (
          <div className="flex items-center gap-0.5">
            <MessageActions
              role={message.role}
              onCopy={onCopy}
              onRegenerate={onRegenerate}
              onEdit={onEdit}
              onDelete={onDelete}
            />
            {messageActionsExtra ? messageActionsExtra(message) : null}
          </div>
        ) : null}

        {showTimestamp ? (
          <div className={cn('mt-1 text-[10px] text-muted-foreground', ap.meta)}>
            {new Date(message.createdAt).toLocaleTimeString()}
          </div>
        ) : null}

        {afterContent}
      </div>
    </div>
  );
};

export const MessageBubble = memo(MessageBubbleInner, (prev, next) => {
  const a = prev.message;
  const b = next.message;
  return (
    a.id === b.id &&
    a.content === b.content &&
    a.isStreaming === b.isStreaming &&
    a.isError === b.isError &&
    (a.version ?? 0) === (b.version ?? 0) &&
    a.toolActivity === b.toolActivity &&
    a.toolCalls === b.toolCalls &&
    a.sources === b.sources &&
    a.attachments === b.attachments &&
    a.blocks === b.blocks
  );
});
MessageBubble.displayName = 'MessageBubble';
