import { LLMMessage, LLMResponse, LLMProviderConfig } from '../plugins/plugin_entry.js';
import { AnyAgentTool, ToolResultContent, toolResultToText } from '../tools/common.js';
import { getProvider } from './model_selection.js';
import {
  RuntimeProcessEvent,
  RuntimeProcessEventInput,
  buildOperationId,
  normalizeRuntimeProcessEvent,
  sanitizeProcessRecord,
} from './process_events.js';

export interface AttemptParams {
  messages: LLMMessage[];
  tools: AnyAgentTool[];
  config: LLMProviderConfig;
  maxToolRounds?: number;
  onToken?: (text: string) => void;
  onThinking?: (text: string) => void;
  onToolCall?: (name: string, args: Record<string, unknown>, toolCallId?: string) => void;
  onToolResult?: (name: string, result: string, toolCallId?: string, rawResult?: ToolResultContent | string) => void;
  onToolProcessEvent?: (input: {
    toolCallId?: string;
    operationId: string;
    toolName: string;
    args: Record<string, unknown>;
    event: RuntimeProcessEvent;
  }) => void;
  beforeToolCall?: (input: {
    toolCallId?: string;
    name: string;
    label?: string;
    args: Record<string, unknown>;
    tool?: AnyAgentTool;
    messages: LLMMessage[];
  }) => Promise<{ args?: Record<string, unknown>; block?: boolean; reason?: string } | void>;
  afterToolCall?: (input: {
    toolCallId?: string;
    name: string;
    label?: string;
    args: Record<string, unknown>;
    result: string;
    rawResult?: ToolResultContent | string;
    isError: boolean;
    tool?: AnyAgentTool;
    messages: LLMMessage[];
  }) => Promise<{ result?: string; rawResult?: ToolResultContent | string } | void>;
  onClarifyRequest?: (question: string, choices?: string[]) => Promise<string>;
}

export interface AttemptResult {
  messages: LLMMessage[];
  finalContent: string;
  usage: { inputTokens: number; outputTokens: number };
  toolCallCount: number;
  thinking?: string;
}

export async function runAttempt(params: AttemptParams): Promise<AttemptResult> {
  const {
    messages,
    tools,
    config,
    maxToolRounds = 20,
    onToken,
    onThinking,
    onToolCall,
    onToolResult,
    onToolProcessEvent,
    beforeToolCall,
    afterToolCall,
    onClarifyRequest,
  } = params;

  const provider = getProvider(config.provider);
  const conversation = [...messages];
  let totalInput = 0;
  let totalOutput = 0;
  let toolCallCount = 0;
  const shouldPreserveIntermediateContent = config.channelSupplier?.toLowerCase() === 'minimax';
  const visibleContentParts: string[] = [];
  const thinkingParts: string[] = [];

  for (let round = 0; round < maxToolRounds; round++) {
    let roundThinking = '';
    const response: LLMResponse = await provider.chat(conversation, tools, config, {
      onTextDelta: onToken,
      onThinkingDelta: text => {
        roundThinking += text;
        onThinking?.(text);
      },
    });
    if (!roundThinking && response.thinking) {
      roundThinking = response.thinking;
      onThinking?.(response.thinking);
    }
    if (roundThinking) {
      thinkingParts.push(roundThinking);
    }
    if (response.content.trim()) {
      visibleContentParts.push(response.content);
    }

    totalInput += response.usage?.inputTokens ?? 0;
    totalOutput += response.usage?.outputTokens ?? 0;

    if (!response.toolCalls?.length) {
      conversation.push({ role: 'assistant', content: response.content });
      return {
        messages: conversation,
        finalContent: shouldPreserveIntermediateContent
          ? visibleContentParts.join('\n\n')
          : response.content,
        usage: { inputTokens: totalInput, outputTokens: totalOutput },
        toolCallCount,
        thinking: thinkingParts.join('\n\n') || undefined,
      };
    }

    conversation.push({
      role: 'assistant',
      content: response.content,
      toolCalls: response.toolCalls,
    });

    for (const tc of response.toolCalls) {
      toolCallCount++;
      const tool = tools.find(t => t.name === tc.name);
      const visibility = tool?.visibility ?? 'tool';
      const operationId = buildOperationId(tc.id);

      let parsedArgs: Record<string, unknown>;
      try {
        parsedArgs = JSON.parse(tc.arguments);
      } catch {
        conversation.push({
          role: 'assistant',
          content: 'Tool call arguments were truncated before completing valid JSON. Please retry with shorter content or split the file write into smaller chunks.',
        });
        return {
          messages: conversation,
          finalContent: shouldPreserveIntermediateContent
            ? visibleContentParts.join('\n\n')
            : response.content,
          usage: { inputTokens: totalInput, outputTokens: totalOutput },
          toolCallCount,
          thinking: thinkingParts.join('\n\n') || undefined,
        };
      }
      const beforeResult = await beforeToolCall?.({
        toolCallId: tc.id,
        name: tc.name,
        label: tool?.label,
        args: parsedArgs,
        tool,
        messages: [...conversation],
      });
      const before: { args?: Record<string, unknown>; block?: boolean; reason?: string } = beforeResult || {};
      const effectiveArgs = before.args ?? parsedArgs;

      if (visibility !== 'internal') {
        onToolCall?.(tc.name, effectiveArgs, tc.id);
      }

      let resultText: string;
      let rawResult: ToolResultContent | string | undefined;
      let isError = false;

      if (before.block) {
        resultText = before.reason?.trim()
          ? `Tool call blocked: ${before.reason}`
          : 'Tool call blocked';
        isError = true;
      } else if (tc.name === 'clarify' && onClarifyRequest) {
        try {
          resultText = await onClarifyRequest(
            typeof effectiveArgs.question === 'string' ? effectiveArgs.question : '',
            Array.isArray(effectiveArgs.choices) ? effectiveArgs.choices as string[] : undefined
          );
        } catch {
          resultText = 'User did not answer clarify request; continue with best effort.';
          isError = true;
        }
      } else if (!tool) {
        resultText = `Error: tool "${tc.name}" does not exist`;
        isError = true;
      } else {
        try {
          const emitProcessEvent = (input: RuntimeProcessEventInput) => {
            const event = normalizeRuntimeProcessEvent({
              targetType: 'tool',
              source: 'tool',
              ...input,
              operationId,
            });
            try {
              onToolProcessEvent?.({
                toolCallId: tc.id,
                operationId,
                toolName: tc.name,
                args: sanitizeProcessRecord(effectiveArgs),
                event,
              });
            } catch {
              // Process events are observational only; callback failures must not fail the tool.
            }
          };
          const result = await tool.execute(tc.id, effectiveArgs, emitProcessEvent);
          rawResult = result as ToolResultContent | string;
          resultText = toolResultToText(result);
        } catch (err: any) {
          resultText = `Tool execution error: ${err.message}`;
          isError = true;
        }
      }

      const afterResult = await afterToolCall?.({
        toolCallId: tc.id,
        name: tc.name,
        label: tool?.label,
        args: effectiveArgs,
        result: resultText,
        rawResult,
        isError,
        tool,
        messages: [...conversation],
      });
      const after: { result?: string; rawResult?: ToolResultContent | string } = afterResult || {};
      resultText = after.result ?? resultText;
      rawResult = after.rawResult ?? rawResult;

      if (visibility !== 'internal') {
        onToolResult?.(tc.name, resultText, tc.id, rawResult);
      }

      conversation.push({ role: 'tool', content: resultText, toolCallId: tc.id });
    }
  }

  const lastAssistant = conversation.filter(m => m.role === 'assistant').pop();
  return {
    messages: conversation,
    finalContent: shouldPreserveIntermediateContent
      ? visibleContentParts.join('\n\n') || lastAssistant?.content || 'Reached max tool rounds'
      : lastAssistant?.content || 'Reached max tool rounds',
    usage: { inputTokens: totalInput, outputTokens: totalOutput },
    toolCallCount,
    thinking: thinkingParts.join('\n\n') || undefined,
  };
}
