///|
enum DiffFormat {
  Text
  Jsonl
} derive(Eq, Debug)

///|
fn DiffFormat::parse(value : String) -> DiffFormat raise CliError {
  match value {
    "text" => Text
    "jsonl" => Jsonl
    _ =>
      raise CliError(
        "expected text or jsonl after --format, found '\{value}'",
        2,
      )
  }
}

///|
/// Host operations write all bytes or raise, including backpressure. Only stdout
/// needs a flush after every record. File sinks finish with one flush and close.
struct OutputSink {
  write : (Bytes) -> Unit raise
  flush : () -> Unit raise
  close : () -> Unit raise
  flush_records : Bool
}

///|
fn diff_event_json(event : @smithy.ValidationEvent) -> Json {
  let shape : Json = match event.shape {
    Some(id) => Json(id.to_string())
    None => null
  }
  let source : Json = match event.source {
    None => null
    Some(source) => {
      let span : Json = match source.span {
        None => null
        Some(span) => { "start": span.start.0, "length": span.length.0 }
      }
      { "file": source.file, "path": source.path, "span": span }
    }
  }
  {
    "id": event.id,
    "severity": event.severity,
    "shape": shape,
    "message": event.message,
    "source": source,
  }
}

///|
/// Serialize only the current event. The comparison result already owns the
/// events, but this writer never creates another aggregate of serialized output.
fn write_diff_jsonl(
  events : Array[@smithy.ValidationEvent],
  sink : OutputSink,
) -> Unit raise {
  // Preserve the original write/flush failure, but always release the file.
  {
    errdefer ((sink.close)() catch { _ => () })
    for event in events {
      if event.severity == "SUPPRESSED" {
        continue
      }
      (sink.write)(@utf8.encode(diff_event_json(event).stringify() + "\n"))
      if sink.flush_records {
        (sink.flush)()
      }
    }
    if !sink.flush_records || events.is_empty() {
      (sink.flush)()
    }
  }
  (sink.close)()
}

///|
/// Diagnostics must remain separate from JSONL even on Wasm GC, whose default
/// print host routes eprintln through stdout. Reporting must not mask a failure.
fn cli_stderr(message : String) -> Unit {
  try {
    let sink = stderr_sink()
    (sink.write)(@utf8.encode(message + "\n"))
    (sink.flush)()
  } catch {
    _ => ()
  }
}