/**
 * Shared chat utilities for DotAgents apps (desktop and mobile)
 * 
 * These utilities provide consistent behavior for chat UI features
 * across both platforms while allowing platform-specific rendering.
 */

import type { AgentUserResponseEvent } from './agent-progress';
import { ToolCall, ToolResult } from './types';

export type ToolArgumentEntry = {
  key: string;
  value: unknown;
};

const COLLAPSE_THRESHOLD = 200;
const MARKDOWN_IMAGE_PAYLOAD_REGEX = /!\[[^\]]*\]\((?:data:image\/|https?:\/\/|assets:\/\/conversation-image\/)[^)]*\)/gi;
const EXEC_COMMAND_RESULT_PREVIEW_MAX_LENGTH = 96;

/**
 * Determine if a message should be collapsible based on its content
 * @param content The message content
 * @param toolCalls Optional array of tool calls
 * @param toolResults Optional array of tool results
 * @returns True if the message should be collapsible
 */
export function shouldCollapseMessage(
  content: string | undefined,
  toolCalls?: ToolCall[],
  toolResults?: ToolResult[]
): boolean {
  const hasExtras = (toolCalls?.length ?? 0) > 0 || (toolResults?.length ?? 0) > 0;
  const contentLength = content?.replace(MARKDOWN_IMAGE_PAYLOAD_REGEX, '').length ?? 0;
  return contentLength > COLLAPSE_THRESHOLD || hasExtras;
}

/**
 * Generate a summary of tool calls for collapsed view
 * @param toolCalls Array of tool calls
 * @returns A formatted string showing only tool names
 */
export function getToolCallsSummary(toolCalls: ToolCall[]): string {
  if (!toolCalls || toolCalls.length === 0) return '';
  return toolCalls.map(tc => getToolCallPreview(tc)).join(', ');
}

/**
 * Generate a compact single-token label for a collapsed tool call.
 * Details belong in expanded tool views, not collapsed rows.
 *
 * For `execute_command`, the first command token (e.g. `git`, `pnpm`) is
 * preferred over the generic tool name so collapsed group previews still
 * communicate which command was run.
 */
export function getToolCallPreview(toolCall: ToolCall): string {
  const toolName = toolCall.name?.trim() || '';
  const normalizedName = toolName.toLowerCase();
  if (normalizedName === 'execute_command' || normalizedName.endsWith(':execute_command')) {
    const args = normalizeToolArguments(toolCall.arguments);
    const command = args?.command;
    if (typeof command === 'string') {
      const firstToken = command.trim().split(/\s+/)[0];
      if (firstToken) return firstToken;
    }
  }
  return toolName.replace(/\s+/g, '_') || 'tool';
}

/**
 * Generate a collapsed preview for an individual tool row.
 * Grouped tool previews intentionally stay tool-name-only via getToolCallPreview.
 */
export function getIndividualToolCallPreview(toolCall: ToolCall): string {
  const toolName = toolCall.name?.trim() || '';
  const normalizedName = toolName.toLowerCase();
  if (normalizedName === 'execute_command' || normalizedName.endsWith(':execute_command')) {
    const args = normalizeToolArguments(toolCall.arguments);
    const command = args?.command;
    if (typeof command === 'string' && command.trim()) {
      return command.replace(/\s+/g, ' ').trim();
    }
  }

  return getToolCallPreview(toolCall);
}

type StructuredCommandOutput = {
  found: boolean;
  output: string;
};

function extractStringFields(obj: Record<string, unknown>, keys: string[]): string[] {
  const values: string[] = [];
  for (const key of keys) {
    const value = obj[key];
    if (typeof value === 'string' && value.trim()) values.push(value.trim());
  }
  return values;
}

function extractStructuredCommandOutput(raw: string): StructuredCommandOutput | null {
  const trimmed = raw.trim();
  if (!trimmed) return null;

  const candidates = [trimmed];
  const objectStart = trimmed.indexOf('{');
  const objectEnd = trimmed.lastIndexOf('}');
  if (objectStart > 0 && objectEnd > objectStart) {
    candidates.push(trimmed.slice(objectStart, objectEnd + 1));
  }

  for (const candidate of candidates) {
    try {
      const parsed = JSON.parse(candidate) as unknown;
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;

      const obj = parsed as Record<string, unknown>;
      const looksLikeCommandResult =
        'stdout' in obj || 'stderr' in obj || 'exitCode' in obj || 'command' in obj || 'cwd' in obj;
      if (!looksLikeCommandResult) continue;

      const streamOutput = extractStringFields(obj, ['stdout', 'stderr']);
      const fallbackOutput = extractStringFields(obj, ['output', 'result', 'message', 'error']);

      return {
        found: true,
        output: (streamOutput.length > 0 ? streamOutput : fallbackOutput).join('\n'),
      };
    } catch {
      // Try the next candidate. Plain-text tool results are handled by the caller.
    }
  }

  return null;
}

/**
 * Generate a summary of tool results for collapsed view
 * @param toolResults Array of tool results
 * @returns A formatted string showing result status and key information
 */
export function getToolResultsSummary(toolResults: ToolResult[]): string {
  if (!toolResults || toolResults.length === 0) return '';
  const allSuccess = toolResults.every(r => r.success);
  const icon = allSuccess ? '✅' : '⚠️';
  const count = toolResults.length;

  if (count === 1) {
    const preview = generateToolResultPreview(toolResults[0]);
    if (preview) {
      return `${icon} ${preview}`;
    }
  }

  const previews = toolResults
    .map(r => generateToolResultPreview(r))
    .filter(Boolean)
    .slice(0, 2);

  if (previews.length > 0) {
    const suffix = count > previews.length ? ` (+${count - previews.length} more)` : '';
    return `${icon} ${previews.join(', ')}${suffix}`;
  }

  return `${icon} ${count} result${count > 1 ? 's' : ''}`;
}

function normalizeCompactPreviewText(text: string): string {
  return text.replace(/\s+/g, ' ').trim();
}

function truncatePreviewToChars(text: string, maxLength: number): string {
  if (!text || maxLength <= 0) return '';
  if (text.length <= maxLength) return text;
  if (maxLength <= 3) return text.slice(0, maxLength);
  return `${text.slice(0, maxLength - 3).trimEnd()}...`;
}

function buildBalancedCommandResultPreview(command: string, output: string): string {
  const commandText = normalizeCompactPreviewText(command);
  const outputText = normalizeCompactPreviewText(output);
  if (!outputText) return commandText;

  const fullPreview = `${commandText}:${outputText}`;
  if (fullPreview.length <= EXEC_COMMAND_RESULT_PREVIEW_MAX_LENGTH) return fullPreview;

  const availableChars = EXEC_COMMAND_RESULT_PREVIEW_MAX_LENGTH - 1;
  const commandBaseBudget = Math.floor(availableChars / 2);
  const outputBaseBudget = availableChars - commandBaseBudget;
  const unusedCommandBudget = Math.max(0, commandBaseBudget - commandText.length);
  const unusedOutputBudget = Math.max(0, outputBaseBudget - outputText.length);
  const commandBudget = Math.min(commandText.length, commandBaseBudget + unusedOutputBudget);
  const outputBudget = Math.min(outputText.length, outputBaseBudget + unusedCommandBudget);

  return `${truncatePreviewToChars(commandText, commandBudget)}:${truncatePreviewToChars(outputText, outputBudget)}`;
}

/**
 * Compact character-budgeted `<command>:<output>` preview for an
 * `execute_command` tool call. Used in collapsed tool-activity groups and
 * single-line execute_command rows where space is at a premium.
 *
 * Returns null when the call is not an execute_command or has no command. When
 * a result is supplied, the preview balances the character budget between the
 * command input and stdout/stderr output, lending unused space to the longer
 * side so short commands still leave room for useful output detail.
 */
export function getExecuteCommandResultPreview(
  toolCall: ToolCall,
  toolResult?: ToolResult | null,
): string | null {
  const toolName = toolCall.name?.trim().toLowerCase() || '';
  const isExecute = toolName === 'execute_command' || toolName.endsWith(':execute_command');
  if (!isExecute) return null;

  const args = normalizeToolArguments(toolCall.arguments);
  const command = typeof args?.command === 'string' ? args.command.trim() : '';
  if (!command) return null;
  const firstWord = command.split(/\s+/)[0];
  if (!firstWord) return null;
  if (!toolResult) return firstWord;

  const rawOutput = (() => {
    const outputParts: string[] = [];
    for (const rawPart of [toolResult.content, toolResult.error]) {
      if (!rawPart?.trim()) continue;

      const structured = extractStructuredCommandOutput(rawPart);
      if (structured?.found) {
        if (structured.output.trim()) outputParts.push(structured.output);
        continue;
      }

      outputParts.push(rawPart);
    }

    return Array.from(new Set(outputParts.map(part => part.trim()).filter(Boolean))).join('\n');
  })();
  const output = rawOutput.trim();
  if (!output) return normalizeCompactPreviewText(command) || firstWord;

  return buildBalancedCommandResultPreview(command, output);
}

/**
 * Generate the compact label shown for a single collapsed tool execution row.
 *
 * - execute_command prefers `<command>:<output-preview>` when a result exists.
 * - Other tools prefer `<tool>:<result-preview>` when a meaningful result exists.
 * - Pending tools fall back to the richer per-tool call preview.
 */
export function getCompactToolExecutionPreview(
  toolCall: ToolCall,
  toolResult?: ToolResult | null,
): string {
  const executePreview = getExecuteCommandResultPreview(toolCall, toolResult);
  if (executePreview && toolResult) return executePreview;

  if (toolResult) {
    const resultPreview = generateToolResultPreview(toolResult);
    if (resultPreview) {
      const toolLabel = getToolCallPreview(toolCall);
      if (toolLabel) return `${toolLabel}:${resultPreview}`;
    }
  }

  return getIndividualToolCallPreview(toolCall);
}

/**
 * Generate a preview string for a single tool result.
 * @param result Tool result to preview
 * @returns A short preview string or empty string if no meaningful preview
 */
function generateToolResultPreview(result: ToolResult): string {
  if (!result) return '';

  if (!result.success) {
    const errorText = result.error || result.content || 'Error';
    return truncatePreview(errorText, 40);
  }

  const content = result.content || '';
  if (!content) return '';

  try {
    const parsed = JSON.parse(content);
    return extractJsonPreview(parsed);
  } catch {
    return extractTextPreview(content);
  }
}

/**
 * Extract a preview from a parsed JSON object
 */
function extractJsonPreview(data: unknown): string {
  if (data === null || data === undefined) return '';

  if (Array.isArray(data)) {
    const len = data.length;
    if (len === 0) return 'empty list';

    const firstItem = data[0];
    if (typeof firstItem === 'object' && firstItem !== null) {
      const item = firstItem as Record<string, unknown>;
      const getString = (value: unknown): string | null => {
        return typeof value === 'string' ? value : null;
      };
      const name = getString(item.name) || getString(item.title) || getString(item.path) || getString(item.filename);
      if (name) {
        return len === 1 ? truncatePreview(name, 30) : `${len} items: ${truncatePreview(name, 20)}...`;
      }
    }
    return `${len} item${len > 1 ? 's' : ''}`;
  }

  if (typeof data === 'object') {
    const obj = data as Record<string, unknown>;

    if ('success' in obj && typeof obj.success === 'boolean') {
      if ('message' in obj && typeof obj.message === 'string') {
        return truncatePreview(obj.message, 50);
      }
      if ('result' in obj) {
        return extractJsonPreview(obj.result);
      }
    }

    if ('path' in obj || 'file' in obj || 'filename' in obj) {
      const path = obj.path || obj.file || obj.filename;
      return truncatePreview(String(path), 40);
    }

    if ('content' in obj && typeof obj.content === 'string') {
      return truncatePreview(obj.content, 50);
    }

    if ('data' in obj) {
      return extractJsonPreview(obj.data);
    }

    if ('count' in obj && typeof obj.count === 'number') {
      return `${obj.count} item${obj.count !== 1 ? 's' : ''}`;
    }

    if ('items' in obj && Array.isArray(obj.items)) {
      return extractJsonPreview(obj.items);
    }
    if ('results' in obj && Array.isArray(obj.results)) {
      return extractJsonPreview(obj.results);
    }

    const keys = Object.keys(obj);
    if (keys.length > 0) {
      const firstKey = keys[0];
      const firstValue = obj[firstKey];
      if (typeof firstValue === 'string' || typeof firstValue === 'number' || typeof firstValue === 'boolean') {
        return `${firstKey}: ${truncatePreview(String(firstValue), 30)}`;
      }
      return `${keys.length} field${keys.length > 1 ? 's' : ''}`;
    }
  }

  if (typeof data === 'string') {
    return truncatePreview(data, 50);
  }
  if (typeof data === 'number' || typeof data === 'boolean') {
    return String(data);
  }

  return '';
}

/**
 * Extract a preview from plain text content
 */
function extractTextPreview(content: string): string {
  if (!content) return '';

  const cleaned = content.trim();
  const lines = cleaned.split('\n').filter(l => l.trim());

  if (lines.length > 1) {
    const firstLine = lines[0].trim();
    const cleanedLine = firstLine.replace(/^(successfully|done|completed|created|updated|deleted|read|wrote|found|error:?)\s*/i, '');
    return truncatePreview(cleanedLine || firstLine, 50);
  }

  if (cleaned.length <= 50) {
    return cleaned.replace(/\n/g, ' ').trim();
  }

  if (lines.length > 0) {
    const firstLine = lines[0].trim();
    const cleanedLine = firstLine.replace(/^(successfully|done|completed|created|updated|deleted|read|wrote|found|error:?)\s*/i, '');
    return truncatePreview(cleanedLine || firstLine, 50);
  }

  return truncatePreview(cleaned, 50);
}

/**
 * Truncate a string to a maximum length with ellipsis
 */
function truncatePreview(text: string, maxLength: number): string {
  if (!text) return '';
  const cleaned = text.replace(/\n/g, ' ').trim();
  if (cleaned.length <= maxLength) return cleaned;
  return cleaned.slice(0, maxLength - 3) + '...';
}

// ============================================================================
// Tool Argument Formatting
// ============================================================================

/**
 * Format tool arguments as pretty-printed JSON
 * @param args Tool call arguments object
 * @returns Formatted JSON string with 2-space indentation
 */
export function formatToolArguments(args: unknown): string {
  if (args === null || args === undefined) return '';
  const normalizedArgs = parseJsonStringIfPossible(args);
  try {
    if (typeof normalizedArgs === 'string') return normalizedArgs;
    return JSON.stringify(normalizedArgs, null, 2);
  } catch {
    return String(args);
  }
}

function parseJsonStringIfPossible(value: unknown): unknown {
  if (typeof value !== 'string') return value;

  const trimmed = value.trim();
  if (!trimmed || (!trimmed.startsWith('{') && !trimmed.startsWith('['))) return value;

  try {
    return JSON.parse(trimmed);
  } catch {
    return value;
  }
}

/**
 * Normalize tool arguments into an object suitable for field-by-field rendering.
 * Accepts either an object or a JSON string containing an object.
 */
export function normalizeToolArguments(args: unknown): Record<string, unknown> | null {
  const normalizedArgs = parseJsonStringIfPossible(args);
  if (!normalizedArgs || typeof normalizedArgs !== 'object' || Array.isArray(normalizedArgs)) {
    return null;
  }
  return normalizedArgs as Record<string, unknown>;
}

/**
 * Return normalized tool argument entries in insertion order for UI renderers.
 */
export function getToolArgumentEntries(args: unknown): ToolArgumentEntry[] {
  const normalizedArgs = normalizeToolArguments(args);
  return normalizedArgs ? Object.entries(normalizedArgs).map(([key, value]) => ({ key, value })) : [];
}

/**
 * Format tool arguments as a compact preview for collapsed view.
 * Shows key parameter names and truncated values.
 * @param args Tool call arguments object
 * @returns A compact preview string like "path: /foo/bar, content: Hello..."
 */
export function formatArgumentsPreview(args: unknown): string {
  const normalizedArgs = normalizeToolArguments(args);
  if (!normalizedArgs) return '';
  const entries = Object.entries(normalizedArgs);
  if (entries.length === 0) return '';

  const preview = entries.slice(0, 3).map(([key, value]) => {
    let displayValue: string;
    if (typeof value === 'string') {
      displayValue = truncatePreview(value, 30);
    } else if (typeof value === 'object') {
      displayValue = value === null ? 'null' : Array.isArray(value) ? `[${value.length} items]` : '{...}';
    } else {
      displayValue = String(value);
    }
    return `${key}: ${displayValue}`;
  }).join(', ');

  if (entries.length > 3) {
    return preview + ` (+${entries.length - 3} more)`;
  }
  return preview;
}

// ============================================================================
// respond_to_user Content Extraction
// ============================================================================

/** The tool name used to explicitly respond to the user */
export const RESPOND_TO_USER_TOOL = 'respond_to_user';
export const MARK_WORK_COMPLETE_TOOL = 'mark_work_complete';

const sanitizeRespondToUserMarkdownLabel = (label: string) => label.replace(/[\[\]\(\)`\\]/g, '').trim();

function isCompletionControlTool(name: string | undefined): boolean {
  return name === RESPOND_TO_USER_TOOL || name === MARK_WORK_COMPLETE_TOOL;
}

/**
 * Extract text content from respond_to_user tool call arguments
 * @param args Tool call arguments
 * @returns Extracted text content or null if not valid
 */
export function extractRespondToUserContentFromArgs(args: unknown): string | null {
  if (!args || typeof args !== 'object') return null;

  const parsedArgs = args as Record<string, unknown>;
  const text = typeof parsedArgs.text === 'string' ? parsedArgs.text.trim() : '';
  const images = Array.isArray(parsedArgs.images) ? parsedArgs.images : [];
  const videos = Array.isArray(parsedArgs.videos) ? parsedArgs.videos : [];

  const imagesMd = images
    .map((img, index) => {
      if (!img || typeof img !== 'object') return '';

      const image = img as Record<string, unknown>;
      const alt = typeof image.alt === 'string' && image.alt.trim().length > 0
        ? image.alt.trim()
        : typeof image.altText === 'string' && image.altText.trim().length > 0
          ? image.altText.trim()
          : `Image ${index + 1}`;
      const safeAlt = sanitizeRespondToUserMarkdownLabel(alt) || `Image ${index + 1}`;

      const url = typeof image.url === 'string' ? image.url.trim() : '';
      const dataUrl = typeof image.dataUrl === 'string' ? image.dataUrl.trim() : '';
      const mimeType = typeof image.mimeType === 'string' ? image.mimeType.trim() : '';
      const data = typeof image.data === 'string' ? image.data.trim() : '';
      const legacyDataUrl = mimeType && data ? `data:${mimeType};base64,${data}` : '';
      const uri = url || dataUrl || legacyDataUrl;

      if (uri) return `![${safeAlt}](${uri})`;
      return '';
    })
    .filter(Boolean)
    .join('\n\n');

  const videosMd = videos
    .map((video, index) => {
      if (!video || typeof video !== 'object') return '';

      const parsedVideo = video as Record<string, unknown>;
      const label = typeof parsedVideo.label === 'string' && parsedVideo.label.trim().length > 0
        ? parsedVideo.label.trim()
        : `Video ${index + 1}`;
      const safeLabel = sanitizeRespondToUserMarkdownLabel(label) || `Video ${index + 1}`;
      const url = typeof parsedVideo.url === 'string' ? parsedVideo.url.trim() : '';

      if (url) return `[${safeLabel}](${url})`;
      return '';
    })
    .filter(Boolean)
    .join('\n\n');

  const combined = [text, imagesMd, videosMd].filter(Boolean).join('\n\n').trim();
  return combined || null;
}

/**
 * Resolve a monotonic timestamp for each message, filling missing or invalid
 * timestamps relative to neighboring messages when possible.
 */
export function resolveMessageTimestamps(
  messages: Array<{
    timestamp?: number;
  }>,
): number[] {
  const resolved: Array<number | null> = messages.map((message) => (
    typeof message.timestamp === 'number' && Number.isFinite(message.timestamp)
      ? message.timestamp
      : null
  ));

  for (let index = 1; index < resolved.length; index += 1) {
    if (resolved[index] === null && resolved[index - 1] !== null) {
      resolved[index] = (resolved[index - 1] as number) + 1;
    }
  }

  for (let index = resolved.length - 2; index >= 0; index -= 1) {
    if (resolved[index] === null && resolved[index + 1] !== null) {
      resolved[index] = (resolved[index + 1] as number) - 1;
    }
  }

  for (let index = 0; index < resolved.length; index += 1) {
    if (resolved[index] === null) {
      resolved[index] = index;
    }
  }

  return resolved as number[];
}

/**
 * Extract ordered respond_to_user events from saved chat messages.
 * This preserves duplicates and order from the saved assistant tool calls.
 */
export function extractRespondToUserResponseEvents(
  messages: Array<{
    role: 'user' | 'assistant' | 'tool';
    timestamp?: number;
    toolCalls?: Array<{ name: string; arguments: unknown }>;
  }>,
  options?: {
    sessionId?: string;
    runId?: number;
    idPrefix?: string;
  },
): AgentUserResponseEvent[] {
  const events: AgentUserResponseEvent[] = [];
  const idPrefix = options?.idPrefix ?? 'history';
  const resolvedTimestamps = resolveMessageTimestamps(messages);

  for (let messageIndex = 0; messageIndex < messages.length; messageIndex += 1) {
    const message = messages[messageIndex];
    if (message.role !== 'assistant' || !message.toolCalls?.length) continue;

    for (let toolCallIndex = 0; toolCallIndex < message.toolCalls.length; toolCallIndex += 1) {
      const call = message.toolCalls[toolCallIndex];
      if (call.name !== RESPOND_TO_USER_TOOL) continue;
      const content = extractRespondToUserContentFromArgs(call.arguments);
      if (!content) continue;

      events.push({
        id: `${idPrefix}-${messageIndex}-${toolCallIndex}-${events.length + 1}`,
        sessionId: options?.sessionId ?? 'history',
        runId: options?.runId,
        ordinal: events.length + 1,
        text: content,
        timestamp: resolvedTimestamps[messageIndex],
      });
    }
  }

  return events;
}

/**
 * Check if a message is purely a tool call message (no user-facing content).
 * Used to determine if a message should be collapsed by default.
 * @param message A chat message object
 * @returns True if the message is only tool calls with no real content
 */
export function isToolOnlyMessage(message: {
  content?: string;
  toolCalls?: Array<{ name: string }>;
  toolResults?: Array<unknown>;
}): boolean {
  const hasToolCalls = (message.toolCalls?.length ?? 0) > 0;
  const hasToolResults = (message.toolResults?.length ?? 0) > 0;
  const hasContent = !!(message.content && message.content.trim().length > 0);

  // A message is "tool-only" if it has tool calls but no meaningful content
  // or only placeholder content like "Executing tools..."
  if (!hasToolCalls && !hasToolResults) return false;
  if (!hasContent) return true;

  const trimmedContent = message.content?.trim().toLowerCase() || '';
  const placeholderPhrases = [
    'executing tools...',
    'executing tools',
    'running tools...',
    'running tools',
  ];
  return placeholderPhrases.includes(trimmedContent);
}

export function isInternalCompletionControlMessage(message: {
  role: 'user' | 'assistant' | 'tool';
  content?: string;
  toolCalls?: Array<{ name: string }>;
  toolResults?: Array<unknown>;
}): boolean {
  if (message.role === 'assistant' && (message.toolCalls?.length ?? 0) > 0) {
    const onlyCompletionControlTools = message.toolCalls!.every((call) => isCompletionControlTool(call.name));
    if (onlyCompletionControlTools && isToolOnlyMessage(message)) {
      return true;
    }
  }

  if (message.role === 'tool') {
    const normalized = message.content?.trim().toLowerCase() || '';
    if (normalized.startsWith(`[${RESPOND_TO_USER_TOOL}]`) || normalized.startsWith(`[${MARK_WORK_COMPLETE_TOOL}]`)) {
      return true;
    }
  }

  return false;
}

export function filterVisibleChatMessages<
  T extends {
    role: 'user' | 'assistant' | 'tool';
    content?: string;
    toolCalls?: Array<{ name: string }>;
    toolResults?: Array<unknown>;
  },
>(messages: T[]): T[] {
  const filtered = messages.filter((message) => !isInternalCompletionControlMessage(message));
  return filtered.length > 0 ? filtered : messages;
}
