///|
/// One request as the runner sent it: the request document
/// (`docs/child-contract.md` §2). `prompt` is derived from `input` the
/// way `Workflow::agent` builds it: a `query` plus optional `hints`.
///
/// A shim READS one; `serve` gets it off the wire. A caller fabricating one
/// (a dialect's own tests) goes through `Request(...)`, so a later request
/// field costs no dialect anything.
pub struct Request {
  /// Echoed in the result, so the runner knows the result answers it.
  request_id : String
  kind : String
  /// `limits.max_steps`: the step ceiling the shim enforces.
  max_steps : Int?
  input : Json
  prompt : String
  /// The JSON Schema the report must satisfy, when the call asked for one;
  /// the shims hand it to the CLI's structured-output flag, and it takes
  /// precedence over a `--schema` file on argv.
  schema : Json?
}

///|
/// A request from its parts, for a caller that FABRICATES one instead of
/// reading it off the wire. The step ceiling and the schema are optional,
/// as on the wire.
pub fn Request::Request(
  request_id~ : String,
  kind~ : String,
  input~ : Json,
  prompt~ : String,
  max_steps? : Int,
  schema? : Json,
) -> Request {
  { request_id, kind, max_steps, input, prompt, schema, }
}

///|
/// Decode the request line: a version 1 request document with a
/// `request_id`, a `kind`, and an `input` that yields a prompt — a `query`
/// string with optional `hints`, a `prompt` string, or a bare string. A
/// present-but-malformed `limits.max_steps` or `schema` is refused, never
/// read as absent: a bad ceiling must not become an unbounded run.
fn Request::parse(line : String) -> Result[Request, String] {
  let trimmed = line.trim(chars="\r\n")
  guard (Some(@json.parse(trimmed.to_owned())) catch { _ => None })
    is Some(json) else {
    return Err("the request is not JSON")
  }
  guard json is Object(fields) else {
    return Err("the request is not a JSON object")
  }
  match fields.get("version") {
    Some(Number(version, ..)) if integral(version) is Some(1) => ()
    Some(Number(version, ..)) =>
      return Err("unsupported request version \{version}")
    _ => return Err("the request has no `version`")
  }
  guard fields.get("request_id") is Some(String(request_id)) else {
    return Err("the request has no `request_id` string")
  }
  guard fields.get("kind") is Some(String(kind)) else {
    return Err("the request has no `kind` string")
  }
  guard fields.get("input") is Some(input) else {
    return Err("the request has no `input`")
  }
  let max_steps = match fields.get("limits") {
    None => None
    Some({ "max_steps": Number(steps, ..), .. }) =>
      match integral(steps) {
        Some(steps) if steps >= 1 => Some(steps)
        _ => return Err("`limits.max_steps` must be an integer of at least 1")
      }
    Some({ "max_steps": _, .. }) =>
      return Err("`limits.max_steps` must be an integer of at least 1")
    Some(Object(_)) => None
    Some(_) => return Err("`limits` must be a JSON object")
  }
  let schema = match fields.get("schema") {
    None | Some(Null) => None
    Some(Object(_) as schema) => Some(schema)
    Some(_) => return Err("`schema` must be a JSON object")
  }
  guard prompt_of(input) is Some(prompt) else {
    return Err("`input` needs a `query` (or `prompt`) string")
  }
  Ok({ request_id, kind, max_steps, input, prompt, schema, })
}

///|
fn prompt_of(input : Json) -> String? {
  match input {
    String(text) if !text.is_blank() => Some(text)
    { "query": String(query), "hints": String(hints), .. } if !query.is_blank() &&
      !hints.is_blank() => Some("\{query}\n\nHints: \{hints}")
    { "query": String(query), .. } if !query.is_blank() => Some(query)
    { "prompt": String(prompt), .. } if !prompt.is_blank() => Some(prompt)
    _ => None
  }
}

///|
/// The `request_id` of a line `Request::parse` refused, when it names one:
/// the runner accepts a result only for its own request, so a `failed`
/// result can answer a malformed request only if its id is known.
fn request_id_of(line : String) -> String? {
  match
    (Some(@json.parse(line.trim(chars="\r\n").to_owned())) catch { _ => None }) {
    Some({ "request_id": String(request_id), .. }) => Some(request_id)
    _ => None
  }
}

///|
/// JSON has one number type; an `Int` counter arrives as a `Number`.
/// Reject a fractional value rather than truncating it.
pub fn integral(value : Double) -> Int? {
  let rounded = value.to_int()
  if rounded.to_double() == value {
    Some(rounded)
  } else {
    None
  }
}