/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import React from 'react';
import { Box, Text } from 'ink';
import { DiffRenderer } from './DiffRenderer.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { AnsiOutputText, AnsiLineText } from '../AnsiOutput.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { theme } from '../../semantic-colors.js';
import {
  type AnsiOutput,
  type AnsiLine,
  isSubagentProgress,
} from '@google/gemini-cli-core';
import { useUIState } from '../../contexts/UIStateContext.js';
import { tryParseJSON } from '../../../utils/jsonoutput.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
import { Scrollable } from '../shared/Scrollable.js';
import { ScrollableList } from '../shared/ScrollableList.js';
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
import {
  calculateToolContentMaxLines,
  TOOL_RESULT_MIN_LINES_SHOWN,
} from '../../utils/toolLayoutUtils.js';
import { SubagentProgressDisplay } from './SubagentProgressDisplay.js';

// Large threshold to ensure we don't cause performance issues for very large
// outputs that will get truncated further MaxSizedBox anyway.
const MAXIMUM_RESULT_DISPLAY_CHARACTERS = 20000;

export interface ToolResultDisplayProps {
  resultDisplay: string | object | undefined;
  availableTerminalHeight?: number;
  terminalWidth: number;
  renderOutputAsMarkdown?: boolean;
  maxLines?: number;
  hasFocus?: boolean;
}

interface FileDiffResult {
  fileDiff: string;
  fileName: string;
}

export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
  resultDisplay,
  availableTerminalHeight,
  terminalWidth,
  renderOutputAsMarkdown = true,
  maxLines,
  hasFocus = false,
}) => {
  const { renderMarkdown } = useUIState();
  const isAlternateBuffer = useAlternateBuffer();

  const availableHeight = calculateToolContentMaxLines({
    availableTerminalHeight,
    isAlternateBuffer,
    maxLinesLimit: maxLines,
  });

  const effectiveMaxHeight =
    availableHeight !== undefined
      ? Math.max(TOOL_RESULT_MIN_LINES_SHOWN, availableHeight)
      : undefined;

  const combinedPaddingAndBorderWidth = 4;
  const childWidth = terminalWidth - combinedPaddingAndBorderWidth;

  const keyExtractor = React.useCallback(
    (_: AnsiLine, index: number) => index.toString(),
    [],
  );

  const renderVirtualizedAnsiLine = React.useCallback(
    ({ item }: { item: AnsiLine }) => (
      <Box height={1} overflow="hidden">
        <AnsiLineText line={item} />
      </Box>
    ),
    [],
  );

  const { truncatedResultDisplay, hiddenLinesCount } = React.useMemo(() => {
    let hiddenLines = 0;
    // Only truncate string output if not in alternate buffer mode to ensure
    // we can scroll through the full output.
    if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
      let text = resultDisplay;
      if (text.length > MAXIMUM_RESULT_DISPLAY_CHARACTERS) {
        text = '...' + text.slice(-MAXIMUM_RESULT_DISPLAY_CHARACTERS);
      }
      if (maxLines) {
        const hasTrailingNewline = text.endsWith('\n');
        const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
        const lines = contentText.split('\n');
        if (lines.length > maxLines) {
          // We will have a label from MaxSizedBox. Reserve space for it.
          const targetLines = Math.max(1, maxLines - 1);
          hiddenLines = lines.length - targetLines;
          text =
            lines.slice(-targetLines).join('\n') +
            (hasTrailingNewline ? '\n' : '');
        }
      }
      return { truncatedResultDisplay: text, hiddenLinesCount: hiddenLines };
    }

    if (Array.isArray(resultDisplay) && !isAlternateBuffer && maxLines) {
      if (resultDisplay.length > maxLines) {
        // We will have a label from MaxSizedBox. Reserve space for it.
        const targetLines = Math.max(1, maxLines - 1);
        return {
          truncatedResultDisplay: resultDisplay.slice(-targetLines),
          hiddenLinesCount: resultDisplay.length - targetLines,
        };
      }
    }

    return { truncatedResultDisplay: resultDisplay, hiddenLinesCount: 0 };
  }, [resultDisplay, isAlternateBuffer, maxLines]);

  if (!truncatedResultDisplay) return null;

  // 1. Early return for background tools (Todos)
  if (
    typeof truncatedResultDisplay === 'object' &&
    'todos' in truncatedResultDisplay
  ) {
    // display nothing, as the TodoTray will handle rendering todos
    return null;
  }

  // 2. High-performance path: Virtualized ANSI in interactive mode
  if (isAlternateBuffer && Array.isArray(truncatedResultDisplay)) {
    // If availableHeight is undefined, fallback to a safe default to prevents infinite loop
    // where Container grows -> List renders more -> Container grows.
    const limit = maxLines ?? effectiveMaxHeight ?? ACTIVE_SHELL_MAX_LINES;
    const listHeight = Math.min(
      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
      (truncatedResultDisplay as AnsiOutput).length,
      limit,
    );

    return (
      <Box width={childWidth} flexDirection="column" maxHeight={listHeight}>
        <ScrollableList
          width={childWidth}
          // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
          data={truncatedResultDisplay as AnsiOutput}
          renderItem={renderVirtualizedAnsiLine}
          estimatedItemHeight={() => 1}
          keyExtractor={keyExtractor}
          initialScrollIndex={SCROLL_TO_ITEM_END}
          hasFocus={hasFocus}
        />
      </Box>
    );
  }

  // 3. Compute content node for non-virtualized paths
  // Check if string content is valid JSON and pretty-print it
  const prettyJSON =
    typeof truncatedResultDisplay === 'string'
      ? tryParseJSON(truncatedResultDisplay)
      : null;
  const formattedJSON = prettyJSON ? JSON.stringify(prettyJSON, null, 2) : null;

  let content: React.ReactNode;

  if (formattedJSON) {
    // Render pretty-printed JSON
    content = (
      <Text wrap="wrap" color={theme.text.primary}>
        {formattedJSON}
      </Text>
    );
  } else if (isSubagentProgress(truncatedResultDisplay)) {
    content = <SubagentProgressDisplay progress={truncatedResultDisplay} />;
  } else if (
    typeof truncatedResultDisplay === 'string' &&
    renderOutputAsMarkdown
  ) {
    content = (
      <MarkdownDisplay
        text={truncatedResultDisplay}
        terminalWidth={childWidth}
        renderMarkdown={renderMarkdown}
        isPending={false}
      />
    );
  } else if (
    typeof truncatedResultDisplay === 'string' &&
    !renderOutputAsMarkdown
  ) {
    content = (
      <Text wrap="wrap" color={theme.text.primary}>
        {truncatedResultDisplay}
      </Text>
    );
  } else if (
    typeof truncatedResultDisplay === 'object' &&
    'fileDiff' in truncatedResultDisplay
  ) {
    content = (
      <DiffRenderer
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        diffContent={(truncatedResultDisplay as FileDiffResult).fileDiff}
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        filename={(truncatedResultDisplay as FileDiffResult).fileName}
        availableTerminalHeight={effectiveMaxHeight}
        terminalWidth={childWidth}
      />
    );
  } else {
    const shouldDisableTruncation =
      isAlternateBuffer ||
      (availableTerminalHeight === undefined && maxLines === undefined);

    content = (
      <AnsiOutputText
        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
        data={truncatedResultDisplay as AnsiOutput}
        availableTerminalHeight={
          isAlternateBuffer ? undefined : effectiveMaxHeight
        }
        width={childWidth}
        maxLines={isAlternateBuffer ? undefined : maxLines}
        disableTruncation={shouldDisableTruncation}
      />
    );
  }

  // 4. Final render based on session mode
  if (isAlternateBuffer) {
    return (
      <Scrollable
        width={childWidth}
        maxHeight={maxLines ?? effectiveMaxHeight}
        hasFocus={hasFocus} // Allow scrolling via keyboard (Shift+Up/Down)
        scrollToBottom={true}
      >
        {content}
      </Scrollable>
    );
  }

  return (
    <Box width={childWidth} flexDirection="column">
      <MaxSizedBox
        maxHeight={effectiveMaxHeight}
        maxWidth={childWidth}
        additionalHiddenLinesCount={hiddenLinesCount}
      >
        {content}
      </MaxSizedBox>
    </Box>
  );
};
