///|
#derive(Eq, Debug)
pub(all) suberror ExecutionError {
  Invalid(String)
}

///|
pub extend ExecutionError with Eq::{equal, not_equal}

///|
pub extend ExecutionError with @debug.Debug::{to_repr}

///|
#derive(Eq, Debug)
pub(all) enum Value {
  Integer(@bigint.BigInt)
  Boolean(Bool)
  Unit
  Decimal(@bigint.BigInt, Int32)
  Float64(UInt64)
  Text(Array[Int32])
  Character(Array[Int32])
  Sequence(Array[Value])
  Product(Array[Value])
  Record(Array[(String, Value)])
  Constructor(String, String, Array[Value])
  Optional(Value?)
  Outcome(Result[Value, Value])
  ModelError(String)
}

///|
pub extend Value with Eq::{equal, not_equal}

///|
pub extend Value with @debug.Debug::{to_repr}

///|
#derive(Eq, Debug)
pub(all) enum ValueType {
  Int
  Bool
  Unit
  Decimal
  Float64
  Text
  Character
  Sequence(ValueType)
  Product(Array[ValueType])
  Record(Array[(String, ValueType)])
  Custom(String)
  Optional(ValueType)
  Outcome(ValueType, ValueType)
  Function
}

///|
pub extend ValueType with Eq::{equal, not_equal}

///|
pub extend ValueType with @debug.Debug::{to_repr}

///|
#derive(Eq, Debug)
pub(all) struct Call {
  id : String
  entry : String
  arguments : Array[Value]
}

///|
pub extend Call with Eq::{equal, not_equal}

///|
pub extend Call with @debug.Debug::{to_repr}

///|
#derive(Eq, Debug)
pub(all) struct Suite {
  calls : Array[Call]
  extensions : Bytes?
}

///|
pub extend Suite with Eq::{equal, not_equal}

///|
pub extend Suite with @debug.Debug::{to_repr}

///|
pub fn Suite::validate(self : Suite) -> Unit raise {
  if self.calls.is_empty() || self.calls.length() > 1000 {
    raise Invalid("execution.call_limit")
  }
  if self.extensions is Some(bytes) {
    if bytes.length() > 65536 {
      raise Invalid("execution.extension_limit")
    }
    ignore(@binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128))))
  }
  let seen = Map([])
  for call in self.calls {
    if call.id == "" ||
      call.id.length() > 128 ||
      call.entry == "" ||
      call.entry.length() > 1024 ||
      seen.contains(call.id) {
      raise Invalid("execution.invalid_call_identity")
    }
    seen[call.id] = true
    if call.arguments.length() > 64 {
      raise Invalid("execution.argument_limit")
    }
    let budget = Ref(100000)
    for argument in call.arguments {
      validate_value(argument, 0, budget)
    }
  }
}

///|
pub fn fields(
  value : @ion.IonValue,
  allowed : Array[String],
) -> Map[String, @ion.IonValue] raise {
  let result = Map([])
  guard value.kind() is Struct(items) else {
    raise Invalid("execution.expected_struct")
  }
  for item in items {
    let name = item.name().text().unwrap_or("")
    if !allowed.contains(name) || result.contains(name) {
      raise Invalid("execution.invalid_field: " + name)
    }
    result[name] = item.value()
  }
  result
}

///|
pub fn required(
  fields : Map[String, @ion.IonValue],
  name : String,
) -> @ion.IonValue raise {
  match fields.get(name) {
    Some(v) => v
    None => raise Invalid("execution.missing_field: " + name)
  }
}

///|
pub fn string(value : @ion.IonValue) -> String raise {
  if value.has_annotations() {
    raise Invalid("execution.unexpected_annotation")
  }
  match value.kind() {
    String(s) => s
    _ => raise Invalid("execution.expected_string")
  }
}

///|
fn list(value : @ion.IonValue) -> Array[@ion.IonValue] raise {
  if value.has_annotations() {
    raise Invalid("execution.unexpected_annotation")
  }
  match value.kind() {
    List(v) => v
    _ => raise Invalid("execution.expected_list")
  }
}

///|
pub fn record(fields : Array[(String, @ion.IonValue)]) -> @ion.IonValue {
  @ion.IonValue::from_fields(fields.map(p => @ion.field(p.0, p.1)))
}

///|
pub fn Suite::to_ion(self : Suite) -> @ion.IonValue raise {
  self.validate()
  let pairs = [
    ("profile", @ion.IonValue::string("morphir-invocations-v1")),
    (
      "calls",
      @ion.IonValue::list(
        self.calls.map(call => {
          record([
            ("id", @ion.IonValue::string(call.id)),
            ("entry", @ion.IonValue::string(call.entry)),
            (
              "arguments",
              @ion.IonValue::list(call.arguments.map(Value::to_ion)),
            ),
          ])
        }),
      ),
    ),
  ]
  if self.extensions is Some(_) {
    pairs.push(("extensions", self.extension_value()))
  }
  record(pairs)
}

///|
pub fn Suite::from_ion(value : @ion.IonValue) -> Suite raise {
  if value.has_annotations() {
    raise Invalid("execution.unexpected_annotation")
  }
  let f = fields(value, ["profile", "calls", "extensions"])
  if string(required(f, "profile")) != "morphir-invocations-v1" {
    raise Invalid("execution.unsupported_profile")
  }
  let suite = Suite::{
    extensions: f.get("extensions").map(v => @binary.write_binary_value(v)),
    calls: list(required(f, "calls")).map(v => {
      if v.has_annotations() {
        raise Invalid("execution.unexpected_annotation")
      }
      let f = fields(v, ["id", "entry", "arguments"])
      Call::{
        id: string(required(f, "id")),
        entry: string(required(f, "entry")),
        arguments: list(required(f, "arguments")).map(Value::from_ion),
      }
    }),
  }
  suite.validate()
  suite
}

///|
pub fn Suite::to_json(self : Suite) -> Json {
  let result : Json = {
    "profile": "morphir-invocations-v1",
    "calls": self.calls.map(call => {
      (
        {
          "id": call.id,
          "entry": call.entry,
          "arguments": call.arguments.map(Value::to_json),
        } : Json)
    }),
  }
  if self.extensions is Some(bytes) && result is Object(fields) {
    fields["extensions"] = {
      "format": "ion-binary",
      "bytes": bytes.to_array().map(b => b.to_int()),
    }
  }
  result
}

///|
pub fn Suite::from_json(value : Json) -> Suite raise {
  json_ion_suite(
    @text.read_ion(value.stringify()[:], fuel=Limited(@ion.Budget(128))),
  )
}

///|
pub fn Suite::decode(bytes : Bytes, format : String) -> Suite raise {
  if bytes.length() > 1048576 {
    raise Invalid("execution.input_limit")
  }
  match format {
    "ion-binary" =>
      Suite::from_ion(
        @binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128))),
      )
    "ion-text" =>
      Suite::from_ion(
        @text.read_ion(
          @utf8.decode(bytes[:])[:],
          fuel=Limited(@ion.Budget(128)),
        ),
      )
    "json" => {
      let source = @utf8.decode(bytes[:])
      ignore(@json.parse(source))
      json_ion_suite(@text.read_ion(source[:], fuel=Limited(@ion.Budget(128))))
    }
    _ => raise Invalid("execution.unsupported_format")
  }
}

///|
pub fn Suite::encode(self : Suite, format : String) -> Bytes raise {
  let value = self.to_ion()
  match format {
    "ion-binary" => @binary.write_binary_value(value)
    "ion-text" => @utf8.encode(@text.write_ion(value))
    "json" => @utf8.encode(self.to_json().stringify())
    _ => raise Invalid("execution.unsupported_format")
  }
}

///|
// Reading the schema as Ion after JSON syntax validation preserves duplicate keys.
fn json_ion_suite(value : @ion.IonValue) -> Suite raise {
  let f = fields(value, ["profile", "calls", "extensions"])
  if string(required(f, "profile")) != "morphir-invocations-v1" {
    raise Invalid("execution.unsupported_profile")
  }
  let extensions = match f.get("extensions") {
    None => None
    Some(v) => Some(json_extension(v))
  }
  let suite = Suite::{
    extensions,
    calls: list(required(f, "calls")).map(call => {
      let f = fields(call, ["id", "entry", "arguments"])
      Call::{
        id: string(required(f, "id")),
        entry: string(required(f, "entry")),
        arguments: list(required(f, "arguments")).map(v => payload_value(v, 0)),
      }
    }),
  }
  suite.validate()
  suite
}

///|
fn json_extension(value : @ion.IonValue) -> Bytes raise {
  if value.has_annotations() {
    raise Invalid("execution.unexpected_annotation")
  }
  let f = fields(value, ["format", "bytes"])
  if string(required(f, "format")) != "ion-binary" {
    raise Invalid("execution.extension_format")
  }
  let bytes = Bytes::from_array(
    list(required(f, "bytes")).map(v => {
      let n = small_integer(v)
      if n < 0 || n > 255 {
        raise Invalid("execution.extension_byte")
      }
      n.to_byte()
    }),
  )
  ignore(@binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128))))
  bytes
}

///|
/// Extensions preserve Ion types and annotations outside semantic arguments.
pub fn Suite::extension_value(self : Suite) -> @ion.IonValue raise {
  match self.extensions {
    Some(bytes) =>
      @binary.read_binary_value(bytes[:], fuel=Limited(@ion.Budget(128)))
    None => @ion.IonValue::untyped_null()
  }
}