import type { OpenAICompatClient, Message, ToolDef } from '../../llm/openai-compat.js';
import { consumeLlmStream, type ConsumedLLMResponse } from '../llm-stream.js';
import type { EventLogger } from '../../progress/event-log.js';
import { logger } from '../../logger.js';
import { TRANSITION_TOOL_NAME, COMPLETE_TOOL_NAME } from './terminal-control.js';
import type { AgentLoopCallbacks } from './types.js';
import type { MovementWatchdogs } from './watchdogs.js';

export interface LlmIterationResult extends ConsumedLLMResponse {
  /** Stream wall-clock time from request send to last chunk (ms). */
  llmDurationMs: number;
}

export interface RunLlmIterationArgs {
  client: OpenAICompatClient;
  messages: Message[];
  tools: ToolDef[];
  cancelSignal?: AbortSignal;
  callbacks?: AgentLoopCallbacks;
  /**
   * Mutated in place: the regular (non flow-control) tool names used so far in
   * this movement. The onToolUse hook appends newly-seen names, matching the
   * original inline behaviour.
   */
  toolsUsed: string[];
  /** Movement watchdogs; markToolUse is fired for every tool the LLM invokes. */
  watchdogs: MovementWatchdogs;
  movementName: string;
  iteration: number;
  /** Movement-scoped EventLogger (movementEvents). */
  events: EventLogger;
  userId?: string;
}

/**
 * Run one LLM call for the current iteration: emit the start/end trace events,
 * stream the response through consumeLlmStream (wiring the public callbacks and
 * filtering the hidden transition/complete control tools out of the UI-facing
 * channels), fire onLLMCall, and log the usage / text-preview summary.
 *
 * Extracted verbatim from executeMovement (agent-loop.ts) to slim the loop
 * body. Returns the consumed stream plus the call's wall-clock duration.
 */
export async function runLlmIteration(args: RunLlmIterationArgs): Promise<LlmIterationResult> {
  const {
    client,
    messages,
    tools,
    cancelSignal,
    callbacks,
    toolsUsed,
    watchdogs,
    movementName,
    iteration,
    events,
    userId,
  } = args;

  logger.info(`[agent-loop] movement=${movementName} sending LLM request (iteration=${iteration})`);
  // provider.timeoutMinutes に連動（デフォルト10分）。チャンク間の無応答がこの時間を超えたら接続断とみなす
  const idleTimeoutMs = client.timeoutMs > 0 ? client.timeoutMs : 10 * 60 * 1000;
  const llmStartedAt = Date.now();
  events.emit('llm_call_start', {
    iteration,
    messageCount: messages.length,
  });
  const consumed = await consumeLlmStream(
    client,
    messages,
    tools,
    cancelSignal,
    idleTimeoutMs,
    {
      onText: callbacks?.onText,
      onToolUse: (name, input, callId) => {
        if (name !== TRANSITION_TOOL_NAME && name !== COMPLETE_TOOL_NAME) {
          callbacks?.onToolUse?.(name, input, callId);
          if (!toolsUsed.includes(name)) toolsUsed.push(name);
        }
        watchdogs.markToolUse(name);
      },
      onToolCallDelta: (_index, callId, name, chunk) => {
        // Hidden control tools never stream to the UI.
        if (name && (name === TRANSITION_TOOL_NAME || name === COMPLETE_TOOL_NAME)) {
          return;
        }
        callbacks?.onToolCallDelta?.(callId, name, chunk);
      },
      onPromptProgress: (progress) => {
        callbacks?.onPromptProgress?.(progress);
      },
      // Phase A: surface proxy backend identity to the worker. Only
      // fires for proxy-mode clients that received x-litellm-model-id.
      onBackend: (backendId, cacheKey) => {
        callbacks?.onBackendResolved?.({ backendId, cacheKey });
      },
    },
    `movement=${movementName} `,
    { userId },
  );
  const llmDurationMs = Date.now() - llmStartedAt;
  const { accumulatedText, pendingToolCalls, hadError, lastUsage } = consumed;

  events.emit('llm_call_end', {
    iteration,
    durationMs: llmDurationMs,
    promptTokens: lastUsage?.prompt_tokens,
    completionTokens: lastUsage?.completion_tokens,
    toolCalls: pendingToolCalls.length,
    textChars: accumulatedText.length,
    hadError,
  });
  callbacks?.onLLMCall?.({
    iteration,
    durationMs: llmDurationMs,
    promptTokens: lastUsage?.prompt_tokens,
    completionTokens: lastUsage?.completion_tokens,
    toolCalls: pendingToolCalls.length,
    textChars: accumulatedText.length,
    hadError,
  });
  logger.info(`[agent-loop] movement=${movementName} LLM stream ended (iteration=${iteration}, hadError=${hadError}, ${llmDurationMs}ms${lastUsage ? ` in=${lastUsage.prompt_tokens} out=${lastUsage.completion_tokens}` : ''})`);

  // LLM 応答のサマリーログ
  logger.info(`[agent-loop] movement=${movementName} response: text=${accumulatedText.length}chars toolCalls=${pendingToolCalls.length} tools=[${pendingToolCalls.map((t) => t.function.name).join(',')}]`);
  if (accumulatedText.length > 0) {
    logger.info(`[agent-loop] movement=${movementName} text preview: ${accumulatedText.substring(0, 300)}`);
    callbacks?.onTextPreview?.(movementName, accumulatedText);
  }

  return { ...consumed, llmDurationMs };
}
