///|
fn json_object(
  value : Json,
  path : String,
) -> Map[String, Json] raise SieveError {
  match value {
    Object(obj) => obj
    _ => {
      fail("json.type", "\{path}: expected object", origin())
      Map([])
    }
  }
}

///|
fn json_keys(
  obj : Map[String, Json],
  allowed : Array[String],
  path : String,
) -> Unit raise SieveError {
  for key, _ in obj {
    if !allowed.contains(key) {
      fail("json.field", "\{path}: unknown field \{key}", origin())
    }
  }
}

///|
fn json_required(
  obj : Map[String, Json],
  key : String,
  path : String,
) -> Json raise SieveError {
  match obj.get(key) {
    Some(value) => value
    None => {
      fail("json.missing", "\{path}.\{key}: missing required field", origin())
      Json::null()
    }
  }
}

///|
fn json_string(value : Json, path : String) -> String raise SieveError {
  match value {
    String(text) => text
    _ => {
      fail("json.type", "\{path}: expected string", origin())
      ""
    }
  }
}

///|
fn json_integer(value : Json, path : String) -> Int raise SieveError {
  match value {
    Number(n, ..) if n >= 0.0 && n <= 2147483647.0 => {
      let integer = n.to_int()
      if integer.to_double() != n {
        fail("json.integer", "\{path}: expected nonnegative integer", origin())
      }
      integer
    }
    _ => {
      fail(
        "json.integer",
        "\{path}: expected nonnegative 31-bit integer",
        origin(),
      )
      0
    }
  }
}

///|
fn json_array(value : Json, path : String) -> Array[Json] raise SieveError {
  match value {
    Array(items) => items
    _ => {
      fail("json.type", "\{path}: expected array", origin())
      []
    }
  }
}

///|
fn parse_json_bounded(text : String, limits : Limits) -> Json raise SieveError {
  limits.check()
  if text.length() > limits.source_chars {
    fail("limit.json", "JSON input exceeds configured limit", origin())
  }
  check_json_depth(text)
  @json.parse(text) catch {
    _ => {
      fail("json.syntax", "invalid JSON or nesting limit exceeded", origin())
      Json::null()
    }
  }
}

///|
pub fn Message::from_json(
  value : Json,
  limits? : Limits = Limits::default(),
) -> Message raise SieveError {
  limits.check()
  let root = json_object(value, "$")
  json_keys(root, ["headers", "size", "envelope"], "$")
  let raw = json_array(json_required(root, "headers", "$"), "$.headers")
  if raw.length() > limits.headers {
    fail("limit.headers", "too many headers", origin())
  }
  let headers : Array[Header] = []
  for i = 0; i < raw.length(); i = i + 1 {
    let path = "$.headers[\{i}]"
    let obj = json_object(raw[i], path)
    json_keys(obj, ["name", "value"], path)
    headers.push({
      name: json_string(json_required(obj, "name", path), path + ".name"),
      value: json_string(json_required(obj, "value", path), path + ".value"),
    })
  }
  let size = json_integer(json_required(root, "size", "$"), "$.size")
  let mut sender = ""
  let recipients : Array[String] = []
  match root.get("envelope") {
    None => ()
    Some(value) => {
      let envelope = json_object(value, "$.envelope")
      json_keys(envelope, ["from", "to"], "$.envelope")
      match envelope.get("from") {
        None => ()
        Some(value) => sender = json_string(value, "$.envelope.from")
      }
      match envelope.get("to") {
        None => ()
        Some(value) => {
          let list = json_array(value, "$.envelope.to")
          if list.length() > limits.headers {
            fail("limit.recipients", "too many recipients", origin())
          }
          for i = 0; i < list.length(); i = i + 1 {
            recipients.push(json_string(list[i], "$.envelope.to[\{i}]"))
          }
        }
      }
    }
  }
  Message::new(
    headers,
    size,
    envelope_from=sender,
    envelope_to=recipients,
    limits~,
  )
}

///|
pub fn parse_message_json(
  text : String,
  limits? : Limits = Limits::default(),
) -> Message raise SieveError {
  Message::from_json(parse_json_bounded(text, limits), limits~)
}

///|
pub fn Message::as_json(self : Message) -> Json {
  Json::object({
    "headers": Json::array(
      self.headers.map(header => {
        Json::object({
          "name": Json::string(header.name),
          "value": Json::string(header.value),
        })
      }),
    ),
    "size": Json::number(self.size.to_double()),
    "envelope": Json::object({
      "from": Json::string(self.envelope_from),
      "to": Json::array(self.envelope_to.map(Json::string)),
    }),
  })
}

///|
fn span_json(span : Span) -> Json {
  Json::object({
    "start": Json::number(span.start.to_double()),
    "end": Json::number(span.end.to_double()),
    "line": Json::number(span.line.to_double()),
    "column": Json::number(span.column.to_double()),
  })
}

///|
pub fn Diagnostic::as_json(self : Diagnostic) -> Json {
  Json::object({
    "code": Json::string(self.code),
    "message": Json::string(self.message),
    "span": span_json(self.span),
    "hint": Json::string(diagnostic_hint(self.code)),
  })
}

///|
fn action_json(action : Action, redact : Bool) -> Json {
  match action {
    Keep => Json::object({ "kind": Json::string("keep") })
    Discard => Json::object({ "kind": Json::string("discard") })
    FileInto(target) =>
      Json::object({
        "kind": Json::string("fileinto"),
        "target": Json::string(if redact { "" } else { target }),
      })
    Redirect(target) =>
      Json::object({
        "kind": Json::string("redirect"),
        "target": Json::string(if redact { "" } else { target }),
      })
  }
}

///|
pub fn Plan::as_json(
  self : Plan,
  include_trace? : Bool = true,
  redact_destinations? : Bool = false,
) -> Json {
  let obj : Map[String, Json] = {
    "schema_version": Json::number(1.0),
    "actions": Json::array(
      self.actions.map(action => action_json(action, redact_destinations)),
    ),
    "implicit_keep": Json::boolean(self.implicit_keep),
    "steps": Json::number(self.steps.to_double()),
    "comparisons": Json::number(self.comparisons.to_double()),
  }
  if include_trace {
    obj["trace"] = Json::array(
      self.trace.map(event => {
        Json::object({
          "kind": Json::string(event.kind),
          "label": Json::string(event.label),
          "span": span_json(event.span),
          "matched": match event.matched {
            Some(value) => Json::boolean(value)
            None => Json::null()
          },
        })
      }),
    )
  }
  Json::object(obj)
}

///|
pub fn ScriptAudit::as_json(self : ScriptAudit) -> Json {
  Json::object({
    "warnings": Json::array(self.warnings.map(Diagnostic::as_json)),
    "commands": Json::number(self.commands.to_double()),
    "predicates": Json::number(self.predicates.to_double()),
    "max_depth": Json::number(self.max_depth.to_double()),
    "literal_redirects": Json::array(self.literal_redirects.map(Json::string)),
    "literal_mailboxes": Json::array(self.literal_mailboxes.map(Json::string)),
  })
}

///|
pub fn run_json(
  source : String,
  message_json : String,
  limits? : Limits = Limits::default(),
  options? : ExecutionOptions = ExecutionOptions::default(),
) -> Json raise SieveError {
  compile(source, limits~)
  .run(parse_message_json(message_json, limits~), options~)
  .as_json()
}

///|
fn check_json_depth(text : String) -> Unit raise SieveError {
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for c in text {
    if quoted {
      if escaped {
        escaped = false
      } else if c == '\\' {
        escaped = true
      } else if c == '"' {
        quoted = false
      }
    } else if c == '"' {
      quoted = true
    } else if c == '[' || c == '{' {
      depth += 1
      if depth > 32 {
        fail("json.depth", "JSON nesting exceeds 32", origin())
      }
    } else if c == ']' || c == '}' {
      depth -= 1
    }
  }
}