///|
priv struct AnthropicRequest {
  model : String
  max_tokens : Int
  messages : Array[AnthropicMessage]
  system : String?
  tools : Array[AnthropicTool]?
  stream : Bool?
}

///|
impl FromJson for AnthropicRequest with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> AnthropicRequest raise @json.JsonDecodeError {
  guard json is Object(obj) else {
    raise JsonDecodeError((path, "AnthropicRequest: expected object"))
  }
  let model : String = match obj.get("model") {
    Some(String(s)) => s
    _ => raise JsonDecodeError((path, "AnthropicRequest: missing 'model'"))
  }
  let max_tokens : Int = match obj.get("max_tokens") {
    Some(Number(n, ..)) => n.trunc().to_int()
    _ => 4096
  }
  let messages : Array[AnthropicMessage] = match obj.get("messages") {
    Some(Array(arr)) => {
      let result : Array[AnthropicMessage] = []
      for m in arr {
        result.push(@json.from_json(m))
      }
      result
    }
    _ => raise JsonDecodeError((path, "AnthropicRequest: missing 'messages'"))
  }
  let system : String? = match obj.get("system") {
    Some(String(s)) => Some(s)
    _ => None
  }
  let tools : Array[AnthropicTool]? = match obj.get("tools") {
    Some(Array(arr)) => {
      let result : Array[AnthropicTool] = []
      for t in arr {
        result.push(@json.from_json(t))
      }
      Some(result)
    }
    _ => None
  }
  let stream : Bool? = match obj.get("stream") {
    Some(true) => Some(true)
    Some(false) => Some(false)
    _ => None
  }
  { model, max_tokens, messages, system, tools, stream }
}

///|
priv struct AnthropicMessage {
  role : String
  content : AnthropicContent
}

///|
impl FromJson for AnthropicMessage with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> AnthropicMessage raise @json.JsonDecodeError {
  guard json is Object(obj) else {
    raise JsonDecodeError((path, "AnthropicMessage: expected object"))
  }
  let role : String = match obj.get("role") {
    Some(String(s)) => s
    _ => raise JsonDecodeError((path, "AnthropicMessage: missing 'role'"))
  }
  let content : AnthropicContent = match obj.get("content") {
    Some(String(s)) => Text(s)
    Some(Array(arr)) => {
      let result : Array[AnthropicBlock] = []
      for b in arr {
        result.push(@json.from_json(b))
      }
      Blocks(result)
    }
    _ => raise JsonDecodeError((path, "AnthropicMessage: missing 'content'"))
  }
  { role, content }
}

///|
priv enum AnthropicContent {
  Text(String)
  Blocks(Array[AnthropicBlock])
}

///|
priv struct AnthropicBlock {
  type_ : String
  text : String?
}

///|
impl FromJson for AnthropicBlock with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> AnthropicBlock raise @json.JsonDecodeError {
  guard json is Object(obj) else {
    raise JsonDecodeError((path, "AnthropicBlock: expected object"))
  }
  let type_ : String = match obj.get("type") {
    Some(String(s)) => s
    _ => raise JsonDecodeError((path, "AnthropicBlock: missing 'type'"))
  }
  let text : String? = match obj.get("text") {
    Some(String(s)) => Some(s)
    _ => None
  }
  { type_, text }
}

///|
priv struct AnthropicTool {
  name : String
  description : String?
  input_schema : Json
}

///|
impl FromJson for AnthropicTool with fn from_json(
  json : Json,
  path : @json.JsonPath,
) -> AnthropicTool raise @json.JsonDecodeError {
  guard json is Object(obj) else {
    raise JsonDecodeError((path, "AnthropicTool: expected object"))
  }
  let name : String = match obj.get("name") {
    Some(String(s)) => s
    _ => raise JsonDecodeError((path, "AnthropicTool: missing 'name'"))
  }
  let description : String? = match obj.get("description") {
    Some(String(s)) => Some(s)
    _ => None
  }
  let input_schema : Json = match obj.get("input_schema") {
    Some(schema) => schema
    _ => raise JsonDecodeError((path, "AnthropicTool: missing 'input_schema'"))
  }
  { name, description, input_schema }
}

///|
priv struct AnthropicResponse {
  id : String
  type_ : String
  role : String
  content : Array[AnthropicResponseBlock]
  model : String
  stop_reason : String?
  stop_sequence : String?
  usage : AnthropicUsage
} derive(ToJson)

///|
priv struct AnthropicResponseBlock {
  type_ : String
  text : String?
  name : String?
  input : Json?
  id : String?
} derive(ToJson)

///|
priv struct AnthropicUsage {
  input_tokens : Int
  output_tokens : Int
  cache_read_input_tokens : Int?
  cache_creation_input_tokens : Int?
}

///|
impl ToJson for AnthropicUsage with fn to_json(self : AnthropicUsage) -> Json {
  let json : Map[String, Json] = {
    "input_tokens": Json::number(self.input_tokens.to_double()),
    "output_tokens": Json::number(self.output_tokens.to_double()),
  }
  if self.cache_read_input_tokens is Some(tokens) && tokens > 0 {
    json["cache_read_input_tokens"] = Json::number(tokens.to_double())
  }
  if self.cache_creation_input_tokens is Some(tokens) && tokens > 0 {
    json["cache_creation_input_tokens"] = Json::number(tokens.to_double())
  }
  Json::object(json)
}