import { useState, useEffect, useRef, useCallback, useMemo, forwardRef, memo } from 'react';
import { Link } from '@tanstack/react-router';
import { Sparkles, LogIn, ListTodo, TrendingUp, AlertCircle, Lightbulb, ChevronDown, ChevronUp, X } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { motion, AnimatePresence } from 'framer-motion';
import { trpc } from '@/lib/trpc';
import { useAIChat, type UIChatContext } from '@/hooks/useAIChat';
import { ChatInputBox, type ChatInputBoxHandle } from './ChatInputBox';
import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { useTaskBriefing } from '@/context/TaskBriefingContext';
import { TaskBriefingOverlay } from '@/components/brief/TaskBriefingOverlay';
import { ScrollArea } from '@/components/ui/scroll-area';
import { GradualBlur } from '@/components/ui/gradual-blur';
import { ChatEntityBlock } from './blocks/ChatEntityBlock';
import { ChatChartBlock } from './blocks/ChatChartBlock';
import type { EntityRefBlockData, ChartBlockData } from '../../../shared/api-types';

/** Fluid font size for chat messages — scales with viewport width */
// const CHAT_FONT = '1rem';

// ---------------------------------------------------------------------------
// Inline tag parsing (entities + charts)
// ---------------------------------------------------------------------------

/**
 * Matches entity tags in both formats:
 * - <task>[id1,id2]</task>
 * - <timeline>id1,id2</timeline>
 */
const ENTITY_TAG_RE = /<(?<entity>task|project|note|timeline|timelineEvent)>(?:\[(?<bracketIds>[^\]]+)\]|(?<plainIds>[^<]+))<\/\k<entity>>/;
/** Matches chart tags: <chart>{...JSON...}</chart> */
const CHART_TAG_RE = /<chart>(?<chartJson>\{[\s\S]*?\})<\/chart>/;
/** Combined: matches the first occurrence of either tag */
const INLINE_TAG_RE = new RegExp(`${ENTITY_TAG_RE.source}|${CHART_TAG_RE.source}`);

type ContentSegment =
  | { type: 'text'; content: string }
  | { type: 'entity'; entity: EntityRefBlockData['entity']; ids: string[] }
  | { type: 'chart'; data: ChartBlockData };

function parseInlineTags(content: string): ContentSegment[] {
  const segments: ContentSegment[] = [];
  let remaining = content;

  while (remaining) {
    const match = INLINE_TAG_RE.exec(remaining);
    if (!match) {
      segments.push({ type: 'text', content: remaining });
      break;
    }

    const before = remaining.slice(0, match.index);
    if (before) segments.push({ type: 'text', content: before });

    const groups = match.groups ?? {};

    if (groups.entity) {
      const entity = groups.entity as EntityRefBlockData['entity'];
      const rawIds = groups.bracketIds ?? groups.plainIds ?? '';
      const ids = rawIds.split(',').map((id) => id.trim()).filter(Boolean);
      segments.push({ type: 'entity', entity, ids });
    } else if (groups.chartJson) {
      try {
        const chartData = JSON.parse(groups.chartJson) as ChartBlockData;
        segments.push({ type: 'chart', data: chartData });
      } catch {
        // Malformed JSON — keep as text
        segments.push({ type: 'text', content: match[0] });
      }
    }

    const matchIndex = typeof match.index === 'number' ? match.index : 0;
    remaining = remaining.slice(matchIndex + match[0].length);
  }

  return segments;
}

function hasInlineTags(content: string): boolean {
  return INLINE_TAG_RE.test(content);
}

function mergeTimelineSegments(segments: ContentSegment[]): ContentSegment[] {
  const allTimelineIds: string[] = [];
  for (const seg of segments) {
    if (seg.type === 'entity' && (seg.entity === 'timeline' || seg.entity === 'timelineEvent')) {
      allTimelineIds.push(...seg.ids);
    }
  }

  const uniqueTimelineIds = [...new Set(allTimelineIds)];
  if (!uniqueTimelineIds.length) return segments;

  const merged: ContentSegment[] = [];
  let lastTimelineInsertIndex = 0;

  for (const seg of segments) {
    if (seg.type === 'entity' && (seg.entity === 'timeline' || seg.entity === 'timelineEvent')) {
      lastTimelineInsertIndex = merged.length;
      continue;
    }

    merged.push(seg);
  }

  // Keep prose flow untouched and place consolidated timeline at the last timeline tag position.
  merged.splice(lastTimelineInsertIndex, 0, { type: 'entity', entity: 'timeline', ids: uniqueTimelineIds });

  return merged.filter((seg) => !(seg.type === 'text' && !seg.content.trim()));
}

function mergeConsecutiveTaskSegments(segments: ContentSegment[]): ContentSegment[] {
  const merged: ContentSegment[] = [];

  for (let i = 0; i < segments.length; i += 1) {
    const current = segments[i];
    if (!current) continue;

    if (!(current?.type === 'entity' && current.entity === 'task')) {
      merged.push(current);
      continue;
    }

    const groupedIds: string[] = [...current.ids];
    let j = i + 1;

    // Merge only adjacent task tags, allowing whitespace-only text between them.
    while (j < segments.length) {
      const next = segments[j];

      if (next?.type === 'text' && !next.content.trim()) {
        j += 1;
        continue;
      }

      if (next?.type === 'entity' && next.entity === 'task') {
        groupedIds.push(...next.ids);
        j += 1;
        continue;
      }

      break;
    }

    merged.push({
      type: 'entity',
      entity: 'task',
      ids: [...new Set(groupedIds)],
    });

    i = j - 1;
  }

  return merged;
}

const SUGGESTION_CHIPS = [
  { icon: ListTodo, labelKey: 'home.chipWhatsOnMyPlate' },
  { icon: TrendingUp, labelKey: 'home.chipSummarizeWeek' },
  { icon: AlertCircle, labelKey: 'home.chipOverdueTasks' },
  { icon: Lightbulb, labelKey: 'home.chipSuggestActions' },
] as const;

function getTimeGreeting(t: (key: string) => string): string {
  const hour = new Date().getHours();
  if (hour < 12) return t('home.goodMorning');
  if (hour < 17) return t('home.goodAfternoon');
  return t('home.goodEvening');
}

/* Entrance animation: staggered fade-up */
const stagger = {
  hidden: {},
  show: { transition: { staggerChildren: 0.08 } },
};

const fadeUp = {
  hidden: { opacity: 0, y: 16 },
  show: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.45, ease: [0.25, 0.1, 0.25, 1] as const },
  },
};

/**
 * Module-level brief state — survives component remounts (navigation away and back).
 * `content` is the text to display immediately on remount without waiting for the query.
 * `streamFired` prevents re-streaming across remounts.
 */
const briefModule = {
  content: null as string | null,
  streamFired: false,
};

/** Cached AI response minHeight — survives component remounts (navigation). */
let aiMinHeightCache: number | null = null;

interface AIChatPanelProps {
  isHomePage?: boolean;
  actionsRef?: React.MutableRefObject<{ clear: () => void } | null>;
  onHasMessagesChange?: (has: boolean) => void;
}

export function AIChatPanel({
  isHomePage,
  actionsRef,
  onHasMessagesChange,
}: AIChatPanelProps) {
  const { t } = useTranslation();
  const utils = trpc.useUtils();
  const taskBriefing = useTaskBriefing();
  const authStatusQuery = trpc.auth.status.useQuery();

  // Home-specific queries
  const profile = authStatusQuery.data?.profile;
  const dueTodayQuery = trpc.tasks.dueToday.useQuery(undefined, { enabled: !!isHomePage });

  const chatContext = useMemo<UIChatContext>(
    () => ({ type: 'global' as const }),
    [],
  );
  const {
    messages,
    isStreaming,
    streamingContent,
    handleSend: chatHandleSend,
    clearMessages,
    cacheKey,
  } = useAIChat(chatContext);
  const hasMessages = messages.length > 0 || isStreaming;

  // Notify parent when conversation active state changes
  useEffect(() => {
    onHasMessagesChange?.(hasMessages);
  }, [hasMessages, onHasMessagesChange]);

  // Daily brief state — initialized from module-level cache so remounts are instant.
  const [dailyBrief, setDailyBrief] = useState<string | null>(() => briefModule.content);
  const briefContentRef = useRef('');

  const [briefExpanded, setBriefExpanded] = useState(false);
  const [briefDismissed, setBriefDismissed] = useState(false);

  // --- Scroll-to-user-message + AI response minHeight ---
  const chatInputWrapperRef = useRef<HTMLDivElement | null>(null);
  const inputRef = useRef<ChatInputBoxHandle>(null);
  const briefWrapper = useRef<HTMLDivElement | null>(null);
  const lastUserMsgRef = useRef<HTMLDivElement | null>(null);
  const lastAiRef = useRef<HTMLDivElement | null>(null);

  const [aiMinHeight, setAiMinHeight] = useState<number | null>(
    () => aiMinHeightCache,
  );

  // Expose clear action to parent (AppShell) via ref — placed after setAiMinHeight is in scope
  if (actionsRef) {
    actionsRef.current = {
      clear: () => {
        inputRef.current?.clear();
        clearMessages();
        aiMinHeightCache = null;
        setAiMinHeight(null);
      },
    };
  }

  const pendingScrollRef = useRef(false);

  // Stable layout values — recomputed only on mount and window resize.
  const stableLayout = useRef({ messageGap: 0 });
  useEffect(() => {
    function measureStable() {
      const rootStyles = window.getComputedStyle(document.documentElement);
      const spacingNumber = parseFloat(rootStyles.getPropertyValue('--spacing').trim()) || 0;
      const rootFontSize = parseFloat(rootStyles.fontSize) || 16;
      stableLayout.current = {
        messageGap: spacingNumber * 8 * rootFontSize,
      };
    }
    measureStable();
    window.addEventListener('resize', measureStable);
    return () => window.removeEventListener('resize', measureStable);
  }, []);

  const briefMutation = trpc.ai.dailyBrief.useMutation();
  const briefLoading = briefMutation.isPending;

  // Fetch cached brief from main process — null means stale/missing (needs regen).
  const cachedBriefQuery = trpc.ai.getBrief.useQuery(undefined, {
    enabled: !!isHomePage && !!authStatusQuery.data?.authenticated,
    staleTime: Infinity,
    refetchOnWindowFocus: false,
  });

  // When user sends a message: compute how much vertical space remains below the
  // user bubble → that becomes the minHeight of the AI response div. Then scroll.
  useEffect(() => {
    if (!pendingScrollRef.current) return;
    const lastMsg = messages[messages.length - 1];
    if (!lastMsg || lastMsg.role !== 'user') return;

    pendingScrollRef.current = false;

    const { messageGap } = stableLayout.current;
    const vhw = window.innerHeight;
    const briefHeight = briefWrapper.current?.getBoundingClientRect().height || 0;
    const userMsgHeight = lastUserMsgRef.current?.getBoundingClientRect().height || 0;
    
    const minH = Math.max(0, Math.round(vhw - (briefHeight + userMsgHeight + messageGap)));

    aiMinHeightCache = minH;
    setAiMinHeight(minH);

    // Auto-scroll — the only auto-scroll point in the entire flow
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        lastUserMsgRef.current?.scrollIntoView({ block: 'start', behavior: 'smooth' });
      });
    });
  }, [messages]);

  // Sync query result into module cache so remounts get it instantly.
  useEffect(() => {
    const cached = cachedBriefQuery.data;
    if (!cached) return;
    briefModule.content = cached;
    setDailyBrief(cached);
  }, [cachedBriefQuery.data]);

  // Listen for background brief refreshes pushed by the main process.
  useEffect(() => {
    if (!isHomePage) return;

    const unsubscribe = window.electronAI.onBriefUpdated((content) => {
      briefModule.content = content;
      briefModule.streamFired = true;
      setDailyBrief(content);
      void utils.ai.getBrief.invalidate();
    });

    return () => {
      unsubscribe?.();
    };
  }, [isHomePage, utils]);

  // Stream a fresh brief when cache is empty (once per app session).
  useEffect(() => {
    if (!isHomePage || !authStatusQuery.data?.authenticated) return;
    // Already have content (from module cache or query) — nothing to do.
    if (briefModule.content) return;
    // Wait for query to finish before deciding whether to stream.
    if (cachedBriefQuery.isLoading) return;
    // Query returned content — handled by the sync effect above.
    if (cachedBriefQuery.data) return;
    // Guard against firing twice.
    if (briefModule.streamFired) return;
    briefModule.streamFired = true;
    briefContentRef.current = '';
    const requestId = crypto.randomUUID();

    let unsubscribe: (() => void) | null = window.electronAI.onStreamEvent((event) => {
      if (event.requestId !== requestId) return;

      if (event.type === 'stream_text') {
        briefContentRef.current += event.chunk;
        briefModule.content = briefContentRef.current;
        setDailyBrief(briefContentRef.current);
      } else if (event.type === 'stream_end') {
        const final = briefContentRef.current || null;
        briefModule.content = final;
        setDailyBrief(final);
        unsubscribe?.();
        unsubscribe = null;
        // Invalidate so the persisted cache is loaded on next app launch / reload.
        void utils.ai.getBrief.invalidate();
      }
    });

    briefMutation.mutate({ requestId }, {
      onError: () => {
        unsubscribe?.();
        unsubscribe = null;
        briefModule.streamFired = false; // allow retry on error
      },
    });

    return () => {
      unsubscribe?.();
      unsubscribe = null;
    };
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isHomePage, authStatusQuery.data?.authenticated, cachedBriefQuery.isLoading, cachedBriefQuery.data]);

  const handleSend = useCallback((message: string) => {
    pendingScrollRef.current = true;
    chatHandleSend(message);
  }, [chatHandleSend]);

  // Derived values for home page
  const dueCount = dueTodayQuery.data?.length ?? 0;

  // When briefing is open on home page, show inline briefing section
  if (isHomePage && taskBriefing.isOpen) {
    return (
      <div className="absolute inset-0 z-0">
        <AnimatePresence mode="wait">
          <TaskBriefingOverlay key="briefing" />
        </AnimatePresence>
      </div>
    );
  }

  return (
    <div className="absolute inset-0 z-0 flex flex-col">
      {/* Scrollable messages area */}
      <div className="relative flex-1 min-h-0">
        {/* Brief toast — floats over the scroll area so messages scroll underneath */}
        <AnimatePresence>
          {isHomePage && hasMessages && dailyBrief && !briefDismissed && (
            <motion.div
              ref={briefWrapper}
              initial={{ y: -80, opacity: 0 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{ y: -80, opacity: 0 }}
              transition={{ type: 'spring', stiffness: 400, damping: 30 }}
              className="absolute top-0 left-0 right-0 z-30 flex justify-center px-4 pt-3 pb-1"
            >
              <div className="w-full max-w-2xl rounded-xl border border-border/60 bg-background/60 backdrop-blur-md shadow-[0_8px_30px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.4)] ring-1 ring-border/10">
                {/* Toast header — always visible */}
                <div className="flex items-center gap-2 px-4 py-2.5">
                  <Sparkles size={14} className="text-primary shrink-0" />
                  <span className="text-xs font-semibold tracking-wide text-foreground">{t('home.dailyBrief')}</span>
                  <div className="flex-1" />
                  <button
                    onClick={() => setBriefExpanded((v) => !v)}
                    aria-label={briefExpanded ? 'Collapse brief' : 'Expand brief'}
                    className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
                  >
                    {briefExpanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
                  </button>
                  <button
                    onClick={() => setBriefDismissed(true)}
                    aria-label="Dismiss brief"
                    className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:text-foreground hover:bg-muted/60"
                  >
                    <X size={14} />
                  </button>
                </div>
                {/* Collapsed: one-line preview */}
                {!briefExpanded && (
                  <div className="px-4 pb-3 -mt-1">
                    <p className="text-xs text-muted-foreground truncate">
                      {dailyBrief.replace(/[#*_~`>-]/g, '').slice(0, 120)}...
                    </p>
                  </div>
                )}
                {/* Expanded: full brief content */}
                <AnimatePresence>
                  {briefExpanded && (
                    <motion.div
                      initial={{ height: 0, opacity: 0 }}
                      animate={{ height: 'auto', opacity: 1 }}
                      exit={{ height: 0, opacity: 0 }}
                      transition={{ duration: 0.25, ease: [0.25, 0.1, 0.25, 1] }}
                      className="overflow-hidden"
                    >
                      <div className="px-4 pb-3 max-h-64 overflow-y-auto">
                        <MessageContent content={dailyBrief} />
                      </div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            </motion.div>
          )}
        </AnimatePresence>
        {/* Gradual blur at the bottom of messages */}
        {hasMessages && (
          <GradualBlur
            position="bottom"
            strength={0.6}
            height="4.5rem"
            divCount={10}
            curve="ease-out"
            opacity={0.8}
            zIndex={20}
          />
        )}
        <ScrollArea
          className="h-full"
          scrollbarClassName={hasMessages ? 'z-30' : undefined}
          viewportClassName={
            isHomePage && !hasMessages
              ? '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-start'
              : '[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-end'
          }
        >
        {/* Home page initial state: greeting + brief */}
        {isHomePage && !hasMessages && (
          <motion.div
            className="mx-auto w-full max-w-4xl px-8 pt-35 pb-8"
            variants={stagger}
            initial="hidden"
            animate="show"
          >
            <div className="flex flex-col" style={{ gap: 'clamp(2.5rem, 4vh, 4rem)' }}>
              {/* Greeting — editorial hero moment */}
              <motion.div variants={fadeUp} className="flex flex-col gap-1">
                <span
                  className="font-light tracking-wide text-muted-foreground"
                  style={{ fontSize: 'clamp(1rem, 1.6vw, 1.25rem)' }}
                >
                  {getTimeGreeting(t)}
                </span>
                <h1
                  className="font-bold leading-[1.05]"
                  style={{ fontSize: 'clamp(3.25rem, 5.5vw, 5.5rem)', letterSpacing: '-0.035em' }}
                >
                  {profile?.name}
                  <span className="text-primary ml-3 inline-block">✦</span>
                </h1>
                {dueCount > 0 && (
                  <p
                    className="text-muted-foreground mt-2"
                    style={{ fontSize: 'clamp(0.875rem, 1.2vw, 1.125rem)' }}
                  >
                    {t('home.tasksDueToday', { count: dueCount })}
                  </p>
                )}
              </motion.div>

              {/* Daily brief */}
              <motion.div variants={fadeUp} className="max-w-3xl">
                {authStatusQuery.data?.authenticated === false ? (
                  <div className="flex flex-col items-start gap-3 py-2">
                    <LogIn size={20} className="text-muted-foreground" />
                    <p className="text-muted-foreground" style={{ fontSize: 'clamp(0.9375rem, 1.2vw, 1.0625rem)' }}>
                      {t('home.loginPrompt')}
                    </p>
                    <Button variant="outline" size="sm" asChild>
                      <Link to="/settings">{t('home.openSettings')}</Link>
                    </Button>
                  </div>
                ) : briefLoading && !dailyBrief ? (
                  <div className="space-y-3">
                    <Skeleton className="h-5 w-3/4" />
                    <Skeleton className="h-5 w-1/2" />
                    <Skeleton className="h-5 w-2/3" />
                  </div>
                ) : dailyBrief ? (
                  <MessageContent content={dailyBrief} />
                ) : null}
                {authStatusQuery.data?.authenticated !== false && (
                  <button
                    type="button"
                    onClick={() => taskBriefing.open()}
                    className="mt-3 text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline transition-colors"
                  >
                    {t('home.openTaskBriefing')}
                  </button>
                )}
              </motion.div>

              {/* Input + suggestion links */}
              <motion.div variants={fadeUp} className="max-w-3xl">
                <div className="relative rounded-2xl bg-background/70 backdrop-blur-xl border border-border/50 shadow-lg ring-1 ring-border/20 transition-shadow focus-within:shadow-xl focus-within:border-ring/50">
                  <ChatInputBox
                    ref={inputRef}
                    cacheKey={cacheKey}
                    isStreaming={isStreaming}
                    onSend={handleSend}
                    placeholder={t('home.askAnything')}
                    autoFocus
                  />
                </div>
                <div className="flex flex-col gap-0.5 mt-5">
                  {SUGGESTION_CHIPS.map((chip) => {
                    const label = t(chip.labelKey);
                    return (
                    <button
                      key={chip.labelKey}
                      type="button"
                      className="group flex items-center gap-3 py-1.5 text-muted-foreground transition-all duration-200 hover:text-foreground hover:translate-x-1 cursor-pointer text-left"
                      style={{ fontSize: 'clamp(0.9375rem, 1.2vw, 1.0625rem)' }}
                      onClick={() => { inputRef.current?.setValue(label); inputRef.current?.focus(); }}
                    >
                      <chip.icon
                        size={16}
                        className="shrink-0 transition-colors duration-200 group-hover:text-primary"
                      />
                      <span>{label}</span>
                    </button>
                    );
                  })}
                </div>
              </motion.div>
            </div>
          </motion.div>
        )}

        {/* Home page with messages: brief stays, then messages */}
        {isHomePage && hasMessages && (
          <div className="mx-auto w-full max-w-6xl px-6 pt-8">
            <div className="flex flex-col gap-4">
              <div
                aria-hidden
                style={{
                  height: '4vw',
                  flexShrink: 0,
                }}
              />

              {/* Chat messages */}
              {messages.map((msg, idx) => {
                const isLast = idx === messages.length - 1;
                // The last user message gets a ref for scroll targeting.
                // The last assistant message (when not streaming) gets the
                // minHeight so it fills remaining viewport space.
                const isLastUser = isLast && msg.role === 'user';
                const isLastAssistant = isLast && msg.role === 'assistant' && !isStreaming;

                if (msg.role === 'user') {
                  return (
                    <div
                      key={msg.id}
                      ref={isLastUser ? lastUserMsgRef : undefined}
                      className="flex justify-end"
                    >
                      <div className="ml-auto max-w-[75%] rounded-2xl bg-muted px-4 py-2">
                        <ChatMarkdown content={msg.content} />
                      </div>
                    </div>
                  );
                }

                if (msg.error) {
                  return (
                    <div key={msg.id} className="mr-auto max-w-[75%]">
                      <p className="text-destructive whitespace-pre-wrap">
                        {msg.content}
                      </p>
                    </div>
                  );
                }

                return (
                  <AIMessage
                    key={msg.id}
                    ref={isLastAssistant ? lastAiRef : undefined}
                    content={msg.content}
                    bottomPad={isLastAssistant}
                    minHeight={isLastAssistant ? aiMinHeight : undefined}
                  />
                );
              })}

              {/* Streaming AI response — minHeight fills remaining viewport space */}
              {isStreaming && (
                <AIMessage
                  ref={lastAiRef}
                  content={streamingContent}
                  bottomPad
                  minHeight={aiMinHeight}
                  skeleton={!streamingContent}
                />
              )}
            </div>
          </div>
        )}

        {/* Non-home messages */}
        </ScrollArea>
      </div>

      {/* Fixed input — pinned to the bottom, above the blur */}
      {hasMessages && (
        <div ref={chatInputWrapperRef} className="absolute bottom-0 left-0 right-0 z-30 px-6 pb-5 pt-4 pointer-events-none">
          <div className="relative pointer-events-auto mx-auto max-w-3xl">
            <div className="relative rounded-2xl bg-background/70 backdrop-blur-xl border border-border/50 shadow-lg ring-1 ring-border/20 transition-shadow focus-within:shadow-xl focus-within:border-ring/50">
              <ChatInputBox
                ref={inputRef}
                cacheKey={cacheKey}
                isStreaming={isStreaming}
                onSend={handleSend}
                placeholder={t('home.askAnything')}
              />
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* ---------- AIMessage: shared layout for completed + streaming AI turns ---------- */

interface AIMessageProps {
  content: string;
  bottomPad?: boolean;
  minHeight?: number | null;
  skeleton?: boolean;
}

const AIMessage = memo(forwardRef<HTMLDivElement, AIMessageProps>(
  ({ content, bottomPad, minHeight, skeleton }, ref) => (
    <div
      ref={ref}
      className={`mr-auto ${hasInlineTags(content) ? 'w-full' : 'max-w-[75%]'}`}
      style={minHeight ? { minHeight } : undefined}
    >
      <div className="flex items-end gap-2.5 mb-1">
        <Sparkles size={24} className="text-foreground" />
        <span className="text-xl font-semibold leading-none">adiuv<span className="font-bold text-primary">AI</span></span>
      </div>
      {skeleton ? (
        <div className="space-y-2 pl-[32px] pb-40">
          <Skeleton className="h-4 w-48" />
          <Skeleton className="h-4 w-32" />
        </div>
      ) : (
        <div className={`pl-[32px] flex flex-col gap-3${bottomPad ? ' pb-40' : ''}`}>
          <MessageContent content={content} />
        </div>
      )}
    </div>
  )
));
AIMessage.displayName = 'AIMessage';

/* ---------- MessageContent: text with inline entity blocks ---------- */

const MessageContent = memo(function MessageContent({ content, fontSize }: { content: string; fontSize?: string }) {
  const segments = useMemo(
    () => mergeConsecutiveTaskSegments(mergeTimelineSegments(parseInlineTags(content))),
    [content],
  );

  // Fast path: no inline tags, just render markdown
  if (segments.length === 1 && segments[0]?.type === 'text') {
    return <ChatMarkdown content={content} fontSize={fontSize} />;
  }

  // No content at all
  if (segments.length === 0) return null;

  return (
    <div className="flex flex-col gap-3">
      {segments.map((seg, i) => {
        if (seg.type === 'text') {
          return <ChatMarkdown key={i} content={seg.content} fontSize={fontSize} />;
        }
        if (seg.type === 'chart') {
          return (
            <motion.div key={i} {...blockAnimation}>
              <ChatChartBlock data={seg.data} />
            </motion.div>
          );
        }
        return (
          <motion.div key={i} {...blockAnimation}>
            <ChatEntityBlock data={{ entity: seg.entity, ids: seg.ids }} />
          </motion.div>
        );
      })}
    </div>
  );
});

const blockAnimation = {
  initial: { opacity: 0, scale: 0.95 },
  animate: { opacity: 1, scale: 1 },
  transition: { type: 'spring' as const, stiffness: 400, damping: 30 },
};

/* ---------- ChatMarkdown: lightweight markdown renderer ---------- */

// Stable references — defined outside the component so react-markdown never
// sees a changed prop reference and re-parses content on every render.
const REMARK_PLUGINS: Parameters<typeof ReactMarkdown>[0]['remarkPlugins'] = [remarkGfm];
const MARKDOWN_COMPONENTS: Parameters<typeof ReactMarkdown>[0]['components'] = {
  pre: ({ children }) => (
    <pre className="bg-muted rounded-lg p-3 overflow-x-auto text-xs">
      {children}
    </pre>
  ),
  code: ({ children, className }: { children?: React.ReactNode; className?: string }) => {
    if (!className) {
      return (
        <code className="bg-muted rounded px-1.5 py-0.5 text-xs font-mono">
          {children}
        </code>
      );
    }
    return <code className={className}>{children}</code>;
  },
};

export function ChatMarkdown({ content, size = 'sm', fontSize }: { content: string; size?: 'sm' | 'lg'; fontSize?: string }) {
  return (
    <div
      className={`prose dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 ${size === 'lg' ? 'prose-base' : 'prose-sm'}`}
      style={fontSize ? { fontSize } : undefined}
    >
      <ReactMarkdown
        remarkPlugins={REMARK_PLUGINS}
        components={MARKDOWN_COMPONENTS}
      >
        {content}
      </ReactMarkdown>
    </div>
  );
}
