'use client';

import { forwardRef, useEffect, useRef } from 'react';

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

import { useChatContextOptional } from '../context';
import type { UseChatComposerReturn } from '../hooks/useChatComposer';
import { Attachments } from '../messages/Attachments';
import { ComposerActionBar } from './ComposerActionBar';
import { ComposerButton } from './ComposerButton';
import { ComposerFooter } from './ComposerFooter';
import { ComposerSizeProvider } from './size-context';
import { useComposerActions } from './useComposerActions';
import type {
  ComposerAppearance,
  ComposerFooterProps,
  ComposerLayout,
  ComposerSize,
  ComposerSlotComponents,
  ComposerSlotProps,
  ComposerSlots,
} from './types';

export type { ComposerSize, ComposerAppearance } from './types';

export interface ComposerProps {
  composer: UseChatComposerReturn;
  placeholder?: string;
  disabled?: boolean;
  showAttachmentButton?: boolean;
  onPickFiles?: () => void;
  className?: string;
  textareaClassName?: string;
  /** Visual size — controls textarea height + button slot size.
   *
   * - ``sm``  — dense compact composer (admin sidebars, etc); defaults
   *   to the `inline` single-row layout.
   * - ``md``  — default. Stacked layout (textarea + action bar).
   * - ``lg``  — generous textarea. Use when the chat is the page's
   *   primary surface (onboarding, support).
   */
  size?: ComposerSize;
  /** Spaciousness of the surface — orthogonal to `size`.
   *
   * - ``compact`` — default; embedded composer (FAB panel, dock).
   * - ``full``    — full-page chat; roomier surface, taller textarea,
   *   larger padding + radius. The host owns the centered max-width
   *   container; the composer just makes itself more spacious.
   */
  appearance?: ComposerAppearance;
  /** Show "Stop" button instead of "Send" while streaming. */
  isStreaming?: boolean;
  onCancel?: () => void;

  // ── Slot system ──────────────────────────────────────────────────────────
  /** Layout geometry. Default `stacked`; `size="sm"` falls back to `inline`. */
  layout?: ComposerLayout;
  /** Tier A — declarative action descriptors + raw-node escape hatches. */
  composerSlots?: ComposerSlots;
  /** Tier B — replace a primitive entirely. */
  slots?: ComposerSlotComponents;
  /** Per-slot prop overrides for the built-in primitives. */
  slotProps?: ComposerSlotProps;
  /** Telegram-style mic↔send swap. Default `true`. */
  micSendSwap?: boolean;
  /** Footer toolbar config below the input surface. `false` hides it. */
  footer?: ComposerFooterProps | false;
}

const SIZE_CLASSES: Record<ComposerSize, {
  surface: string;
  textarea: string;
  text: string;
  containerPadding: string;
  /** inline-layout slot height enforcement */
  inlineSlot: string;
}> = {
  sm: {
    surface: 'rounded-xl px-2 py-1',
    textarea: 'min-h-7 max-h-44 px-1.5 py-1',
    text: 'text-sm',
    containerPadding: 'px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))]',
    inlineSlot: '[&>:not(textarea)]:h-7',
  },
  md: {
    surface: 'rounded-2xl px-2 py-1.5',
    textarea: 'min-h-9 max-h-60 px-2 py-1.5',
    text: 'text-base sm:text-sm',
    containerPadding: 'px-2.5 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))]',
    inlineSlot: '[&>:not(textarea)]:h-9',
  },
  lg: {
    surface: 'rounded-2xl px-3 py-2.5',
    textarea: 'min-h-12 max-h-72 px-2.5 py-2',
    text: 'text-base',
    containerPadding: 'px-3.5 pt-3 pb-[max(0.875rem,env(safe-area-inset-bottom))]',
    inlineSlot: '[&>:not(textarea)]:h-12',
  },
};

/** Per-size cluster gap for the stacked grid layout. */
const GAP: Record<ComposerSize, string> = {
  sm: 'gap-0.5',
  md: 'gap-1',
  lg: 'gap-1.5',
};

/** Extra spaciousness layered on top of `size` when `appearance="full"`.
 *  Empty strings for `compact` keep the current geometry untouched. */
const APPEARANCE_CLASSES: Record<ComposerAppearance, {
  surface: string;
  textarea: string;
  text: string;
  containerPadding: string;
}> = {
  compact: { surface: '', textarea: '', text: '', containerPadding: '' },
  full: {
    surface: 'rounded-3xl px-3 py-2',
    textarea: 'min-h-10 max-h-80 px-2.5 py-2',
    text: 'text-base',
    containerPadding: 'px-4 pt-3 pb-[max(0.75rem,env(safe-area-inset-bottom))]',
  },
};

export const Composer = forwardRef<HTMLDivElement, ComposerProps>(function Composer(
  {
    composer,
    placeholder = 'Type a message...',
    disabled,
    showAttachmentButton = false,
    onPickFiles,
    className,
    textareaClassName,
    size = 'md',
    appearance = 'compact',
    isStreaming: isStreamingProp,
    onCancel: onCancelProp,
    layout: layoutProp,
    composerSlots,
    slots,
    slotProps,
    micSendSwap = true,
    footer,
  },
  ref,
) {
  const ctx = useChatContextOptional();
  const isStreaming = isStreamingProp ?? ctx?.isStreaming ?? false;
  const onCancel = onCancelProp ?? ctx?.cancelStream;
  const isDisabled = disabled ?? isStreaming;
  const sz = SIZE_CLASSES[size];
  // `appearance` is orthogonal to `size` — `full` layers extra
  // spaciousness (radius, padding, textarea height) over the chosen size.
  const ap = APPEARANCE_CLASSES[appearance];
  // `size="sm"` defaults to the single-row `inline` layout unless the
  // host overrides it explicitly — see §3.1 of the redesign doc.
  const layout: ComposerLayout = layoutProp ?? (size === 'sm' ? 'inline' : 'stacked');

  // Register the composer's handle with the chat context so other parts
  // of the tree (useAutoFocusOnStreamEnd, VoiceComposerSlot) can drive it
  // imperatively. No-op when used outside a ChatProvider.
  const register = ctx?.registerComposer;
  const composerFocus = composer.focus;
  const composerSetValue = composer.setValue;
  const textareaRef = composer.textareaRef;
  const getValueRef = useRef<() => string>(() => composer.value);
  getValueRef.current = () => composer.value;
  // A custom `Textarea` slot (e.g. the TipTap `<ComposerRichTextarea>`)
  // has no `<textarea>` — the default `composer.focus` would be a no-op.
  // In that case the slot registers its own imperative handle; skip
  // ours so we don't overwrite it (child effects run before parent).
  const hasCustomTextarea = !!slots?.Textarea;
  useEffect(() => {
    if (!register || hasCustomTextarea) return;
    register({
      focus: composerFocus,
      moveCursorToEnd: () => {
        const el = textareaRef.current;
        if (!el) return;
        const end = el.value.length;
        el.setSelectionRange(end, end);
      },
      getValue: () => getValueRef.current(),
      setValue: composerSetValue,
    });
    return () => register(null);
  }, [register, hasCustomTextarea, composerFocus, composerSetValue, textareaRef]);

  const inlineStart = composerSlots?.inlineStart;
  const inlineEnd = composerSlots?.inlineEnd;
  const blockStart = composerSlots?.blockStart;

  // Clicking the padding around the input (not a button/link) should focus
  // the editor — same affordance as ChatGPT/Gemini. Works for both the
  // plain `<textarea>` and the TipTap (`contenteditable`) backend by
  // querying the focusable node inside the surface itself, so it does not
  // depend on which textarea variant is mounted.
  const surfaceRef = useRef<HTMLDivElement>(null);
  const handleSurfaceMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
    if (isDisabled) return;
    const target = e.target as HTMLElement;
    // A real interactive element (or the editor itself) handles its own
    // focus — only act on clicks that land on the bare surface padding.
    if (
      target.closest(
        'textarea, input, button, a, [contenteditable="true"], [role="button"], [role="menu"]',
      )
    ) {
      return;
    }
    const editable = surfaceRef.current?.querySelector<HTMLElement>(
      'textarea, [contenteditable="true"]',
    );
    if (!editable) return;
    // Prevent the mousedown from blurring/again-selecting and move focus
    // (caret lands at the end via the textarea's own selection on focus).
    e.preventDefault();
    editable.focus();
  };

  // Merge built-in send/stop/attach descriptors with host arrays.
  const { actionsStart, actionsEnd } = useComposerActions({
    composer,
    isStreaming,
    isDisabled,
    showAttachmentButton,
    onPickFiles,
    actionsStart: composerSlots?.actionsStart,
    actionsEnd: composerSlots?.actionsEnd,
    onSend: () => void composer.submit(),
    onCancel,
    micSendSwap,
  });

  const SendSlot = slots?.SendButton;
  const AttachSlot = slots?.AttachButton;
  const TextareaSlot = slots?.Textarea;
  const ActionBarSlot = slots?.ActionBar;

  // ── Textarea ───────────────────────────────────────────────────────────
  const textareaNode = TextareaSlot ? (
    <TextareaSlot
      composer={composer}
      placeholder={placeholder}
      disabled={isDisabled}
      size={size}
      className={slotProps?.textarea?.className ?? textareaClassName}
    />
  ) : (
    <Textarea
      {...composer.textareaProps}
      rows={1}
      placeholder={placeholder}
      aria-label={placeholder}
      aria-multiline="true"
      aria-keyshortcuts="Enter"
      disabled={isDisabled}
      className={cn(
        'flex-1 resize-none border-0 bg-transparent shadow-none',
        'focus-visible:ring-0 focus-visible:outline-none',
        sz.textarea,
        sz.text,
        ap.textarea,
        ap.text,
        slotProps?.textarea?.className,
        textareaClassName,
      )}
    />
  );

  // ── Action bar ─────────────────────────────────────────────────────────
  const actionBarNode = ActionBarSlot ? (
    <ActionBarSlot
      actionsStart={actionsStart}
      actionsEnd={actionsEnd}
      size={size}
      layout={layout}
      inlineStart={inlineStart}
      inlineEnd={inlineEnd}
    />
  ) : (
    <ComposerActionBar
      actionsStart={actionsStart}
      actionsEnd={actionsEnd}
      size={size}
      layout={layout}
      inlineStart={inlineStart}
      inlineEnd={inlineEnd}
      className={slotProps?.actionBar?.className}
    />
  );

  // Tier B fully-custom Send / Attach replacements override the
  // descriptor-driven action bar. They render alongside it for hosts
  // that need a branded button — the built-in send still lives in
  // `actionsEnd` unless the host also drops it from `composerSlots`.
  const customSend = SendSlot ? (
    <SendSlot
      streaming={isStreaming}
      disabled={!composer.canSubmit}
      size={size}
      onSend={() => void composer.submit()}
      onCancel={onCancel}
      {...slotProps?.send}
    />
  ) : null;
  const customAttach =
    AttachSlot && showAttachmentButton ? (
      <AttachSlot
        disabled={isDisabled}
        size={size}
        onClick={() => onPickFiles?.()}
        {...slotProps?.attach}
      />
    ) : null;

  // ── Footer ─────────────────────────────────────────────────────────────
  const footerNode =
    footer === false ? null : (
      <ComposerFooter
        {...footer}
        value={composer.value}
        size={footer?.size ?? size}
      />
    );

  const trayNode =
    blockStart ??
    (composer.attachments.length > 0 ? (
      <Attachments
        attachments={composer.attachments}
        onRemove={(a) => composer.removeAttachment(a.id)}
      />
    ) : null);

  // ── Inline (compact single-row) layout ─────────────────────────────────
  // Start cluster sits left of the textarea, end cluster right of it —
  // `[📎][actionsStart] [textarea] [actionsEnd][🎙][▶]` (§3.6).
  if (layout === 'inline') {
    return (
      <ComposerSizeProvider value={size}>
        <div
          ref={ref}
          className={cn(
            'border-t border-border bg-background/95',
            sz.containerPadding,
            ap.containerPadding,
            className,
          )}
        >
          {trayNode ? <div className="mb-1.5">{trayNode}</div> : null}
          <div
            className={cn(
              'flex items-end gap-1.5 [&>:not(textarea)]:shrink-0',
              sz.inlineSlot,
            )}
          >
            {customAttach}
            {ActionBarSlot ? (
              actionBarNode
            ) : (
              <ComposerActionBar
                actionsStart={actionsStart}
                actionsEnd={[]}
                size={size}
                layout={layout}
                inlineStart={inlineStart}
                cluster="start"
              />
            )}
            {textareaNode}
            {ActionBarSlot ? null : (
              <ComposerActionBar
                actionsStart={[]}
                actionsEnd={actionsEnd}
                size={size}
                layout={layout}
                inlineEnd={inlineEnd}
                cluster="end"
              />
            )}
            {customSend}
          </div>
          {footerNode}
        </div>
      </ComposerSizeProvider>
    );
  }

  // ── Stacked (default) layout — two-tier surface ────────────────────────
  // The textarea always occupies the full top row; the action bar (attach /
  // tools on the left, mic / send on the right) sits in a fixed row below.
  // The draft never shares a baseline with the buttons — the text stays put
  // whether it is one line or many.
  const surfaceWrapper = (
    <div
      ref={surfaceRef}
      onMouseDown={handleSurfaceMouseDown}
      className={cn(
        // Raised input panel — reads against the chat background via the
        // neutral `muted` surface + a hairline ring (Gemini/ChatGPT
        // style), not a heavy drawn border. Focus firms up the ring
        // without a hard accent frame.
        'flex flex-col rounded-2xl text-foreground',
        'bg-muted ring-1 ring-border',
        'focus-within:ring-ring/50',
        'transition-colors',
        // The whole surface reads as a text field — the padding around the
        // editor shows a text caret and is click-to-focus.
        'cursor-text',
        sz.surface,
        ap.surface,
      )}
    >
      {/* Tier B: a fully-custom ActionBar renders below the textarea. */}
      {ActionBarSlot ? (
        <>
          {textareaNode}
          <div className="mt-1 flex items-center gap-1.5">
            {customAttach}
            {actionBarNode}
            {customSend}
          </div>
        </>
      ) : (
        <>
          {/* primary — textarea, full width on top */}
          <div className="flex">{textareaNode}</div>

          {/* action bar — leading cluster left, trailing cluster right */}
          <div className={cn('mt-1 flex items-center justify-between', GAP[size])}>
            <div className={cn('flex items-center', GAP[size])}>
              {customAttach}
              {inlineStart}
              {actionsStart.map((action) => (
                <ComposerButton key={action.id} action={action} size={size} />
              ))}
            </div>
            <div className={cn('flex items-center', GAP[size])}>
              {inlineEnd}
              {actionsEnd.map((action) => (
                <ComposerButton key={action.id} action={action} size={size} />
              ))}
              {customSend}
            </div>
          </div>
        </>
      )}
    </div>
  );

  return (
    <ComposerSizeProvider value={size}>
      <div
        ref={ref}
        className={cn(
          'border-t border-border bg-background/95',
          sz.containerPadding,
          ap.containerPadding,
          className,
        )}
      >
        {trayNode ? <div className="mb-2">{trayNode}</div> : null}
        {surfaceWrapper}
        {footerNode}
      </div>
    </ComposerSizeProvider>
  );
});
