import React, { useEffect, useMemo, useState } from 'react';
import { themedColor } from '../../theme';
import { Pressable, ScrollView, Text, View } from 'react-native';
import { AlertCircle, CheckCircle2, ChevronRight, Loader2, type LucideIcon } from 'lucide-react-native';

import { flattenToolCall, getToolTypeIcon, isActiveToolStatus, normalizeStatus } from '../conversation/agentPhase';
import { getPackageActionMode, type PackageActionMode } from './packageUtils';

// Muted monochrome palette, matching the web tool timeline (gray for normal /
// active, red only for errors). See agent-timeline-tool-row-utils.ts + TimelineRow.tsx.
const TOOL_ICON_MUTED = '#A1A1AA';
const TOOL_ICON_ERROR = '#FCA5A5';

import { conversationStyles } from '../conversation/conversationStyles';
import { SendImageGallery } from '../attachments/SendImageGallery';
import { isSendImageTool, parseSentImages } from '../attachments/sendImage';
import { SpinningLoader } from '../../components/SpinningLoader';
import { ToolApprovalCard } from './ToolApprovalCard';
import {
  approvalCapableToolNames,
  groupedToolNames,
  resolveDefaultToolRenderer,
} from '../../toolWidgets/defaultToolRenderers';
import { EntityApprovalBatchCard } from '../../toolWidgets/EntityApprovalBatchCard';
import { isPendingEntityApproval } from '../../toolWidgets/entityWidgetUtils';
import {
  getStringArg,
  groupConsecutiveToolCalls,
  parseToolArgs,
  truncateSingleLine,
} from '../../toolWidgets/toolWidgetUtils';
import type {
  SuperagentAgent,
  SuperagentConnector,
  SuperagentConnectorActionInput,
  SuperagentConversation,
  SuperagentMessage,
  SuperagentToolCall,
  SuperagentToolRenderers,
} from '../../types';

// Low-level tools hidden in the timeline — mirror of the web's HIDDEN_IN_BOTH
// (activity-groups.ts). The screenshot-producing browser tools
// (browserbase_screenshot / local_browser_screenshot) are NOT hidden: they
// render via their widget in defaultToolRenderers.
const HIDDEN_TOOLS = new Set([
  'browserbase_get_session',
  'browserbase_stop_session',
  'browserbase_get_live_urls',
  'browserbase_reset_context',
  'browserbase_navigate',
  'browserbase_click',
  'browserbase_type',
  'browserbase_get_content',
  'edit_file',
  // Internal agent→Base44 feedback signal — never user-facing on the web either.
  'vent_send_feedback',
]);

// Labels only for tools that exist in superagent flows (create_user_agent_tools
// in backend/.../user_agent/tools.py) or that older conversations can still
// carry (legacy channel/connector approval tools, the Anthropic-native
// str_replace editor). Builder-only names from the original builder-era port
// were pruned.
const FRIENDLY_TOOL_LABELS: Record<string, { active: string; completed: string }> = {
  ask_clarifying_questions: { active: 'Asking a clarifying question...', completed: 'Asked a clarifying question' },
  bash: { active: 'Running commands...', completed: 'Ran commands' },
  browserbase_screenshot: { active: 'Taking screenshots...', completed: 'Took screenshots' },
  build_mode: { active: 'Designing your agent...', completed: 'Designed your agent' },
  // Neutral completed copy: the PSP tools resolve success=True on "Skip" too, so a
  // connected/selected label would falsely claim payments are active (web parity).
  configure_psp_credentials: { active: 'Connecting payment provider...', completed: 'Payment processor setup' },
  create_automation: { active: 'Creating automation...', completed: 'Created automation' },
  create_or_update_workflow: { active: 'Saving workflow...', completed: 'Saved workflow' },
  delete_entities: { active: 'Deleting data...', completed: 'Deleted data' },
  delete_file: { active: 'Deleting files...', completed: 'Deleted files' },
  fetch_website: { active: 'Reading websites...', completed: 'Read websites' },
  find_replace: { active: 'Editing code...', completed: 'Edited code' },
  get_connectors_info: { active: 'Checking connectors...', completed: 'Checked connectors' },
  grep: { active: 'Searching files...', completed: 'Searched files' },
  install_npm_package: { active: 'Installing packages...', completed: 'Installed packages' },
  list_automations: { active: 'Listing automations...', completed: 'Listed automations' },
  manage_automation: { active: 'Managing automation...', completed: 'Managed automation' },
  request_oauth_authorization: { active: 'Requesting connector access...', completed: 'Requested connector access' },
  run_skill: { active: 'Running a skill...', completed: 'Ran a skill' },
  search_base44_docs: { active: 'Checking docs...', completed: 'Checked docs' },
  search_web: { active: 'Searching the web...', completed: 'Searched the web' },
  select_payment_provider: { active: 'Selecting payment provider...', completed: 'Payment provider selection' },
  sequence_connect: { active: 'Connecting banking...', completed: 'Bank connection' },
  sequence_create_account: { active: 'Creating account...', completed: 'Account setup' },
  sequence_create_rule: { active: 'Creating automation rule...', completed: 'Automation rule setup' },
  sequence_create_transfer: { active: 'Creating transfer...', completed: 'Transfer' },
  sequence_get_account: { active: 'Getting account details...', completed: 'Got account details' },
  sequence_get_rule: { active: 'Getting automation rule...', completed: 'Got automation rule' },
  sequence_get_rule_execution: { active: 'Getting rule run...', completed: 'Got rule run' },
  sequence_get_transfer: { active: 'Getting transfer...', completed: 'Got transfer' },
  sequence_list_accounts: { active: 'Listing accounts...', completed: 'Listed accounts' },
  sequence_list_audit_logs: { active: 'Listing audit logs...', completed: 'Listed audit logs' },
  sequence_list_beneficiaries: { active: 'Listing beneficiaries...', completed: 'Listed beneficiaries' },
  sequence_list_card_transactions: { active: 'Listing card transactions...', completed: 'Listed card transactions' },
  sequence_list_external_transactions: { active: 'Listing external transactions...', completed: 'Listed external transactions' },
  sequence_list_rule_executions: { active: 'Listing rule runs...', completed: 'Listed rule runs' },
  sequence_list_rules: { active: 'Listing automation rules...', completed: 'Listed automation rules' },
  sequence_list_transfers: { active: 'Listing transfers...', completed: 'Listed transfers' },
  sequence_list_transfers_by_account: { active: 'Listing account transfers...', completed: 'Listed account transfers' },
  sequence_trigger_rule: { active: 'Running automation rule...', completed: 'Automation run' },
  sequence_update_rule: { active: 'Updating automation rule...', completed: 'Automation rule update' },
  set_app_user_connector: { active: 'Requesting connector credentials...', completed: 'Requested connector credentials' },
  set_secrets: { active: 'Setting a secret...', completed: 'Set a secret' },
  setup_slack_connection: { active: 'Connecting Slack...', completed: 'Connected Slack' },
  setup_telegram_connection: { active: 'Connecting Telegram...', completed: 'Connected Telegram' },
  suggest_payments_installation: { active: 'Setting up payments...', completed: 'Requested payment setup' },
  str_replace_based_edit_tool: { active: 'Editing code...', completed: 'Edited code' },
  test_backend_function: { active: 'Testing backend...', completed: 'Tested backend' },
  update_entities: { active: 'Updating data...', completed: 'Updated data' },
  update_identity: { active: 'Updating memory...', completed: 'Updated memory' },
  web_search: { active: 'Searching the web...', completed: 'Searched the web' },
};

type SubmitToolCallInput = (
  toolCallId: string,
  approve: boolean,
  extraUserInput?: unknown,
  originRequestId?: string,
) => Promise<SuperagentConversation | null>;

export function ToolCallSummary({
  agent,
  availableConnectors,
  conversationId,
  isLastAssistantMessage,
  message,
  onConnectConnector,
  showDebugPayloads,
  submitToolCallInput,
  toolCalls,
  toolRenderers,
}: {
  agent: SuperagentAgent;
  availableConnectors?: SuperagentConnector[];
  conversationId: string | null;
  isLastAssistantMessage: boolean;
  message: SuperagentMessage;
  onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise<boolean | string | void> | boolean | string | void;
  showDebugPayloads?: boolean;
  submitToolCallInput?: SubmitToolCallInput;
  toolCalls: SuperagentToolCall[];
  toolRenderers?: SuperagentToolRenderers;
}) {
  const chunks = useMemo(() => createToolChunks(toolCalls, toolRenderers), [toolCalls, toolRenderers]);

  if (chunks.length === 0) return null;

  return (
    <View style={conversationStyles.toolCallWrap}>
      {chunks.map((chunk) => {
        if (chunk.type === 'timeline') {
          return (
            <DefaultToolTimeline
              agent={agent}
              availableConnectors={availableConnectors}
              isLastAssistantMessage={isLastAssistantMessage}
              key={chunk.key}
              onConnectConnector={onConnectConnector}
              submitToolCallInput={submitToolCallInput}
              toolCalls={chunk.toolCalls}
            />
          );
        }

        if (chunk.type === 'gallery') {
          return (
            <View key={chunk.key} style={conversationStyles.toolCallSlot}>
              <SendImageGallery
                toolCalls={chunk.toolCalls}
                // Collapse (don't hide) when the agent also pasted these image
                // URLs into the message text, so the images aren't shown twice
                // but stay reachable even if the text rendered them as links.
                defaultCollapsed={galleryUrlsAllInText(chunk.toolCalls, message.content)}
              />
            </View>
          );
        }

        if (chunk.type === 'entityApprovalBatch') {
          return (
            <View key={chunk.key} style={conversationStyles.toolCallSlot}>
              <EntityApprovalBatchCard
                isLastAssistantMessage={isLastAssistantMessage}
                submitToolCallInput={submitToolCallInput}
                toolCalls={chunk.toolCalls}
              />
            </View>
          );
        }

        const Renderer = chunk.Renderer;
        return (
          <View key={chunk.key} style={conversationStyles.toolCallSlot}>
            <View style={conversationStyles.customToolWidget}>
              <Renderer
                agent={agent}
                conversationId={conversationId}
                isLastAssistantMessage={isLastAssistantMessage}
                message={message}
                showDebugPayloads={showDebugPayloads}
                submitToolCallInput={submitToolCallInput}
                toolCall={chunk.toolCall}
              />
            </View>
          </View>
        );
      })}
    </View>
  );
}

function DefaultToolTimeline({
  agent,
  availableConnectors,
  isLastAssistantMessage,
  onConnectConnector,
  submitToolCallInput,
  toolCalls,
}: {
  agent: SuperagentAgent;
  availableConnectors?: SuperagentConnector[];
  isLastAssistantMessage: boolean;
  onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise<boolean | string | void> | boolean | string | void;
  submitToolCallInput?: SubmitToolCallInput;
  toolCalls: SuperagentToolCall[];
}) {
  const visibleToolCalls = useMemo(
    // Hidden tools (browserbase_*, ...) stay hidden during normal execution,
    // but a pre-execution guard can pause one as waiting_for_user_input.
    // Keep those visible so the approval card renders and the agent can resume.
    () => toolCalls
      .flatMap(flattenToolCall)
      .filter((toolCall) => !HIDDEN_TOOLS.has(toolCall.name || '') || isApprovalRequest(toolCall)),
    [toolCalls],
  );
  const hasWaitingTool = visibleToolCalls.some(isApprovalRequest);
  const isRunning = visibleToolCalls.some(isRunningStatus);
  const [expanded, setExpanded] = useState(hasWaitingTool);
  const [openRows, setOpenRows] = useState<Record<string, boolean>>({});

  useEffect(() => {
    if (hasWaitingTool) setExpanded(true);
  }, [hasWaitingTool]);

  if (visibleToolCalls.length === 0) return null;

  const label = getCollapsedLabel(visibleToolCalls, isRunning);

  // While tools are still running (and nothing needs the user's input), the live
  // status is carried by the bottom TypingIndicator — don't render a duplicate
  // in-bubble indicator here. The timeline renders once the tools settle. If an
  // approval/waiting tool is present alongside a running one, fall through to the
  // expanded timeline so the approval card stays reachable.
  if (isRunning && !hasWaitingTool) return null;

  return (
    <View style={conversationStyles.toolCallSlot}>
      <Pressable
        accessibilityRole="button"
        onPress={() => setExpanded((current) => !current)}
        style={conversationStyles.toolTimelineHeader}
      >
        <Text style={conversationStyles.toolTimelineHeaderText}>{label}</Text>
        <ChevronRight
          color={themedColor('#8E8E93')}
          size={15}
          strokeWidth={2.3}
          style={expanded ? conversationStyles.toolTimelineChevronOpen : conversationStyles.toolTimelineChevron}
        />
      </Pressable>

      {expanded ? (
        <View style={conversationStyles.toolTimelineRows}>
          {visibleToolCalls.map((toolCall, index) => {
            const rowKey = getToolCallKey(toolCall, index);
            const isOpen = Boolean(openRows[rowKey]);
            return (
              <ToolTimelineRow
                agent={agent}
                availableConnectors={availableConnectors}
                isLastAssistantMessage={isLastAssistantMessage}
                isLast={index === visibleToolCalls.length - 1}
                isOpen={isOpen}
                key={rowKey}
                onToggle={() => setOpenRows((current) => ({ ...current, [rowKey]: !current[rowKey] }))}
                onConnectConnector={onConnectConnector}
                submitToolCallInput={submitToolCallInput}
                toolCall={toolCall}
              />
            );
          })}
          {!hasWaitingTool ? <DoneRow /> : null}
        </View>
      ) : null}
    </View>
  );
}

function ToolTimelineRow({
  agent,
  availableConnectors,
  isLastAssistantMessage,
  isLast,
  isOpen,
  onConnectConnector,
  onToggle,
  submitToolCallInput,
  toolCall,
}: {
  agent: SuperagentAgent;
  availableConnectors?: SuperagentConnector[];
  isLastAssistantMessage: boolean;
  isLast: boolean;
  isOpen: boolean;
  onConnectConnector?: (input: SuperagentConnectorActionInput) => Promise<boolean | string | void> | boolean | string | void;
  onToggle: () => void;
  submitToolCallInput?: SubmitToolCallInput;
  toolCall: SuperagentToolCall;
}) {
  const preview = getToolPreview(toolCall);
  const detail = getToolDetail(toolCall);
  const showsApproval = isApprovalRequest(toolCall);
  const canOpen = Boolean(detail) && !showsApproval;

  return (
    <View style={conversationStyles.toolTimelineItem}>
      <View style={conversationStyles.toolTimelineRow}>
        <View style={conversationStyles.toolTimelineIconCol}>
          <ToolStatusIcon name={toolCall.name} status={toolCall.status} />
          {!isLast ? <View style={conversationStyles.toolTimelineLine} /> : null}
        </View>
        <View style={conversationStyles.toolTimelineMeta}>
          <Pressable
            accessibilityRole={canOpen ? 'button' : undefined}
            disabled={!canOpen}
            onPress={onToggle}
            style={conversationStyles.toolTimelineRowHeader}
          >
            <ChevronRight
              color={themedColor('#A1A1AA')}
              size={15}
              strokeWidth={2.3}
              style={isOpen ? conversationStyles.toolTimelineChevronOpen : conversationStyles.toolTimelineChevron}
            />
            <Text style={conversationStyles.toolTimelineTitle} numberOfLines={1}>{getToolTitle(toolCall)}</Text>
            {preview ? <Text style={conversationStyles.toolTimelineInline} numberOfLines={1}>{preview}</Text> : null}
          </Pressable>
          {shouldShowStatusLine(toolCall, preview) ? (
            <Text style={conversationStyles.toolTimelineStatus} numberOfLines={1}>{formatStatus(toolCall.status || 'done')}</Text>
          ) : null}
        </View>
      </View>

      <View style={conversationStyles.toolTimelineDetailsSlot}>
        {showsApproval ? (
          <ToolApprovalCard
            agent={agent}
            availableConnectors={availableConnectors}
            isLastAssistantMessage={isLastAssistantMessage}
            onConnectConnector={onConnectConnector}
            submitToolCallInput={submitToolCallInput}
            toolCall={toolCall}
          />
        ) : null}
        {!showsApproval && isOpen && detail ? <ToolDetailBox detail={detail} /> : null}
      </View>
    </View>
  );
}

function ToolDetailBox({ detail }: { detail: string }) {
  return (
    <View style={conversationStyles.toolTimelineDetails}>
      <ScrollView
        nestedScrollEnabled
        showsVerticalScrollIndicator
        style={conversationStyles.toolTimelineDetailVerticalScroll}
      >
        <ScrollView
          horizontal
          nestedScrollEnabled
          showsHorizontalScrollIndicator
          contentContainerStyle={conversationStyles.toolTimelineDetailScrollContent}
          style={conversationStyles.toolTimelineDetailHorizontalScroll}
        >
          <Text style={conversationStyles.toolTimelineDetailText}>{detail}</Text>
        </ScrollView>
      </ScrollView>
    </View>
  );
}

function DoneRow() {
  return (
    <View style={conversationStyles.toolDoneRow}>
      <View style={conversationStyles.toolTimelineIconCol}>
        <View style={conversationStyles.toolCallIcon}>
          <CheckCircle2 color={themedColor(TOOL_ICON_MUTED)} size={17} strokeWidth={2.3} />
        </View>
      </View>
      <Text style={conversationStyles.toolDoneText}>Done</Text>
    </View>
  );
}

// active → spinner, error → alert, otherwise the tool-type icon (shared with the
// thinking-status phase indicator in agentPhase.ts).
function getToolIcon(name: string, isActive: boolean, isError: boolean): LucideIcon {
  if (isActive) return Loader2;
  if (isError) return AlertCircle;
  return getToolTypeIcon(name);
}

function ToolStatusIcon({ name, status }: { name?: string; status?: string }) {
  const isActive = isRunningStatus({ status });
  const isError = isErrorStatus({ status });
  const color = isError ? themedColor(TOOL_ICON_ERROR) : themedColor(TOOL_ICON_MUTED);
  const Icon = getToolIcon(name || '', isActive, isError);

  return (
    <View style={conversationStyles.toolCallIcon}>
      {isActive ? <SpinningLoader color={color} /> : <Icon color={color} size={17} strokeWidth={2.3} />}
    </View>
  );
}

// True when every image in a send_image gallery resolved AND its URL already
// appears in the agent's message text — used to collapse (not hide) the gallery
// so it doesn't visually duplicate what the text already shows, while staying
// reachable (the text may render the URL as a link, not an inline image). A
// still-loading/failed tile has no text equivalent, so it returns false.
function galleryUrlsAllInText(toolCalls: SuperagentToolCall[], content: string | undefined): boolean {
  if (!content) return false;
  const images = parseSentImages(toolCalls);
  return images.length > 0 && images.every((img) => img.url != null && content.includes(img.url));
}

function createToolChunks(toolCalls: SuperagentToolCall[], toolRenderers?: SuperagentToolRenderers) {
  const chunks: Array<
    | { key: string; toolCalls: SuperagentToolCall[]; type: 'timeline' }
    | { key: string; toolCalls: SuperagentToolCall[]; type: 'gallery' }
    | { key: string; toolCalls: SuperagentToolCall[]; type: 'entityApprovalBatch' }
    | { key: string; Renderer: NonNullable<SuperagentToolRenderers>[string]; toolCall: SuperagentToolCall; type: 'custom' }
  > = [];
  let timelineBuffer: SuperagentToolCall[] = [];
  let galleryBuffer: SuperagentToolCall[] = [];
  let timelineIndex = 0;
  let galleryIndex = 0;

  // Runs of consecutive calls to a groupable tool (grep, generate_image)
  // collapse into one synthetic grouped call, so their renderer shows a
  // single combined card — the native mirror of the web's groupConsecutive
  // preprocessing. Non-groupable calls pass through untouched.
  const groupedToolCalls = groupConsecutiveToolCalls(toolCalls, groupedToolNames);

  const flushTimeline = () => {
    if (timelineBuffer.length === 0) return;
    chunks.push({
      key: `timeline-${timelineIndex}`,
      toolCalls: timelineBuffer,
      type: 'timeline',
    });
    timelineIndex += 1;
    timelineBuffer = [];
  };

  // Consecutive send_image calls collapse into one gallery widget so a batch of
  // images (e.g. a deck of slides) renders as a grid rather than N timeline rows.
  const flushGallery = () => {
    if (galleryBuffer.length === 0) return;
    chunks.push({
      key: `gallery-${galleryIndex}`,
      toolCalls: galleryBuffer,
      type: 'gallery',
    });
    galleryIndex += 1;
    galleryBuffer = [];
  };

  // Two or more pending update/delete entity approvals collapse into one batch
  // card (per-call checkboxes) rendered after everything else, mirroring the
  // web ToolCallsContainer. A single one stays inline via its own widget.
  // Host-overridden tools are excluded — their renderer keeps the call.
  // Entity tools are never in groupedToolNames, so grouping passes them
  // through untouched and this filter sees the original calls.
  const batchApprovalCalls = groupedToolCalls.filter(
    (toolCall) => isPendingEntityApproval(toolCall) && !toolRenderers?.[toolCall.name],
  );
  const batchedIds = batchApprovalCalls.length > 1
    ? new Set(batchApprovalCalls.map((toolCall) => toolCall.id))
    : null;

  groupedToolCalls.forEach((toolCall, index) => {
    if (batchedIds?.has(toolCall.id)) return;

    const flattened = flattenToolCall(toolCall);
    // Host renderers (the toolRenderers prop) win over the built-in widget
    // registry. Built-in widgets additionally step aside for approval-paused
    // calls so the timeline's ToolApprovalCard keeps handling those — except
    // the approval-capable widgets, which render their own approval UI.
    const Renderer = toolRenderers?.[toolCall.name]
      ?? (flattened.some(isApprovalRequest) && !approvalCapableToolNames.has(toolCall.name)
        ? undefined
        : resolveDefaultToolRenderer(toolCall));
    if (Renderer) {
      // A renderer for the tool always wins over the gallery.
      flushTimeline();
      flushGallery();
      chunks.push({
        key: toolCall.id ?? `${toolCall.name}-${index}`,
        Renderer,
        toolCall,
        type: 'custom',
      });
      return;
    }

    // send_image (incl. a group consisting only of send_image calls) is shown as
    // an image gallery instead of generic timeline rows.
    if (flattened.length > 0 && flattened.every(isSendImageTool)) {
      flushTimeline();
      galleryBuffer.push(...flattened);
      return;
    }

    flushGallery();
    timelineBuffer.push(toolCall);
  });

  flushTimeline();
  flushGallery();
  if (batchedIds) {
    chunks.push({ key: 'entity-approval-batch', toolCalls: batchApprovalCalls, type: 'entityApprovalBatch' });
  }
  return chunks;
}

function getCollapsedLabel(toolCalls: SuperagentToolCall[], isRunning: boolean) {
  if (toolCalls.some(isApprovalRequest)) return 'Waiting for input';

  const firstName = toolCalls[0]?.name || '';
  if (toolCalls.length > 0 && toolCalls.every((toolCall) => toolCall.name === firstName)) {
    if (firstName === 'bash') {
      if (isRunning) return 'Running commands...';
      return toolCalls.length === 1 ? 'Ran a command' : `Ran ${toolCalls.length} commands`;
    }

    if (firstName === 'install_npm_package') {
      // Collapsed rows use the static FRIENDLY_TOOL_LABELS by default, which is
      // install-centric; make the collapsed label reflect the actual action(s)
      // AND the terminal status so a failed (un)install isn't shown as done.
      const modes = toolCalls.map((tc) => getPackageActionModeFromArgs(parseToolArgs(tc)));
      const hasInstall = modes.some((m) => m === 'install' || m === 'mixed');
      const hasUninstall = modes.some((m) => m === 'uninstall' || m === 'mixed');
      const kind = hasInstall && hasUninstall ? 'update' : hasUninstall ? 'uninstall' : 'install';
      // Waiting/approval rows are handled by the isApprovalRequest guard above,
      // so here we only distinguish success from not-done (error or rejection).
      const notDone = !isRunning && toolCalls.some(isNotDoneStatus);
      const base = isRunning
        ? { update: 'Updating packages...', uninstall: 'Uninstalling packages...', install: 'Installing packages...' }[kind]
        : notDone
          ? { update: 'Failed to update packages', uninstall: 'Failed to uninstall packages', install: 'Failed to install packages' }[kind]
          : { update: 'Updated packages', uninstall: 'Uninstalled packages', install: 'Installed packages' }[kind];
      return !isRunning && toolCalls.length > 1 ? `${base} (${toolCalls.length})` : base;
    }

    const label = FRIENDLY_TOOL_LABELS[firstName];
    if (label) {
      const base = isRunning ? label.active : label.completed;
      return !isRunning && toolCalls.length > 1 ? `${base} (${toolCalls.length})` : base;
    }
  }

  if (isRunning) return 'Working...';
  return `${toolCalls.length} ${toolCalls.length === 1 ? 'action' : 'actions'}`;
}

function getPackageActionModeFromArgs(args: Record<string, unknown> | string | null): PackageActionMode {
  const packages = args && typeof args !== 'string' && Array.isArray(args.packages) ? args.packages : [];
  return getPackageActionMode(packages as Array<{ action?: string }>);
}

function getToolTitle(toolCall: SuperagentToolCall) {
  if (toolCall.name === 'install_npm_package') {
    const mode = getPackageActionModeFromArgs(parseToolArgs(toolCall));
    const kind = mode === 'mixed' ? 'update' : mode;
    // Waiting-for-approval is not terminal — show the in-progress wording so the
    // header never implies a destructive removal happened before the user
    // approved. A rejected (stopped) or failed op shows a non-success label.
    const active = isRunningStatus(toolCall) || isWaitingStatus(toolCall);
    const notDone = !active && isNotDoneStatus(toolCall);
    const verbs = {
      install: { active: 'Installing packages', failed: 'Failed to install packages', done: 'Installed packages' },
      uninstall: { active: 'Uninstalling packages', failed: 'Failed to uninstall packages', done: 'Uninstalled packages' },
      update: { active: 'Updating packages', failed: 'Failed to update packages', done: 'Updated packages' },
    }[kind];
    return active ? verbs.active : notDone ? verbs.failed : verbs.done;
  }
  const label = FRIENDLY_TOOL_LABELS[toolCall.name || ''];
  if (label) return isRunningStatus(toolCall) ? label.active.replace(/\.\.\.$/, '') : label.completed;
  return formatToolName(toolCall.name || 'Tool');
}

function getToolPreview(toolCall: SuperagentToolCall) {
  const args = parseToolArgs(toolCall);
  const rawArgs = getRawArgs(toolCall);

  if (toolCall.name === 'bash') {
    const command = getStringArg(args, ['command', 'cmd', 'script']) || (typeof rawArgs === 'string' ? rawArgs : '');
    return command ? truncateSingleLine(command, 48) : null;
  }

  if (toolCall.name === 'grep') {
    const pattern = getStringArg(args, ['pattern', 'query']);
    const path = getStringArg(args, ['path', 'include']);
    return truncateSingleLine([pattern, path].filter(Boolean).join(' in '), 48);
  }

  if (toolCall.name === 'search_web' || toolCall.name === 'web_search') {
    return truncateSingleLine(getStringArg(args, ['query', 'q']), 48);
  }

  if (toolCall.name === 'request_oauth_authorization') {
    return 'waiting for user input / approval';
  }

  if (toolCall.name === 'get_connectors_info') {
    return getResultPreview(toolCall.results);
  }

  return getStringArg(args, ['name', 'title', 'path', 'filename', 'integration_type', 'connector_id']).trim() || null;
}

function getToolDetail(toolCall: SuperagentToolCall) {
  const sections: string[] = [];
  const args = getRawArgs(toolCall);
  const results = toolCall.results;

  if (args) sections.push(`Input:\n${formatValue(args)}`);
  if (results) sections.push(`Output:\n${formatValue(results)}`);

  return sections.length > 0 ? sections.join('\n\n') : null;
}

function getRawArgs(toolCall: SuperagentToolCall) {
  return toolCall.arguments_string ?? toolCall.arguments ?? null;
}

function getResultPreview(result: unknown) {
  if (!result) return null;
  const value = typeof result === 'string' ? result : formatValue(result);
  const firstLine = value.split('\n').find((line) => line.trim());
  return firstLine ? truncateSingleLine(firstLine, 52) : null;
}

function shouldShowStatusLine(toolCall: SuperagentToolCall, preview: string | null) {
  if (!toolCall.status) return false;
  if (toolCall.name === 'bash' || toolCall.name === 'grep') return false;
  return !preview || isWaitingStatus(toolCall) || isErrorStatus(toolCall);
}

function isRunningStatus(toolCall: Pick<SuperagentToolCall, 'status'>) {
  // Shared with resolveThinkingPhase so the in-bubble timeline and the bottom
  // status agree on what "active" means (notably: in_progress counts).
  return isActiveToolStatus(toolCall.status);
}

function isWaitingStatus(toolCall: Pick<SuperagentToolCall, 'status'>) {
  return normalizeStatus(toolCall.status).includes('waiting');
}

function isApprovalRequest(toolCall: SuperagentToolCall) {
  // Web parity: every web tool-UI keys the approval form strictly on
  // `status === 'waiting_for_user_input'`, with no results-text fallback. The
  // old `results.includes('waiting for user input')` heuristic re-triggered the
  // approval UI on settled/declined calls whose results still carried the
  // backend's waiting message (set_waiting_for_user_input overwrites results),
  // making a dismissed suggestion render its approval card again.
  return isWaitingStatus(toolCall);
}

function isErrorStatus(toolCall: Pick<SuperagentToolCall, 'status'>) {
  const normalized = normalizeStatus(toolCall.status);
  return normalized.includes('error') || normalized.includes('failed') || normalized.includes('rejected');
}

// A terminal status that is NOT a successful completion: an error/failure, or a
// user-rejected approval (the backend sets `stopped` on rejection). Used so a
// rejected/failed (un)install never renders the success ("Uninstalled") wording.
function isNotDoneStatus(toolCall: Pick<SuperagentToolCall, 'status'>) {
  return isErrorStatus(toolCall) || normalizeStatus(toolCall.status).includes('stopped');
}

function formatToolName(name: string) {
  return name
    .split('_')
    .filter(Boolean)
    .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
    .join(' ');
}

function formatStatus(status: string) {
  return status
    .split('_')
    .filter(Boolean)
    .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
    .join(' ');
}

function formatValue(value: unknown) {
  if (typeof value === 'string') return value.length > 900 ? `${value.slice(0, 900)}...` : value;

  try {
    const stringified = JSON.stringify(value, null, 2);
    return stringified.length > 900 ? `${stringified.slice(0, 900)}...` : stringified;
  } catch {
    return String(value);
  }
}

function getToolCallKey(toolCall: SuperagentToolCall, index: number) {
  return toolCall.id ?? `${toolCall.name || 'tool'}-${index}`;
}
