///|
/// Envelope fields supply the run/task identity missing from provider responses.
/// Each line must contain one final response or one error, never a stream chunk.
pub fn decode_ollama(value : Json) -> Call raise DecodeError {
  guard value is Object(obj) else {
    raise DecodeError("expected provider envelope")
  }
  guard obj.get("response") is Some(Object(response)) else {
    raise DecodeError("expected response object")
  }
  let model = required_string(obj, "model")
  let status = match response.get("error") {
    Some(String(_)) => "error"
    None => {
      if !(response.get("done") is Some(True)) {
        raise DecodeError("expected final done=true response")
      }
      if required_string(response, "model") != model {
        raise DecodeError("response model differs from envelope")
      }
      "ok"
    }
    _ => raise DecodeError("invalid provider error value")
  }
  {
    run_id: required_string(obj, "run_id"),
    task_id: required_string(obj, "task_id"),
    stage: required_string(obj, "stage"),
    call_id: required_string(obj, "call_id"),
    model,
    status,
    input_tokens: optional_tokens(response, "prompt_eval_count"),
    output_tokens: optional_tokens(response, "eval_count"),
  }
}

///|
/// Never includes response text in normalized records or diagnostic messages.
pub fn import_ollama(text : String) -> Parsed {
  let records : Array[Record] = []
  let findings : Array[Finding] = []
  for index, line in text.split("\n").to_array() {
    if line.trim().is_empty() {
      continue
    }
    let value = @json.parse(
      line,
      reject_duplicate_keys=true,
      preserve_number_repr=true,
    ) catch {
      _ => {
        findings.push({
          line: index + 1,
          code: "invalid_json",
          message: "malformed provider JSON or duplicate keys",
        })
        continue
      }
    }
    let call = decode_ollama(value) catch {
      DecodeError(message) => {
        findings.push({
          line: index + 1,
          code: "invalid_provider_record",
          message,
        })
        continue
      }
    }
    records.push({ line: index + 1, call, })
  }
  { records, findings, }
}

///|
/// Encode interoperable integer counters as JSON numbers, not Int64 strings.
/// Call implements ToJson using this same canonical encoder.
pub fn encode_call(call : Call) -> Json {
  let obj : Map[String, Json] = Map([
    ("run_id", Json::string(call.run_id)),
    ("task_id", Json::string(call.task_id)),
    ("stage", Json::string(call.stage)),
    ("call_id", Json::string(call.call_id)),
    ("model", Json::string(call.model)),
    ("status", Json::string(call.status)),
  ])
  match call.input_tokens {
    Some(n) =>
      obj.set("input_tokens", Json::number(n.to_double(), repr=n.to_string()))
    None => obj.set("input_tokens", Json::null())
  }
  match call.output_tokens {
    Some(n) =>
      obj.set("output_tokens", Json::number(n.to_double(), repr=n.to_string()))
    None => obj.set("output_tokens", Json::null())
  }
  Json::object(obj)
}