///|
/// JsonChatToolCall represents a function call in JSON chat loop.
pub(all) struct JsonChatToolCall {
  id : String
  name : String
  arguments : Json
}

///|
pub fn JsonChatToolCall::new(
  id : String,
  name : String,
  arguments : Json,
) -> JsonChatToolCall {
  { id, name, arguments }
}

///|
/// JsonChatResponse is the per-step response from a model callback.
pub(all) struct JsonChatResponse {
  content : String
  tool_calls : Array[JsonChatToolCall]
  reasoning_content : String?
}

///|
pub fn JsonChatResponse::new(
  content : String,
  tool_calls : Array[JsonChatToolCall],
  reasoning_content : String?,
) -> JsonChatResponse {
  { content, tool_calls, reasoning_content }
}

///|
/// JsonChatLoopResult is the final result of run_json_chat_loop.
pub(all) struct JsonChatLoopResult {
  final_content : String
  messages : Array[Json]
  iterations : Int
}

///|
pub fn run_json_chat_loop(
  messages : Array[Json],
  tools : Array[Json],
  max_iterations : Int,
  chat : (Array[Json], Array[Json]) -> JsonChatResponse,
  execute_tool : (String, Json) -> String,
  fallback_content? : String = "I've completed processing but have no response to give.",
) -> JsonChatLoopResult {
  let current_messages = messages
  let mut iteration = 0
  let mut final_content : String? = None
  while iteration < max_iterations {
    iteration = iteration + 1
    let response = chat(current_messages, tools)
    if response.tool_calls.is_empty() {
      final_content = Some(response.content)
      break
    }
    current_messages.push(
      build_assistant_message(
        response.content,
        response.tool_calls,
        response.reasoning_content,
      ),
    )
    for tc in response.tool_calls {
      let result = execute_tool(tc.name, tc.arguments)
      current_messages.push(build_tool_result_message(tc.id, tc.name, result))
    }
  }
  {
    final_content: match final_content {
      Some(c) => c
      None => fallback_content
    },
    messages: current_messages,
    iterations: iteration,
  }
}

///|
fn build_assistant_message(
  content : String,
  tool_calls : Array[JsonChatToolCall],
  reasoning_content : String?,
) -> Json {
  let msg : Map[String, Json] = {}
  msg["role"] = "assistant".to_json()
  msg["content"] = content.to_json()
  if not(tool_calls.is_empty()) {
    let arr : Array[Json] = []
    for tc in tool_calls {
      arr.push(tool_call_to_openai_json(tc))
    }
    msg["tool_calls"] = Json::array(arr)
  }
  match reasoning_content {
    Some(reasoning) =>
      if not(reasoning.is_empty()) {
        msg["reasoning_content"] = reasoning.to_json()
      }
    None => ()
  }
  Json::object(msg)
}

///|
fn build_tool_result_message(
  tool_call_id : String,
  tool_name : String,
  result : String,
) -> Json {
  {
    "role": "tool".to_json(),
    "tool_call_id": tool_call_id.to_json(),
    "name": tool_name.to_json(),
    "content": result.to_json(),
  }
}

///|
fn tool_call_to_openai_json(tc : JsonChatToolCall) -> Json {
  {
    "id": tc.id.to_json(),
    "type": "function".to_json(),
    "function": {
      "name": tc.name.to_json(),
      "arguments": tc.arguments.stringify().to_json(),
    },
  }
}