///|
/// Framing diagnostics are stages, not native invocation error effects.
pub(all) suberror Invalid {
  Invalid(String)
}

///|
/// A syntactically admitted frame carries no caller, session or grant authority.
/// The source documents remain unchanged for later admission and capture.
pub struct Request {
  priv value : Json
}

///|
pub fn Request::operation(self : Request) -> String {
  match self.value {
    Object(fields) =>
      match fields["operation"] {
        String(op) => op
        _ => abort("Admitted request operation")
      }
    _ => abort("Admitted request object")
  }
}

///|
pub fn Request::to_json(self : Request) -> Json {
  // Json maps are mutable. Return a fresh copy so no caller can change an
  // admitted frame behind another owner's reference.
  @wire.parse_bounded(self.value.stringify()) catch {
    _ => abort("Admitted request copy")
  }
}

///|
fn closed(
  value : Json,
  keys : Array[String],
) -> Map[String, Json] raise Invalid {
  guard value is Object(fields) &&
    fields.length() == keys.length() &&
    keys.all(key => fields.contains(key)) else {
    raise Invalid("native.frame.shape")
  }
  fields
}

///|
fn text(value : Json, minimum : Int32, maximum : Int32) -> String raise Invalid {
  guard value is String(source) &&
    source.iter().count() >= minimum &&
    source.iter().count() <= maximum else {
    raise Invalid("native.frame.shape")
  }
  source
}

///|
fn token(value : Json) -> Unit raise Invalid {
  let value = text(value, 1, 128)
  guard value
    .iter()
    .all(c => {
      (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9') ||
      c == '_' ||
      c == '.' ||
      c == '-'
    }) else {
    raise Invalid("native.frame.shape")
  }
}

///|
fn integer(
  value : Json,
  minimum : Double,
  maximum : Double,
) -> Unit raise Invalid {
  guard value is Number(n, ..) &&
    n >= minimum &&
    n <= maximum &&
    n.floor() == n &&
    @wire.is_integral_number(value) else {
    raise Invalid("native.frame.shape")
  }
}

///|
fn version(value : Json) -> Unit raise Invalid {
  guard value == Json::string("0.2.0-draft.1") else {
    raise Invalid("native.frame.shape")
  }
}

///|
fn shape(value : Json) -> Unit raise Invalid {
  guard value is Object(fields) && fields.get("operation") is Some(String(op)) else {
    raise Invalid("native.frame.shape")
  }
  let payload = match op {
    "session.open" => {
      ignore(closed(value, ["profile", "contractVersion", "operation"]))
      None
    }
    "session.check"
    | "session.close"
    | "workspace.choose"
    | "workspace.cancel-picker" => {
      ignore(
        closed(value, ["profile", "contractVersion", "operation", "sessionId"]),
      )
      token(fields["sessionId"])
      None
    }
    "execution.submit"
    | "execution.query"
    | "execution.cancel"
    | "execution.release"
    | "workspace.revoke"
    | "workspace.open"
    | "workspace.read"
    | "workspace.release" => {
      ignore(
        closed(value, [
          "profile", "contractVersion", "operation", "sessionId", "payload",
        ]),
      )
      token(fields["sessionId"])
      Some(fields["payload"])
    }
    _ => raise Invalid("native.frame.shape")
  }
  guard fields["profile"] == Json::string("morphir/workbench/native") else {
    raise Invalid("native.frame.shape")
  }
  version(fields["contractVersion"])
  if payload is Some(payload) {
    match op {
      "execution.submit" => {
        let payload = closed(payload, [
          "contractVersion", "model", "entry", "inputs",
        ])
        version(payload["contractVersion"])
        ignore(text(payload["model"], 1, 1048576))
        ignore(text(payload["entry"], 1, 1024))
        let inputs = closed(payload["inputs"], ["format", "source"])
        guard inputs["format"] == Json::string("json") ||
          inputs["format"] == Json::string("ion-text") else {
          raise Invalid("native.frame.shape")
        }
        ignore(text(inputs["source"], 0, 1048576))
      }
      "execution.query" | "execution.cancel" | "execution.release" => {
        let payload = closed(payload, ["contractVersion", "identity"])
        version(payload["contractVersion"])
        let keys = [
          "hostId", "sessionId", "providerId", "executionId", "snapshotId",
        ]
        let identity = closed(payload["identity"], keys)
        for key in keys {
          let item = text(identity[key], 1, 128)
          guard item.length() <= 128 else {
            raise Invalid("native.frame.shape")
          }
        }
      }
      "workspace.revoke" => {
        let payload = closed(payload, ["grantId"])
        token(payload["grantId"])
      }
      "workspace.open" => {
        let payload = closed(payload, ["grantId", "projectId", "sequence"])
        token(payload["grantId"])
        token(payload["projectId"])
        integer(payload["sequence"], 1, 2147483647)
      }
      "workspace.read" => {
        let payload = closed(payload, ["grantId", "sequence", "offset"])
        token(payload["grantId"])
        integer(payload["sequence"], 1, 2147483647)
        integer(payload["offset"], 0, 16777216)
      }
      "workspace.release" => {
        let payload = closed(payload, ["grantId", "sequence"])
        token(payload["grantId"])
        integer(payload["sequence"], 1, 2147483647)
      }
      _ => abort("Payload operation admitted")
    }
  }
}

///|
fn nested(value : Json) -> Unit raise Invalid {
  guard value is Object(fields) &&
    fields["operation"] == Json::string("execution.submit") else {
    return
  }
  let payload = closed(fields["payload"], [
    "contractVersion", "model", "entry", "inputs",
  ])
  let model = text(payload["model"], 1, 1048576)
  ignore(@wire.parse_bounded(model)) catch {
    _ => raise Invalid("native.frame.model")
  }
  let inputs = closed(payload["inputs"], ["format", "source"])
  if inputs["format"] == Json::string("json") {
    let source = text(inputs["source"], 0, 1048576)
    let arguments = @wire.parse_bounded(source) catch {
      _ => raise Invalid("native.frame.arguments")
    }
    guard arguments is Array(_) else { raise Invalid("native.frame.arguments") }
  }
}

///|
/// Production native draft.1 decoder. This boundary only admits framing and
/// closed shape; trusted caller checks, model/type admission and execution follow
/// separately. Never call dispatch with a framework-decoded Json object.
pub fn decode(bytes : Bytes) -> Request raise Invalid {
  guard bytes.length() <= 1048576 else { raise Invalid("native.frame.limit") }
  let source = @utf8.decode(bytes[:], ignore_bom=false) catch {
    _ => raise Invalid("native.frame.utf8")
  }
  let value = @wire.parse_bounded(source) catch {
    @wire.Invalid("json_limit", _) => raise Invalid("native.frame.limit")
    _ => raise Invalid("native.frame.json")
  }
  if value is Object(fields) &&
    fields.get("operation") is Some(String(op)) &&
    [
      "workspace.choose", "workspace.cancel-picker", "workspace.revoke", "workspace.open",
      "workspace.read", "workspace.release",
    ].contains(op) &&
    bytes.length() > 4096 {
    raise Invalid("native.frame.limit")
  }
  shape(value)
  nested(value)
  { value, }
}