import { type JSX, useCallback, useEffect, useMemo, useRef, useState } from 'react';

import { isNearBottom, useAutoScrollBottom } from '@renderer/hooks/useAutoScrollBottom';
import { useTabNavigationController } from '@renderer/hooks/useTabNavigationController';
import { useTabUI } from '@renderer/hooks/useTabUI';
import { useVisibleAIGroup } from '@renderer/hooks/useVisibleAIGroup';
import { useStore } from '@renderer/store';
import { useVirtualizer } from '@tanstack/react-virtual';
import { ChevronRight, ChevronsDown, Users } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';

import { SessionContextPanel } from './SessionContextPanel/index';

/** Pixels from bottom considered "near bottom" for scroll-button visibility and auto-scroll. */
const SCROLL_THRESHOLD = 300;
const CHAT_ITEMS_PAGE_SIZE = 80;

import { computeRemainingContext, sumContextInjectionTokens } from '@renderer/utils/contextMath';
import { deriveContextMetrics } from '@shared/utils/contextMetrics';

import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
import { ChatHistoryItem } from './ChatHistoryItem';
import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';

import type { ContextInjection } from '@renderer/types/contextInjection';
import type { ContextUsageLike } from '@shared/utils/contextMetrics';

/**
 * Waits for two requestAnimationFrame cycles, allowing the virtualizer to render.
 */
function waitForDoubleRaf(): Promise<void> {
  return new Promise((resolve) =>
    requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
  );
}

interface ChatHistoryProps {
  /** Tab ID for per-tab state isolation (scroll position, deep links) */
  tabId?: string;
}

export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
  const VIRTUALIZATION_THRESHOLD = 120;
  const ESTIMATED_CHAT_ITEM_HEIGHT = 260;

  // Per-tab UI state (context panel, scroll position, expansion) from useTabUI
  const {
    isContextPanelVisible,
    setContextPanelVisible,
    savedScrollTop,
    saveScrollPosition,
    expandAIGroup,
    expandSubagentTrace,
    selectedContextPhase,
    setSelectedContextPhase,
  } = useTabUI();

  // Global store subscriptions (shared data)
  const {
    searchQuery,
    currentSearchIndex,
    searchMatches,
    openTabs,
    activeTabId,
    consumeTabNavigation,
    setSearchQuery,
    syncSearchMatchesWithRendered,
    selectSearchMatch,
    setTabVisibleAIGroup,
    openTeamTab,
    openSessionReport,
  } = useStore(
    useShallow((s) => ({
      searchQuery: s.searchQuery,
      currentSearchIndex: s.currentSearchIndex,
      searchMatches: s.searchMatches,
      openTabs: s.openTabs,
      activeTabId: s.activeTabId,
      consumeTabNavigation: s.consumeTabNavigation,
      setSearchQuery: s.setSearchQuery,
      syncSearchMatchesWithRendered: s.syncSearchMatchesWithRendered,
      selectSearchMatch: s.selectSearchMatch,
      setTabVisibleAIGroup: s.setTabVisibleAIGroup,
      openTeamTab: s.openTeamTab,
      openSessionReport: s.openSessionReport,
    }))
  );

  // Per-tab session data (each tab renders its own session independently)
  const tabData = useStore(
    useShallow((s) => {
      const td = tabId ? s.tabSessionData[tabId] : null;
      return {
        conversation: td?.conversation ?? s.conversation,
        conversationLoading: td?.conversationLoading ?? s.conversationLoading,
        sessionContextStats: td?.sessionContextStats ?? s.sessionContextStats,
        sessionPhaseInfo: td?.sessionPhaseInfo ?? s.sessionPhaseInfo,
        sessionDetail: td?.sessionDetail ?? s.sessionDetail,
      };
    })
  );
  const {
    conversation,
    conversationLoading,
    sessionContextStats,
    sessionPhaseInfo,
    sessionDetail,
  } = tabData;

  // Compute combined subagent cost from process metrics
  const subagentCostUsd = useMemo(() => {
    const processes = sessionDetail?.processes;
    if (!processes || processes.length === 0) return undefined;
    const total = processes.reduce((sum, p) => sum + (p.metrics.costUsd ?? 0), 0);
    return total > 0 ? total : undefined;
  }, [sessionDetail?.processes]);

  // State for Context button hover (local state OK - doesn't need per-tab isolation)
  const [isContextButtonHovered, setIsContextButtonHovered] = useState(false);

  // Determine if this tab instance is currently active
  // Use tabId prop if provided, otherwise fall back to activeTabId (for backwards compatibility)
  const effectiveTabId = tabId ?? activeTabId;
  const isThisTabActive = effectiveTabId === activeTabId;

  // Get THIS tab's pending navigation request
  const thisTab = effectiveTabId ? openTabs.find((t) => t.id === effectiveTabId) : null;
  const pendingNavigation = thisTab?.pendingNavigation;

  const teamBySessionId = useStore(useShallow((s) => s.teamBySessionId));
  const leadContextByTeam = useStore(useShallow((s) => s.leadContextByTeam));

  // Look up whether this session belongs to a team
  const sessionTeam = useMemo(() => {
    const sid = sessionDetail?.session?.id;
    if (!sid) return null;
    return teamBySessionId[sid] ?? null;
  }, [teamBySessionId, sessionDetail?.session?.id]);

  // Compute all accumulated context injections (phase-aware)
  const { allContextInjections, lastAssistantUsage, lastAssistantModelName } = useMemo(() => {
    if (!sessionContextStats || !conversation?.items.length) {
      return {
        allContextInjections: [] as ContextInjection[],
        lastAssistantUsage: null as ContextUsageLike | null,
        lastAssistantModelName: undefined as string | undefined,
      };
    }

    // Determine which phase to show
    const effectivePhase = selectedContextPhase;

    // If a specific phase is selected, find the last AI group in that phase
    let targetAiGroupId: string | undefined;
    if (effectivePhase !== null && sessionPhaseInfo) {
      const phase = sessionPhaseInfo.phases.find((p) => p.phaseNumber === effectivePhase);
      if (phase) {
        targetAiGroupId = phase.lastAIGroupId;
      }
    }

    // Default: use the last AI group overall
    if (!targetAiGroupId) {
      const lastAiItem = [...conversation.items].reverse().find((item) => item.type === 'ai');
      if (lastAiItem?.type !== 'ai') {
        return {
          allContextInjections: [] as ContextInjection[],
          lastAssistantUsage: null,
          lastAssistantModelName: undefined,
        };
      }
      targetAiGroupId = lastAiItem.group.id;
    }

    const stats = sessionContextStats.get(targetAiGroupId);
    const injections = stats?.accumulatedInjections ?? [];

    let lastUsage: ContextUsageLike | null = null;
    let lastModelName: string | undefined;
    const targetItem = conversation.items.find(
      (item) => item.type === 'ai' && item.group.id === targetAiGroupId
    );
    if (targetItem?.type === 'ai') {
      const responses = targetItem.group.responses || [];
      for (let i = responses.length - 1; i >= 0; i--) {
        const msg = responses[i];
        if (msg.type === 'assistant' && msg.usage) {
          lastUsage = msg.usage;
          lastModelName = msg.model;
          break;
        }
      }
    }

    return {
      allContextInjections: injections,
      lastAssistantUsage: lastUsage,
      lastAssistantModelName: lastModelName,
    };
  }, [sessionContextStats, conversation, selectedContextPhase, sessionPhaseInfo]);
  const visibleContextTokens = useMemo(
    () => sumContextInjectionTokens(allContextInjections),
    [allContextInjections]
  );
  const sessionLeadContext = sessionTeam ? (leadContextByTeam[sessionTeam.teamName] ?? null) : null;
  const contextMetrics = useMemo(
    () =>
      deriveContextMetrics({
        usage: lastAssistantUsage,
        modelName: lastAssistantModelName,
        contextWindowTokens: sessionLeadContext?.contextWindowTokens ?? null,
        visibleContextTokens,
      }),
    [
      lastAssistantModelName,
      lastAssistantUsage,
      sessionLeadContext?.contextWindowTokens,
      visibleContextTokens,
    ]
  );
  const contextUsedPercentLabel = useMemo(() => {
    const percent = contextMetrics.contextUsedPercentOfContextWindow;
    return percent === null ? null : `${percent.toFixed(1)}%`;
  }, [contextMetrics.contextUsedPercentOfContextWindow]);

  const remainingContext = useMemo(
    () =>
      computeRemainingContext(
        contextMetrics.contextUsedTokens ?? undefined,
        contextMetrics.contextWindowTokens ?? undefined
      ),
    [contextMetrics.contextUsedTokens, contextMetrics.contextWindowTokens]
  );

  // State for navigation highlight (blue, used for Turn navigation from CLAUDE.md panel)
  const [isNavigationHighlight, setIsNavigationHighlight] = useState(false);
  const [pageFromLatest, setPageFromLatest] = useState(0);
  const navigationHighlightTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Refs map for AI groups, chat items, and individual tool items (for scrolling)
  const aiGroupRefs = useRef<Map<string, HTMLElement>>(new Map());
  const chatItemRefs = useRef<Map<string, HTMLElement>>(new Map());
  const toolItemRefs = useRef<Map<string, HTMLElement>>(new Map());

  // Shared scroll container ref - used by both auto-scroll and navigation coordinator
  const scrollContainerRef = useRef<HTMLDivElement>(null);

  const isSearchActive = searchQuery.trim().length > 0;
  const fullConversationItems = conversation?.items ?? [];
  const forceFullConversation =
    isSearchActive || pendingNavigation != null || selectedContextPhase !== null;
  const totalConversationPages = Math.max(
    1,
    Math.ceil(fullConversationItems.length / CHAT_ITEMS_PAGE_SIZE)
  );
  const normalizedPageFromLatest = Math.min(pageFromLatest, totalConversationPages - 1);
  const displayedConversationItems = useMemo(() => {
    if (forceFullConversation || fullConversationItems.length <= CHAT_ITEMS_PAGE_SIZE) {
      return fullConversationItems;
    }
    const end = fullConversationItems.length - normalizedPageFromLatest * CHAT_ITEMS_PAGE_SIZE;
    const start = Math.max(0, end - CHAT_ITEMS_PAGE_SIZE);
    return fullConversationItems.slice(start, end);
  }, [forceFullConversation, fullConversationItems, normalizedPageFromLatest]);
  const shouldShowConversationPagination =
    !forceFullConversation && fullConversationItems.length > CHAT_ITEMS_PAGE_SIZE;
  const currentConversationPage = normalizedPageFromLatest + 1;
  const shouldVirtualize = displayedConversationItems.length >= VIRTUALIZATION_THRESHOLD;
  const emptyRenderedSyncCountRef = useRef(0);

  useEffect(() => {
    setPageFromLatest(0);
  }, [sessionDetail?.session?.id]);

  useEffect(() => {
    if (pageFromLatest > totalConversationPages - 1) {
      setPageFromLatest(totalConversationPages - 1);
    }
  }, [pageFromLatest, totalConversationPages]);

  const goToConversationPage = useCallback(
    (nextPageFromLatest: number): void => {
      setPageFromLatest(Math.max(0, Math.min(nextPageFromLatest, totalConversationPages - 1)));
      requestAnimationFrame(() => {
        if (scrollContainerRef.current) {
          scrollContainerRef.current.scrollTop = 0;
        }
      });
    },
    [totalConversationPages]
  );

  const setSearchQueryForTab = useCallback(
    (query: string): void => {
      setSearchQuery(query, conversation);
    },
    [setSearchQuery, conversation]
  );

  const groupIndexMap = useMemo(() => {
    const map = new Map<string, number>();
    if (!displayedConversationItems) {
      return map;
    }
    displayedConversationItems.forEach((item, index) => {
      map.set(item.group.id, index);
    });
    return map;
  }, [displayedConversationItems]);

  // --- New-item animation tracking ---
  const knownGroupIdsRef = useRef<Set<string>>(new Set());
  const animatedGroupIdsRef = useRef<Set<string>>(new Set());
  const isInitialRenderRef = useRef(true);
  const prevTabIdRef = useRef(effectiveTabId);

  // Reset animation tracking when switching tabs/sessions
  if (prevTabIdRef.current !== effectiveTabId) {
    prevTabIdRef.current = effectiveTabId;
    knownGroupIdsRef.current.clear();
    animatedGroupIdsRef.current.clear();
    isInitialRenderRef.current = true;
  }

  const newGroupIds = useMemo(() => {
    const items = displayedConversationItems;
    if (!items || items.length === 0) {
      knownGroupIdsRef.current.clear();
      animatedGroupIdsRef.current.clear();
      isInitialRenderRef.current = true;
      return new Set<string>();
    }

    // First render: seed all known IDs, no animations
    if (isInitialRenderRef.current) {
      isInitialRenderRef.current = false;
      for (const item of items) {
        knownGroupIdsRef.current.add(item.group.id);
      }
      return new Set<string>();
    }

    // Subsequent updates: detect new items
    const newIds = new Set<string>();
    for (const item of items) {
      if (!knownGroupIdsRef.current.has(item.group.id)) {
        newIds.add(item.group.id);
        knownGroupIdsRef.current.add(item.group.id);
      }
    }
    return newIds;
  }, [displayedConversationItems]);

  // Expire animation flags after the CSS animation completes (350ms + buffer).
  // This prevents replay when the virtualizer remounts off-screen elements.
  useEffect(() => {
    if (newGroupIds.size === 0) return;
    const timer = setTimeout(() => {
      for (const id of newGroupIds) {
        animatedGroupIdsRef.current.add(id);
      }
    }, 400);
    return () => clearTimeout(timer);
  }, [newGroupIds]);

  const rowVirtualizer = useVirtualizer({
    count: shouldVirtualize ? displayedConversationItems.length : 0,
    getScrollElement: () => scrollContainerRef.current,
    estimateSize: () => ESTIMATED_CHAT_ITEM_HEIGHT,
    overscan: 8,
    measureElement: (element) => element.getBoundingClientRect().height,
  });

  const ensureGroupVisible = useCallback(
    async (groupId: string) => {
      if (!shouldVirtualize) {
        return;
      }
      const index = groupIndexMap.get(groupId);
      if (index === undefined) {
        return;
      }
      rowVirtualizer.scrollToIndex(index, { align: 'center' });
      // Wait 2 RAF frames so the virtualizer has time to render the target row
      await waitForDoubleRaf();
    },
    [groupIndexMap, rowVirtualizer, shouldVirtualize]
  );

  // Sticky context button height (py-3 = 12px padding * 2 + button height ~28px + pt-3 = 12px)
  // Total: approximately 52px, round up to 60px for safety
  const STICKY_BUTTON_OFFSET = allContextInjections.length > 0 ? 60 : 0;

  // Unified navigation controller - replaces useNavigationCoordinator + useSearchContextNavigation
  // Must be created before useAutoScrollBottom so we can pass shouldDisableAutoScroll
  const {
    highlightedGroupId,
    setHighlightedGroupId,
    highlightToolUseId: controllerToolUseId,
    isSearchHighlight,
    highlightColor,
    shouldDisableAutoScroll,
  } = useTabNavigationController({
    isActiveTab: isThisTabActive,
    pendingNavigation,
    conversation,
    conversationLoading,
    consumeTabNavigation,
    tabId: effectiveTabId ?? '',
    aiGroupRefs,
    chatItemRefs,
    toolItemRefs,
    expandAIGroup,
    expandSubagentTrace,
    scrollContainerRef,
    stickyOffset: STICKY_BUTTON_OFFSET,
    ensureGroupVisible,
    setSearchQuery: setSearchQueryForTab,
    selectSearchMatch,
  });

  // Local tool highlight for context panel navigation (separate from controller)
  const [contextNavToolUseId, setContextNavToolUseId] = useState<string | null>(null);
  const effectiveHighlightToolUseId = controllerToolUseId ?? contextNavToolUseId ?? undefined;
  // Use blue for context panel tool navigation, otherwise use controller's color
  const effectiveHighlightColor = contextNavToolUseId ? ('blue' as const) : highlightColor;

  // Keep search match indices aligned with this tab's rendered conversation.
  // This avoids stale/global match lists after tab switches or in-place refreshes.
  useEffect(() => {
    if (!isThisTabActive || !searchQuery.trim()) {
      return;
    }
    setSearchQuery(searchQuery, conversation);
  }, [isThisTabActive, searchQuery, conversation, setSearchQuery]);

  // Canonicalize matches from rendered mark elements (DOM order).
  // This guarantees that nth navigation follows the exact nth visible highlight.
  // Skip when virtualizing: only a subset of items are rendered, so DOM-based sync
  // would produce an incomplete match list. The store-level matches are already correct.
  useEffect(() => {
    if (!isThisTabActive || !isSearchActive || !conversation || shouldVirtualize) {
      emptyRenderedSyncCountRef.current = 0;
      return;
    }

    let frameA = 0;
    let frameB = 0;
    let cancelled = false;

    const run = (): void => {
      const container = scrollContainerRef.current;
      if (!container || cancelled) return;

      const renderedMatches: { itemId: string; matchIndexInItem: number }[] = [];
      const marks = container.querySelectorAll<HTMLElement>(
        'mark[data-search-item-id][data-search-match-index]'
      );
      for (const mark of marks) {
        const itemId = mark.dataset.searchItemId;
        const matchIndexRaw = mark.dataset.searchMatchIndex;
        const matchIndex = matchIndexRaw !== undefined ? Number(matchIndexRaw) : Number.NaN;
        if (!itemId || !Number.isFinite(matchIndex)) continue;
        renderedMatches.push({ itemId, matchIndexInItem: matchIndex });
      }

      // Prevent transient "0 marks" snapshots during mount from wiping results.
      if (renderedMatches.length === 0 && searchMatches.length > 0) {
        emptyRenderedSyncCountRef.current += 1;
        if (emptyRenderedSyncCountRef.current < 3) {
          return;
        }
      } else {
        emptyRenderedSyncCountRef.current = 0;
      }

      syncSearchMatchesWithRendered(renderedMatches);
    };

    // Wait for highlight marks to be mounted and stabilized.
    frameA = requestAnimationFrame(() => {
      frameB = requestAnimationFrame(run);
    });

    return () => {
      cancelled = true;
      cancelAnimationFrame(frameA);
      cancelAnimationFrame(frameB);
    };
  }, [
    isThisTabActive,
    isSearchActive,
    shouldVirtualize,
    conversation,
    currentSearchIndex,
    searchMatches,
    syncSearchMatchesWithRendered,
  ]);

  // Track shouldDisableAutoScroll transitions for scroll restore coordination
  const prevShouldDisableRef = useRef(shouldDisableAutoScroll);

  const { registerAIGroupRef } = useVisibleAIGroup({
    onVisibleChange: (aiGroupId) => {
      if (effectiveTabId) {
        setTabVisibleAIGroup(effectiveTabId, aiGroupId);
      }
    },
    threshold: 0.5,
    rootRef: scrollContainerRef,
  });

  // Scroll-to-bottom button visibility
  const [showScrollButton, setShowScrollButton] = useState(false);

  const checkScrollButton = useCallback(() => {
    const container = scrollContainerRef.current;
    if (!container) return;
    const { scrollTop, scrollHeight, clientHeight } = container;
    setShowScrollButton(!isNearBottom(scrollTop, scrollHeight, clientHeight, SCROLL_THRESHOLD));
  }, []);

  // Auto-follow when conversation updates, but only if the user was already near bottom.
  // This preserves manual reading position when the user scrolls up.
  // Disabled during navigation to prevent conflicts with deep-link/search scrolling.
  const { scrollToBottom } = useAutoScrollBottom([conversation], {
    threshold: SCROLL_THRESHOLD,
    smoothDuration: 300,
    autoBehavior: 'auto',
    disabled: shouldDisableAutoScroll,
    externalRef: scrollContainerRef,
    resetKey: effectiveTabId,
  });

  // Re-check button visibility whenever conversation updates
  useEffect(() => {
    checkScrollButton();
  }, [displayedConversationItems, checkScrollButton]);

  // Callback to register AI group refs (combines with visibility hook)
  const registerAIGroupRefCombined = useCallback(
    (groupId: string) => {
      const visibilityRef = registerAIGroupRef(groupId);
      return (el: HTMLElement | null) => {
        if (typeof visibilityRef === 'function') visibilityRef(el);
        if (el) aiGroupRefs.current.set(groupId, el);
        else aiGroupRefs.current.delete(groupId);
      };
    },
    [registerAIGroupRef]
  );

  // Handler to navigate to a specific turn (AI group) from CLAUDE.md panel
  const handleNavigateToTurn = useCallback(
    (turnIndex: number) => {
      if (!conversation) return;
      const targetItem = conversation.items.find(
        (item) => item.type === 'ai' && item.group.turnIndex === turnIndex
      );
      if (targetItem?.type !== 'ai') return;

      const run = async (): Promise<void> => {
        const groupId = targetItem.group.id;
        await ensureGroupVisible(groupId);
        const element = aiGroupRefs.current.get(groupId);
        if (!element) return;

        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        setHighlightedGroupId(groupId);
        setIsNavigationHighlight(true);
        if (navigationHighlightTimerRef.current) {
          clearTimeout(navigationHighlightTimerRef.current);
        }
        navigationHighlightTimerRef.current = setTimeout(() => {
          setHighlightedGroupId(null);
          setIsNavigationHighlight(false);
          navigationHighlightTimerRef.current = null;
        }, 2000);
      };
      void run();
    },
    [conversation, ensureGroupVisible, setHighlightedGroupId]
  );

  // Handler to navigate to a user message group (preceding the AI group at turnIndex)
  const handleNavigateToUserGroup = useCallback(
    (turnIndex: number) => {
      if (!conversation) return;
      const aiItemIndex = conversation.items.findIndex(
        (item) => item.type === 'ai' && item.group.turnIndex === turnIndex
      );
      if (aiItemIndex < 0) return;

      // Find the user item preceding this AI group
      const prevItem = aiItemIndex > 0 ? conversation.items[aiItemIndex - 1] : null;
      if (prevItem?.type !== 'user') return;

      const run = async (): Promise<void> => {
        const groupId = prevItem.group.id;
        await ensureGroupVisible(groupId);
        const element = chatItemRefs.current.get(groupId);
        if (!element) return;

        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
        setHighlightedGroupId(groupId);
        setIsNavigationHighlight(true);
        if (navigationHighlightTimerRef.current) {
          clearTimeout(navigationHighlightTimerRef.current);
        }
        navigationHighlightTimerRef.current = setTimeout(() => {
          setHighlightedGroupId(null);
          setIsNavigationHighlight(false);
          navigationHighlightTimerRef.current = null;
        }, 2000);
      };
      void run();
    },
    [conversation, ensureGroupVisible, setHighlightedGroupId]
  );

  // Handler to navigate to a specific tool within a turn from context panel
  const handleNavigateToTool = useCallback(
    (turnIndex: number, toolUseId: string) => {
      if (!conversation) return;
      const targetItem = conversation.items.find(
        (item) => item.type === 'ai' && item.group.turnIndex === turnIndex
      );
      if (targetItem?.type !== 'ai') return;

      const run = async (): Promise<void> => {
        const groupId = targetItem.group.id;
        await ensureGroupVisible(groupId);

        // Set group + tool highlight immediately
        setHighlightedGroupId(groupId);
        setIsNavigationHighlight(true);
        setContextNavToolUseId(toolUseId);

        // Wait for tool element to appear in DOM (up to 500ms)
        let toolElement: HTMLElement | undefined;
        const startTime = Date.now();
        while (Date.now() - startTime < 500) {
          toolElement = toolItemRefs.current.get(toolUseId);
          if (toolElement) break;
          await new Promise((resolve) => setTimeout(resolve, 50));
        }

        // Scroll to tool element, or fall back to AI group
        const scrollTarget = toolElement ?? aiGroupRefs.current.get(groupId);
        if (scrollTarget) {
          scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }

        // Clear highlight after 2s
        if (navigationHighlightTimerRef.current) {
          clearTimeout(navigationHighlightTimerRef.current);
        }
        navigationHighlightTimerRef.current = setTimeout(() => {
          setHighlightedGroupId(null);
          setIsNavigationHighlight(false);
          setContextNavToolUseId(null);
          navigationHighlightTimerRef.current = null;
        }, 2000);
      };
      void run();
    },
    [conversation, ensureGroupVisible, setHighlightedGroupId]
  );

  // Scroll to current search result when it changes
  useEffect(() => {
    const currentMatch = currentSearchIndex >= 0 ? searchMatches[currentSearchIndex] : null;
    if (!currentMatch) return;

    let frameId = 0;
    let attempt = 0;
    let cancelled = false;

    /**
     * Promote a mark element to "current" (demote any previous) and scroll to it.
     */
    const promoteAndScroll = (el: HTMLElement): void => {
      const container = scrollContainerRef.current;
      if (container) {
        container
          .querySelectorAll<HTMLElement>('mark[data-search-result="current"]')
          .forEach((prev) => {
            prev.setAttribute('data-search-result', 'match');
            prev.style.backgroundColor = 'var(--highlight-bg-inactive)';
            prev.style.color = 'var(--highlight-text-inactive)';
            prev.style.boxShadow = '';
          });
      }

      el.setAttribute('data-search-result', 'current');
      el.style.backgroundColor = 'var(--highlight-bg)';
      el.style.color = 'var(--highlight-text)';
      el.style.boxShadow = '0 0 0 1px var(--highlight-ring)';

      el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    };

    /**
     * DOM text-search fallback: walk text nodes inside the group element to find the
     * Nth occurrence of the search query, then scroll the enclosing element into view.
     * This works even when React hasn't created <mark> elements (ReactMarkdown
     * component memoization, render timing, etc.).
     */
    const fallbackDOMSearch = (): boolean => {
      const groupEl =
        chatItemRefs.current.get(currentMatch.itemId) ??
        aiGroupRefs.current.get(currentMatch.itemId);
      if (!groupEl) return false;

      const query = useStore.getState().searchQuery;
      if (!query) return false;
      const lowerQuery = query.toLowerCase();
      let count = 0;

      // Scope to [data-search-content] elements to exclude UI chrome
      // (timestamps, labels, buttons) from text-node walking
      const searchRoots = groupEl.querySelectorAll<HTMLElement>('[data-search-content]');
      const roots = searchRoots.length > 0 ? Array.from(searchRoots) : [groupEl];

      for (const root of roots) {
        const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
        let node: Node | null;
        while ((node = walker.nextNode())) {
          const text = node.textContent ?? '';
          const lowerText = text.toLowerCase();
          let pos = 0;
          while ((pos = lowerText.indexOf(lowerQuery, pos)) !== -1) {
            if (count === currentMatch.matchIndexInItem) {
              const parent = node.parentElement;
              if (parent) {
                parent.scrollIntoView({ behavior: 'smooth', block: 'center' });
                return true;
              }
            }
            count++;
            pos += lowerQuery.length;
          }
        }
      }
      return false;
    };

    const tryScrollToResult = (): void => {
      const container = scrollContainerRef.current;
      if (!container) return;

      // Primary: find mark by item ID + match index
      const el = container.querySelector<HTMLElement>(
        `mark[data-search-item-id="${CSS.escape(currentMatch.itemId)}"][data-search-match-index="${currentMatch.matchIndexInItem}"]`
      );
      if (el) {
        promoteAndScroll(el);
        return;
      }

      // Secondary: align by global order (nth rendered mark) as canonical fallback.
      if (attempt >= 3) {
        const orderedMarks = Array.from(
          container.querySelectorAll<HTMLElement>(
            'mark[data-search-item-id][data-search-match-index]'
          )
        );
        const byGlobal = orderedMarks[currentSearchIndex];
        if (byGlobal) {
          promoteAndScroll(byGlobal);
          return;
        }
      }

      // After a few frames, try fallback DOM text search
      if (attempt >= 6) {
        if (fallbackDOMSearch()) return;
      }

      // Keep retrying (marks may appear after async render)
      if (attempt < 60) {
        attempt++;
        frameId = requestAnimationFrame(tryScrollToResult);
      }
    };

    const run = async (): Promise<void> => {
      await ensureGroupVisible(currentMatch.itemId);
      if (cancelled) return;
      frameId = requestAnimationFrame(tryScrollToResult);
    };

    void run();
    return () => {
      cancelled = true;
      cancelAnimationFrame(frameId);
    };
  }, [currentSearchIndex, searchMatches, scrollContainerRef, ensureGroupVisible]);

  // Track previous active state to detect when THIS tab becomes active/inactive
  const wasActiveRef = useRef(isThisTabActive);

  // Save scroll position when THIS tab becomes inactive
  useEffect(() => {
    const wasActive = wasActiveRef.current;
    wasActiveRef.current = isThisTabActive;

    // If this tab just became inactive, save its scroll position
    if (wasActive && !isThisTabActive && scrollContainerRef.current) {
      saveScrollPosition(scrollContainerRef.current.scrollTop);
    }
  }, [isThisTabActive, saveScrollPosition, scrollContainerRef]);

  // Also save on unmount (e.g., when tab is closed)
  useEffect(() => {
    const scrollContainer = scrollContainerRef.current;
    return () => {
      if (scrollContainer) {
        saveScrollPosition(scrollContainer.scrollTop);
      }
    };
  }, [saveScrollPosition, scrollContainerRef]);

  // Restore scroll position when THIS tab becomes active with saved position
  // Uses shouldDisableAutoScroll (covers full navigation lifecycle) instead of pendingNavigation
  // After navigation completes (transition true→false), save current position to prevent stale restore
  useEffect(() => {
    const wasDisabled = prevShouldDisableRef.current;
    prevShouldDisableRef.current = shouldDisableAutoScroll;

    // Navigation just completed — save current scroll position, skip restore
    if (wasDisabled && !shouldDisableAutoScroll && scrollContainerRef.current) {
      saveScrollPosition(scrollContainerRef.current.scrollTop);
      return;
    }

    if (
      isThisTabActive &&
      savedScrollTop !== undefined &&
      scrollContainerRef.current &&
      !conversationLoading &&
      !shouldDisableAutoScroll
    ) {
      let frameA = 0;
      let frameB = 0;
      // Use double RAF so layout + virtual rows settle before restore.
      frameA = requestAnimationFrame(() => {
        frameB = requestAnimationFrame(() => {
          if (scrollContainerRef.current) {
            scrollContainerRef.current.scrollTop = savedScrollTop;
          }
        });
      });
      return () => {
        cancelAnimationFrame(frameA);
        cancelAnimationFrame(frameB);
      };
    }
  }, [
    isThisTabActive,
    savedScrollTop,
    conversationLoading,
    scrollContainerRef,
    shouldDisableAutoScroll,
    saveScrollPosition,
  ]);

  useEffect(() => {
    return () => {
      if (navigationHighlightTimerRef.current) {
        clearTimeout(navigationHighlightTimerRef.current);
      }
    };
  }, []);

  // Register ref for user/system chat items
  const registerChatItemRef = useCallback((groupId: string) => {
    return (el: HTMLElement | null) => {
      if (el) chatItemRefs.current.set(groupId, el);
      else chatItemRefs.current.delete(groupId);
    };
  }, []);

  // Register ref for individual tool items (for precise scroll targeting)
  const registerToolRef = useCallback((toolId: string, el: HTMLElement | null) => {
    if (el) toolItemRefs.current.set(toolId, el);
    else toolItemRefs.current.delete(toolId);
  }, []);

  // Loading state
  if (conversationLoading) return <ChatHistoryLoadingState />;

  // Empty state
  if (!conversation || conversation.items.length === 0) return <ChatHistoryEmptyState />;

  return (
    <div
      className="flex flex-1 flex-col overflow-hidden"
      style={{ backgroundColor: 'var(--color-surface)' }}
    >
      <div className="relative flex flex-1 overflow-hidden">
        {/* Context panel sidebar (left) */}
        {isContextPanelVisible && allContextInjections.length > 0 && (
          <div className="w-80 shrink-0">
            <SessionContextPanel
              injections={allContextInjections}
              onClose={() => setContextPanelVisible(false)}
              projectRoot={sessionDetail?.session?.projectPath}
              onNavigateToTurn={handleNavigateToTurn}
              onNavigateToTool={handleNavigateToTool}
              onNavigateToUserGroup={handleNavigateToUserGroup}
              contextMetrics={contextMetrics}
              sessionMetrics={sessionDetail?.metrics}
              subagentCostUsd={subagentCostUsd}
              onViewReport={effectiveTabId ? () => openSessionReport(effectiveTabId) : undefined}
              phaseInfo={sessionPhaseInfo ?? undefined}
              selectedPhase={selectedContextPhase}
              onPhaseChange={setSelectedContextPhase}
              side="left"
            />
          </div>
        )}

        {/* Chat content */}
        <div
          ref={scrollContainerRef}
          className="flex-1 overflow-y-auto"
          style={{ backgroundColor: 'var(--color-surface)' }}
          onScroll={checkScrollButton}
        >
          {/* Sticky Context button */}
          {allContextInjections.length > 0 && (
            <div className="pointer-events-none sticky top-0 z-10 flex justify-start px-4 pb-0 pt-3">
              <button
                onClick={() => setContextPanelVisible(!isContextPanelVisible)}
                onMouseEnter={() => setIsContextButtonHovered(true)}
                onMouseLeave={() => setIsContextButtonHovered(false)}
                className="pointer-events-auto flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs shadow-lg transition-colors"
                style={{
                  backgroundColor: isContextPanelVisible
                    ? 'var(--context-btn-active-bg)'
                    : isContextButtonHovered
                      ? 'var(--context-btn-bg-hover)'
                      : 'var(--context-btn-bg)',
                  color: isContextPanelVisible
                    ? 'var(--context-btn-active-text)'
                    : 'var(--color-text-secondary)',
                }}
              >
                {contextUsedPercentLabel ? (
                  <>
                    {contextUsedPercentLabel}
                    {remainingContext && remainingContext.urgency !== 'normal' && (
                      <span
                        style={{
                          color: remainingContext.urgency === 'critical' ? '#ef4444' : '#f59e0b',
                        }}
                      >
                        {' '}
                        ({remainingContext.remainingPct.toFixed(0)}% left)
                      </span>
                    )}
                  </>
                ) : (
                  `Context (${allContextInjections.length})`
                )}
              </button>
            </div>
          )}
          {sessionTeam && (
            <div
              className="mx-auto max-w-5xl px-6 pt-4"
              style={{ marginTop: allContextInjections.length > 0 ? '-1.5rem' : 0 }}
            >
              <button
                onClick={() => openTeamTab(sessionTeam.teamName, sessionTeam.projectPath)}
                className="inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-xs transition-colors hover:brightness-110"
                style={{
                  backgroundColor: 'var(--color-surface-raised)',
                  color: 'var(--color-text-secondary)',
                  border: '1px solid var(--color-border)',
                }}
              >
                <Users className="size-3.5" />
                <span>{sessionTeam.displayName}</span>
                <ChevronRight className="size-3 opacity-50" />
              </button>
            </div>
          )}
          <div
            className="mx-auto max-w-5xl px-6 py-8"
            style={{ marginTop: allContextInjections.length > 0 && !sessionTeam ? '-2rem' : 0 }}
          >
            <div className="space-y-8">
              {shouldShowConversationPagination && (
                <div className="flex flex-wrap items-center justify-center gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] px-3 py-2 text-xs text-[var(--color-text-muted)]">
                  <button
                    type="button"
                    className="rounded border border-[var(--color-border)] px-2 py-1 transition-colors hover:bg-[var(--color-surface-raised)] disabled:cursor-not-allowed disabled:opacity-40"
                    disabled={normalizedPageFromLatest >= totalConversationPages - 1}
                    onClick={() => goToConversationPage(normalizedPageFromLatest + 1)}
                  >
                    更早一页
                  </button>
                  <span>
                    第 {currentConversationPage} / {totalConversationPages} 页
                    {currentConversationPage === 1 ? '（最新）' : ''}
                  </span>
                  <button
                    type="button"
                    className="rounded border border-[var(--color-border)] px-2 py-1 transition-colors hover:bg-[var(--color-surface-raised)] disabled:cursor-not-allowed disabled:opacity-40"
                    disabled={normalizedPageFromLatest <= 0}
                    onClick={() => goToConversationPage(normalizedPageFromLatest - 1)}
                  >
                    更新一页
                  </button>
                  <button
                    type="button"
                    className="rounded border border-[var(--color-border)] px-2 py-1 transition-colors hover:bg-[var(--color-surface-raised)] disabled:cursor-not-allowed disabled:opacity-40"
                    disabled={normalizedPageFromLatest <= 0}
                    onClick={() => goToConversationPage(0)}
                  >
                    回到最新
                  </button>
                </div>
              )}
              {shouldVirtualize ? (
                <div
                  style={{
                    height: `${rowVirtualizer.getTotalSize()}px`,
                    width: '100%',
                    position: 'relative',
                  }}
                >
                  {rowVirtualizer.getVirtualItems().map((virtualRow) => {
                    const item = displayedConversationItems[virtualRow.index];
                    if (!item) return null;
                    return (
                      <div
                        key={virtualRow.key}
                        ref={rowVirtualizer.measureElement}
                        data-index={virtualRow.index}
                        className="pb-8"
                        style={{
                          position: 'absolute',
                          top: 0,
                          left: 0,
                          width: '100%',
                          transform: `translateY(${virtualRow.start}px)`,
                        }}
                      >
                        <ChatHistoryItem
                          item={item}
                          highlightedGroupId={highlightedGroupId}
                          highlightToolUseId={effectiveHighlightToolUseId}
                          isSearchHighlight={isSearchHighlight}
                          isNavigationHighlight={isNavigationHighlight}
                          highlightColor={effectiveHighlightColor}
                          isNew={
                            newGroupIds.has(item.group.id) &&
                            !animatedGroupIdsRef.current.has(item.group.id)
                          }
                          registerChatItemRef={registerChatItemRef}
                          registerAIGroupRef={registerAIGroupRefCombined}
                          registerToolRef={registerToolRef}
                        />
                      </div>
                    );
                  })}
                </div>
              ) : (
                displayedConversationItems.map((item) => (
                  <ChatHistoryItem
                    key={item.group.id}
                    item={item}
                    highlightedGroupId={highlightedGroupId}
                    highlightToolUseId={effectiveHighlightToolUseId}
                    isSearchHighlight={isSearchHighlight}
                    isNavigationHighlight={isNavigationHighlight}
                    highlightColor={effectiveHighlightColor}
                    isNew={
                      newGroupIds.has(item.group.id) &&
                      !animatedGroupIdsRef.current.has(item.group.id)
                    }
                    registerChatItemRef={registerChatItemRef}
                    registerAIGroupRef={registerAIGroupRefCombined}
                    registerToolRef={registerToolRef}
                  />
                ))
              )}
            </div>
          </div>
        </div>

        {/* Scroll to bottom button */}
        {showScrollButton && (
          <button
            onClick={() => {
              scrollToBottom('smooth');
              setShowScrollButton(false);
            }}
            className="absolute bottom-5 z-20 flex items-center gap-1.5 rounded-full px-3 py-1.5 text-xs shadow-lg transition-colors"
            style={{
              right: '1rem',
              backgroundColor: 'var(--context-btn-bg)',
              color: 'var(--color-text-secondary)',
              border: '1px solid var(--color-border-emphasis)',
            }}
            title="Scroll to bottom"
          >
            <ChevronsDown className="size-3.5" />
            <span>Bottom</span>
          </button>
        )}
      </div>
    </div>
  );
};
