///|
pub fn rule_from_string(name : String) -> Rule raise BeliefError {
  match name {
    "conjunctive" => Conjunctive
    "dempster" => Dempster
    "disjunctive" => Disjunctive
    "yager" => Yager
    "dubois-prade" => DuboisPrade
    "pairwise-pcr5" => PairwisePCR5
    _ => raise Invalid("unsupported fusion rule")
  }
}

///|
fn optional_number(
  object : Map[String, Json],
  name : String,
  default : Double,
) -> Double raise BeliefError {
  match object.get(name) {
    None => default
    Some(value) => json_number(value)
  }
}

///|
fn decode_sources(
  f : Frame,
  value : Json,
  budget : Limits,
) -> Array[Source] raise BeliefError {
  let rows = json_array(value, 32)
  let result : Array[Source] = []
  for row in rows {
    let object = json_object(row, ["name", "reliability", "masses"])
    result.push(
      source(
        json_string(required(object, "name")),
        decode_rows(f, required(object, "masses"), budget),
        reliability=optional_number(object, "reliability", 1.0),
      ),
    )
  }
  result
}

///|
fn encode_step(step : FusionStep) -> Json {
  Json::object({
    "source": Json::string(step.source),
    "input_conflict": Json::number(step.input_conflict),
    "previous_conflict": Json::number(step.previous_conflict),
    "conjunctive_conflict": Json::number(step.conjunctive_conflict),
    "output_conflict": Json::number(step.output_conflict),
    "output_ignorance": Json::number(step.output_ignorance),
    "output_focals": Json::number(step.output_focals.to_double()),
  })
}

///|
fn encode_ranking(m : Mass) -> Json raise BeliefError {
  let result : Array[Json] = []
  for score in m.ranking() {
    result.push(
      Json::object({
        "label": Json::string(score.label),
        "belief": Json::number(score.belief),
        "plausibility": Json::number(score.plausibility),
        "pignistic": Json::number(score.pignistic),
      }),
    )
  }
  Json::array(result)
}

///|
fn encode_queries(m : Mass, value : Json) -> Json raise BeliefError {
  let queries = json_array(value, 256)
  let result : Array[Json] = []
  for query in queries {
    let labels = json_strings(query, 16)
    let mask = m.domain.subset(labels)
    let (belief, plausibility) = m.interval(mask)
    result.push(
      Json::object({
        "subset": encode_strings(m.domain.members(mask)),
        "belief": Json::number(belief),
        "plausibility": Json::number(plausibility),
      }),
    )
  }
  Json::array(result)
}

///|
fn decode_utilities(value : Json) -> Array[Array[Double]] raise BeliefError {
  let rows = json_array(value, 64)
  let result : Array[Array[Double]] = []
  for row in rows {
    let cells = json_array(row, 16)
    let values : Array[Double] = []
    for cell in cells {
      values.push(json_number(cell))
    }
    result.push(values)
  }
  result
}

///|
fn encode_decision(m : Mass, value : Json) -> Json raise BeliefError {
  let object = json_object(value, ["actions", "utilities", "policy"])
  let policy_object = match object.get("policy") {
    None => Map([])
    Some(value) =>
      json_object(value, ["max_conflict", "max_nonspecific", "min_margin"])
  }
  let policy = decision_policy(
    max_conflict=optional_number(policy_object, "max_conflict", 0.5),
    max_nonspecific=optional_number(policy_object, "max_nonspecific", 0.8),
    min_margin=optional_number(policy_object, "min_margin", 0.05),
  )
  let report = m.decide(
    json_strings(required(object, "actions"), 64),
    decode_utilities(required(object, "utilities")),
    policy,
  )
  Json::object({
    "action": Json::string(report.label),
    "index": Json::number(report.index.to_double()),
    "accepted": Json::boolean(report.accepted),
    "expected_utility": Json::number(report.expected_score),
    "margin": Json::number(report.margin),
    "reason": Json::string(report.reason),
    "conflict": Json::number(report.conflict),
    "nonspecific": Json::number(report.nonspecific),
  })
}

///|
/// Pure request/report facade: no I/O, credentials, training or network.
/// Total conflict produces a domain error instead of invalid ranking numbers.
pub fn execute_request(
  value : Json,
  budget~ : Limits = default_limits(),
) -> Json raise BeliefError {
  let object = json_object(value, [
    "schema", "frame", "rule", "sources", "queries", "decision",
  ])
  if json_string(required(object, "schema")) != "moonbelief.request.v1" {
    raise Invalid("unsupported request schema")
  }
  let f = frame(json_strings(required(object, "frame"), 16))
  let rule = rule_from_string(json_string(required(object, "rule")))
  let sources = decode_sources(f, required(object, "sources"), budget)
  let result = pipeline(sources, rule, budget~)
  let m = result.mass()
  let output : Map[String, Json] = {
    "schema": Json::string("moonbelief.report.v1"),
    "mass": m.to_json(),
    "conflict": Json::number(m.conflict()),
    "nonspecific": Json::number(m.nonspecific_mass()),
    "ranking": encode_ranking(m),
    "steps": Json::array(result.steps().map(encode_step)),
    "pair_work": Json::number(result.pair_work().to_double()),
  }
  match object.get("queries") {
    Some(queries) => output["queries"] = encode_queries(m, queries)
    None => ()
  }
  match object.get("decision") {
    Some(decision) => output["decision"] = encode_decision(m, decision)
    None => ()
  }
  Json::object(output)
}

///|
pub fn execute_json(
  text : String,
  budget~ : Limits = default_limits(),
) -> String raise BeliefError {
  execute_request(bounded_parse(text), budget~).stringify()
}