///|
/// Errors returned while decoding an operation JSON document.
pub(all) suberror OperationJsonError {
  InvalidJson
  ExpectedObject
  ExpectedArray
  MissingField(String)
  InvalidField(String)
  UnknownField(String)
} derive(Debug, Eq)

///|
/// Serializes an operation using the same object shape as audit events.
pub fn Operation::to_json(self : Operation) -> String {
  let output = StringBuilder()
  append_operation_json(output, self)
  output.to_string()
}

///|
/// Decodes an operation object emitted by `Operation::to_json`.
///
/// The `tool` field selects one of the built-in operations. Unknown tools with
/// `estimated_output_bytes` decode as `Invoke`; objects with `action` and
/// `resource` decode as `ResourceAccess`.
pub fn Operation::from_json(
  source : String,
) -> Operation raise OperationJsonError {
  let value = @json.parse(source) catch {
    _ => raise OperationJsonError::InvalidJson
  }
  operation_from_json_value(value)
}

///|
/// Decodes a JSON array of operations in input order.
pub fn Operation::from_json_array(
  source : String,
) -> Array[Operation] raise OperationJsonError {
  let value = @json.parse(source) catch {
    _ => raise OperationJsonError::InvalidJson
  }
  match value {
    Json::Array(values) => {
      let operations = []
      for value in values {
        operations.push(operation_from_json_value(value))
      }
      operations
    }
    _ => raise OperationJsonError::ExpectedArray
  }
}

///|
fn operation_from_json_value(
  value : Json,
) -> Operation raise OperationJsonError {
  let fields = operation_json_object(value)
  let tool = operation_required_string(fields, "tool")
  match tool {
    "fs.read" => {
      operation_ensure_known(fields, ["tool", "path", "estimated_bytes"])
      Operation::ReadFile(
        path=operation_required_string(fields, "path"),
        estimated_bytes=operation_required_int(fields, "estimated_bytes"),
      )
    }
    "fs.write" => {
      operation_ensure_known(fields, ["tool", "path", "bytes"])
      Operation::WriteFile(
        path=operation_required_string(fields, "path"),
        bytes=operation_required_int(fields, "bytes"),
      )
    }
    "net.connect" => {
      operation_ensure_known(fields, ["tool", "host", "port"])
      Operation::Connect(
        host=operation_required_string(fields, "host"),
        port=operation_required_int(fields, "port"),
      )
    }
    "process.run" => {
      operation_ensure_known(fields, [
        "tool", "program", "arguments", "estimated_output_bytes",
      ])
      Operation::RunCommand(
        program=operation_required_string(fields, "program"),
        arguments=operation_required_string_array(fields, "arguments"),
        estimated_output_bytes=operation_required_int(
          fields, "estimated_output_bytes",
        ),
      )
    }
    _ =>
      if operation_has_field(fields, "action") ||
        operation_has_field(fields, "resource") {
        operation_ensure_known(fields, [
          "tool", "action", "resource", "estimated_bytes",
        ])
        Operation::ResourceAccess(
          tool~,
          action=operation_required_string(fields, "action"),
          resource=operation_required_string(fields, "resource"),
          estimated_bytes=operation_required_int(fields, "estimated_bytes"),
        )
      } else {
        operation_ensure_known(fields, ["tool", "estimated_output_bytes"])
        Operation::Invoke(
          tool~,
          estimated_output_bytes=operation_required_int(
            fields, "estimated_output_bytes",
          ),
        )
      }
  }
}

///|
fn operation_json_object(
  value : Json,
) -> Map[String, Json] raise OperationJsonError {
  match value {
    Json::Object(fields) => fields
    _ => raise OperationJsonError::ExpectedObject
  }
}

///|
fn operation_required_json(
  fields : Map[String, Json],
  name : String,
) -> Json raise OperationJsonError {
  match fields.get(name) {
    Some(value) => value
    None => raise OperationJsonError::MissingField(name)
  }
}

///|
fn operation_required_string(
  fields : Map[String, Json],
  name : String,
) -> String raise OperationJsonError {
  match operation_required_json(fields, name) {
    Json::String(value) => value
    _ => raise OperationJsonError::InvalidField(name)
  }
}

///|
fn operation_required_string_array(
  fields : Map[String, Json],
  name : String,
) -> Array[String] raise OperationJsonError {
  match operation_required_json(fields, name) {
    Json::Array(values) => {
      let result = []
      for value in values {
        match value {
          Json::String(text) => result.push(text)
          _ => raise OperationJsonError::InvalidField(name)
        }
      }
      result
    }
    _ => raise OperationJsonError::InvalidField(name)
  }
}

///|
fn operation_required_int(
  fields : Map[String, Json],
  name : String,
) -> Int raise OperationJsonError {
  match operation_required_json(fields, name) {
    Json::Number(number, ..) => {
      let integer = number.to_int()
      if integer.to_double() == number {
        integer
      } else {
        raise OperationJsonError::InvalidField(name)
      }
    }
    _ => raise OperationJsonError::InvalidField(name)
  }
}

///|
fn operation_has_field(fields : Map[String, Json], name : String) -> Bool {
  fields.get(name) is Some(_)
}

///|
fn operation_ensure_known(
  fields : Map[String, Json],
  known : Array[String],
) -> Unit raise OperationJsonError {
  for name, _ in fields {
    if !known.contains(name) {
      raise OperationJsonError::UnknownField(name)
    }
  }
}