///|
/// What a run cost, in the contract's counters, plus the CLI's own price
/// when it states one. A dialect that learned nothing reports no `Usage`
/// at all — an unknown cost is never a zero one.
pub struct Usage {
  /// Everything the model read: fresh input and cache reads and writes.
  prompt_tokens : Int
  completion_tokens : Int
  cache_hit_tokens : Int
  cache_miss_tokens : Int
  cost_usd : Double?
} derive(Eq, Debug)

///|
/// A usage reading from its parts; `cost_usd` only when the CLI stated it.
pub fn Usage::Usage(
  prompt_tokens~ : Int,
  completion_tokens~ : Int,
  cache_hit_tokens~ : Int,
  cache_miss_tokens~ : Int,
  cost_usd? : Double,
) -> Usage {
  {
    prompt_tokens,
    completion_tokens,
    cache_hit_tokens,
    cache_miss_tokens,
    cost_usd,
  }
}

///|
/// Two readings together. The price of the sum is known only when both
/// prices are: a sum that left one out would under-report it.
pub fn Usage::add(self : Usage, other : Usage) -> Usage {
  {
    prompt_tokens: self.prompt_tokens + other.prompt_tokens,
    completion_tokens: self.completion_tokens + other.completion_tokens,
    cache_hit_tokens: self.cache_hit_tokens + other.cache_hit_tokens,
    cache_miss_tokens: self.cache_miss_tokens + other.cache_miss_tokens,
    cost_usd: match (self.cost_usd, other.cost_usd) {
      (Some(left), Some(right)) => Some(left + right)
      _ => None
    },
  }
}

///|
/// How a shim run ended: the result-file statuses a shim can reach
/// (`docs/child-contract.md` §3). The engine-side statuses a CLI shim
/// cannot observe (`context_yield`, `aborted`) have no variant.
pub(all) enum Status {
  /// The CLI finished; the payload is the report, the result's `output`.
  Completed(Json)
  /// The CLI exited cleanly without an answer.
  NoReport
  /// The step ceiling stopped the run.
  MaxStepsExhausted
  /// The parent cancelled the run by closing stdin.
  Interrupted(String)
  /// The run could not finish: why.
  Failed(String)
} derive(Eq, Debug)

///|
/// Everything a shim reports about one run: how it ended, what it cost when
/// the CLI said, and how many steps the dialect counted.
struct ShimResult {
  status : Status
  usage : Usage?
  steps : Int?
} derive(Eq, Debug)

///|
/// A result from its parts. Omit `usage` when the CLI reported none, and
/// `steps` when the run never got to count any.
pub fn ShimResult::ShimResult(
  status : Status,
  usage? : Usage,
  steps? : Int,
) -> ShimResult {
  { status, usage, steps, }
}

///|
/// The result document answering request `request_id`, as `serve` writes
/// it: `usage` and `steps` only when known, `total_tokens` derived.
pub fn ShimResult::to_json(self : ShimResult, request_id~ : String) -> Json {
  let (status, output, reason) : (String, Json?, String?) = match self.status {
    Completed(report) => ("completed", Some(report), None)
    NoReport => ("no_report", None, None)
    MaxStepsExhausted => ("max_steps_exhausted", None, None)
    Interrupted(why) => ("interrupted", None, Some(why))
    Failed(why) => ("failed", None, Some(why))
  }
  let fields : Map[String, Json] = {
    "version": 1,
    "request_id": Json(request_id),
    "status": Json(status),
  }
  if output is Some(output) {
    fields["output"] = output
  }
  if reason is Some(reason) {
    fields["reason"] = Json(reason)
  }
  if self.usage is Some(usage) {
    let counters : Map[String, Json] = {
      "prompt_tokens": Json(usage.prompt_tokens),
      "completion_tokens": Json(usage.completion_tokens),
      "total_tokens": Json(usage.prompt_tokens + usage.completion_tokens),
      "prompt_cache_hit_tokens": Json(usage.cache_hit_tokens),
      "prompt_cache_miss_tokens": Json(usage.cache_miss_tokens),
    }
    if usage.cost_usd is Some(cost) {
      counters["cost_usd"] = Json(cost)
    }
    fields["usage"] = Json::object(counters)
  }
  if self.steps is Some(steps) {
    fields["steps"] = Json(steps)
  }
  Json::object(fields)
}

///|
/// Write the result file so that a reader sees either no file or the whole
/// result: into a uniquely named sibling, created exclusively so no other
/// writer's file is ever touched, then renamed over `path`. A failure after
/// the sibling exists removes it.
async fn write_result(
  path : String,
  request_id : String,
  result : ShimResult,
) -> Unit {
  let text = "\{result.to_json(request_id~).stringify()}\n"
  let sibling = "\{path}.\{nonce()}.tmp"
  let file = @fs.open(sibling, mode=WriteOnly, create_mode=CreateNew)
  errdefer (@fs.remove(sibling) catch { _ => () })
  {
    defer file.close()
    file.write(text)
  }
  @fs.rename(sibling, path, replace=true)
}

///|
/// A name part no other writer is likely to pick: platform entropy when
/// there is some, the clock otherwise. Exclusive creation, not this, is
/// what keeps two writers apart.
fn nonce() -> String {
  guard @env.rand(8) is Some(bytes) else { return "\{@env.now()}" }
  let digits = "0123456789abcdef"
  let hex = StringBuilder()
  for byte in bytes {
    hex.write_char(digits[byte.to_int() >> 4].unsafe_to_char())
    hex.write_char(digits[byte.to_int() & 15].unsafe_to_char())
  }
  hex.to_string()
}