///|
fn fixture_action(value : Json, path : String) -> Action raise SieveError {
  let obj = json_object(value, path)
  json_keys(obj, ["kind", "target"], path)
  let kind = json_string(json_required(obj, "kind", path), path + ".kind")
  match kind {
    "keep" | "discard" => {
      if obj.contains("target") {
        fail("json.action", "keep/discard cannot have a target", origin())
      }
      if kind == "keep" {
        Keep
      } else {
        Discard
      }
    }
    "fileinto" | "redirect" => {
      let target = json_string(
        json_required(obj, "target", path),
        path + ".target",
      )
      safe_destination(target, kind, origin())
      if kind == "fileinto" {
        FileInto(target)
      } else {
        Redirect(target)
      }
    }
    _ => {
      fail("json.action", "unsupported expected action", origin())
      Keep
    }
  }
}

///|
pub fn parse_replay_json(
  text : String,
  limits? : Limits = Limits::default(),
) -> Array[ReplayCase] raise SieveError {
  let array = json_array(parse_json_bounded(text, limits), "$")
  if array.length() > 1000 {
    fail("replay.count", "fixture array exceeds 1000 cases", origin())
  }
  let cases : Array[ReplayCase] = []
  for i = 0; i < array.length(); i = i + 1 {
    let path = "$[\{i}]"
    let obj = json_object(array[i], path)
    json_keys(obj, ["id", "message", "expected", "expected_error"], path)
    let id = json_string(json_required(obj, "id", path), path + ".id")
    let message = Message::from_json(
      json_required(obj, "message", path),
      limits~,
    )
    if obj.contains("expected") && obj.contains("expected_error") {
      fail(
        "json.expectation",
        "expected and expected_error are mutually exclusive",
        origin(),
      )
    }
    let expectation = match (obj.get("expected"), obj.get("expected_error")) {
      (Some(value), None) => {
        let values = json_array(value, path + ".expected")
        if values.length() > limits.actions {
          fail("limit.actions", "too many expected actions", origin())
        }
        let actions : Array[Action] = []
        for value in values {
          actions.push(fixture_action(value, path + ".expected[]"))
        }
        Deliver(actions)
      }
      (None, Some(value)) =>
        FailWith(json_string(value, path + ".expected_error"))
      _ => Observe
    }
    cases.push({ id, message, expectation })
  }
  cases
}

///|
pub fn PolicyChange::as_json(
  self : PolicyChange,
  redact_destinations? : Bool = false,
) -> Json {
  Json::object({
    "id": Json::string(self.id),
    "changed": Json::boolean(self.changed),
    "removed": Json::array(
      self.removed.map(action => action_json(action, redact_destinations)),
    ),
    "added": Json::array(
      self.added.map(action => action_json(action, redact_destinations)),
    ),
    "delivery_lost": Json::boolean(self.delivery_lost),
    "new_redirect": Json::boolean(self.new_redirect),
    "before_error": match self.before_error {
      Some(s) => Json::string(s)
      None => Json::null()
    },
    "after_error": match self.after_error {
      Some(s) => Json::string(s)
      None => Json::null()
    },
  })
}

///|
fn dispatch_request(text : String) -> (Int, String) raise SieveError {
  let obj = json_object(
    parse_json_bounded(text, { ..Limits::default(), source_chars: 4194304 }),
    "$",
  )
  json_keys(
    obj,
    ["command", "source", "message", "fixtures", "before", "io_error"],
    "$",
  )
  if obj.contains("io_error") {
    fail(
      "cli.io",
      json_string(json_required(obj, "io_error", "$"), "$.io_error"),
      origin(),
    )
  }
  let command = json_string(json_required(obj, "command", "$"), "$.command")
  if command == "help" {
    return (
      0, "MoonSieve 离线规则工具\n用法:moon run cmd/main --target js -- <命令> <文件...>\n  lint SCRIPT\n  format SCRIPT\n  run SCRIPT MESSAGE.json\n  replay SCRIPT CASES.json\n  diff BEFORE.sieve AFTER.sieve CASES.json\n输出不执行任何实际邮件操作。退出码:0 成功,1 回放不符/规则变化,2 输入或执行错误。",
    )
  }
  let source = json_string(json_required(obj, "source", "$"), "$.source")
  match command {
    "format" => (0, format_script(source))
    "lint" => (0, compile(source).audit().as_json().stringify(indent=2))
    "run" => {
      let message = json_string(json_required(obj, "message", "$"), "$.message")
      (0, run_json(source, message).stringify(indent=2))
    }
    "replay" => {
      let fixtures = json_string(
        json_required(obj, "fixtures", "$"),
        "$.fixtures",
      )
      let report = compile(source).replay(parse_replay_json(fixtures))
      let unexpected_errors = report.results.any(item => {
        item.error is Some(_) && item.expectation_met != Some(true)
      })
      let code = if unexpected_errors {
        2
      } else if report.failed_expectations > 0 {
        1
      } else {
        0
      }
      (code, report.as_json().stringify(indent=2))
    }
    "diff" => {
      let old_source = json_string(
        json_required(obj, "before", "$"),
        "$.before",
      )
      let fixtures = json_string(
        json_required(obj, "fixtures", "$"),
        "$.fixtures",
      )
      let cases = parse_replay_json(fixtures)
      let before = compile(old_source).replay(cases)
      let after = compile(source).replay(cases)
      let changes = compare_replays(before, after)
      let code = if before.execution_errors > 0 || after.execution_errors > 0 {
        2
      } else if changes.any(change => change.changed) {
        1
      } else {
        0
      }
      (
        code,
        Json::array(changes.map(change => change.as_json())).stringify(indent=2),
      )
    }
    _ => {
      fail("cli.command", "unknown command; use help", origin())
      (2, "")
    }
  }
}

///|
pub fn handle_request(text : String) -> (Int, String) {
  dispatch_request(text) catch {
    SieveError(d) =>
      (2, Json::object({ "error": d.as_json() }).stringify(indent=2))
  }
}