/**
 * The agent turn loop. Given a model `complete` fn, the conversation, the tool
 * declarations, and an `execute` that actually runs a tool, it drives the
 * call→tool→result→call cycle until the model answers in prose (or a step
 * budget is hit). Pure orchestration with injected I/O — the model and the tool
 * executor are both stubbable, so the loop is fully unit-testable offline.
 */

import type { ChatMessage, CompletionResult, ToolDecl } from './chat-client.js'

/** A tool the loop ran, with the raw args and its stringified result — for the UI's activity trail. */
export interface AgentStep {
  tool: string
  arguments: string
  result: string
}

export type ToolExecutor = (name: string, args: unknown) => Promise<unknown>

export interface AgentTurn {
  content: string
  steps: AgentStep[]
  /** The full conversation including this turn's assistant/tool messages. */
  messages: ChatMessage[]
}

const DEFAULT_MAX_STEPS = 4

function stringify(result: unknown): string {
  return typeof result === 'string' ? result : JSON.stringify(result)
}

export async function runAgentTurn(opts: {
  complete: (messages: ChatMessage[], tools?: ToolDecl[]) => Promise<CompletionResult>
  messages: ChatMessage[]
  tools: ToolDecl[]
  execute: ToolExecutor
  maxSteps?: number
}): Promise<AgentTurn> {
  const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS
  const convo: ChatMessage[] = [...opts.messages]
  const steps: AgentStep[] = []

  for (let step = 0; step < maxSteps; step++) {
    const { content, toolCalls } = await opts.complete(convo, opts.tools)
    if (toolCalls.length === 0) {
      convo.push({ role: 'assistant', content })
      return { content, steps, messages: convo }
    }
    convo.push({ role: 'assistant', content, toolCalls })
    for (const tc of toolCalls) {
      let result: unknown
      try {
        const args = tc.arguments ? JSON.parse(tc.arguments) : {}
        result = await opts.execute(tc.name, args)
      } catch (e) {
        result = { error: e instanceof Error ? e.message : String(e) }
      }
      const resultStr = stringify(result)
      steps.push({ tool: tc.name, arguments: tc.arguments, result: resultStr })
      convo.push({ role: 'tool', toolCallId: tc.id, name: tc.name, content: resultStr })
    }
  }

  // Out of tool budget — force a final prose answer with tools withheld.
  const final = await opts.complete(convo, [])
  convo.push({ role: 'assistant', content: final.content })
  return { content: final.content, steps, messages: convo }
}
