///|
/// Parsed model indexes; treat as immutable during evaluation.
pub struct Model {
  root : @xml.Node
  decisions : Map[String, @xml.Node]
  bkms : Map[String, @xml.Node]
  services : Map[String, @xml.Node]
  ids : Map[String, String]
}

///|
fn is_expression_name(name : String) -> Bool {
  [
    "literalExpression", "decisionTable", "context", "invocation", "list", "relation",
    "functionDefinition", "conditional", "filter", "for", "some", "every",
  ].contains(name)
}

///|
fn expression_child(node : @xml.Node) -> @xml.Node raise @types.EngineError {
  for child in node.children {
    if child.uri == node.uri && is_expression_name(child.name) {
      return child
    }
  }
  raise @types.Failure(
    "E_DMN_EXPRESSION",
    node.line,
    node.column,
    "expected a boxed expression inside " + node.name,
  )
}

///|
/// Parse namespace-aware DMN XML and index its named elements.
pub fn parse_model(source : String) -> Model raise @types.EngineError {
  let root = @xml.parse(source)
  if root.name != "definitions" ||
    ![
      "https://www.omg.org/spec/DMN/20191111/MODEL/", "https://www.omg.org/spec/DMN/20211108/MODEL/",
      "https://www.omg.org/spec/DMN/20230324/MODEL/",
    ].contains(root.uri) {
    raise @types.Failure(
      "E_DMN_NAMESPACE",
      root.line,
      root.column,
      "expected DMN 1.3/1.4/1.5 definitions",
    )
  }
  let decisions = Map([])
  let bkms = Map([])
  let services = Map([])
  let ids = Map([])
  for node in root.children {
    if node.uri != root.uri {
      continue
    }
    match node.name {
      "decision" | "inputData" | "businessKnowledgeModel" | "decisionService" => {
        let declared = node.attr("name")
        let id = node.attr("id")
        if declared == "" && id == "" {
          raise @types.fail("E_DMN_ID", "missing name or id")
        }
        let name = if declared == "" { id } else { declared }
        if ids.contains(id) || ids.values().any(x => x == name) {
          raise @types.fail("E_DUPLICATE", "duplicate DMN id or name")
        }
        if id != "" {
          ids[id] = name
        }
        if node.name == "decision" {
          decisions[name] = node
        } else if node.name == "businessKnowledgeModel" {
          bkms[name] = node
        } else if node.name == "decisionService" {
          services[name] = node
        }
      }
      "itemDefinition"
      | "import"
      | "knowledgeSource"
      | "DMNDI"
      | "description"
      | "extensionElements" => ()
      _ =>
        raise @types.Failure(
          "E_DMN_UNSUPPORTED",
          node.line,
          node.column,
          "unknown model element: " + node.name,
        )
    }
  }
  { root, decisions, bkms, services, ids, }
}

///|
/// Evaluate a named decision against typed input values.
pub fn evaluate_decision(
  model : Model,
  name : String,
  inputs : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let inputs = inputs.copy()
  for node in model.root.all("inputData") {
    let key = node.attr("name")
    if inputs.get(key) is Some(value) {
      inputs[key] = coerce_variable(model, node, value)
    }
  }
  let cache = Map([])
  let visiting = []
  evaluate_named(model, name, inputs, cache, visiting)
}

///|
fn evaluate_named(
  model : Model,
  name : String,
  inputs : Map[String, @types.Value],
  cache : Map[String, @types.Value],
  visiting : Array[String],
) -> @types.Value raise @types.EngineError {
  if cache.get(name) is Some(value) {
    return value
  }
  if visiting.contains(name) {
    raise @types.fail("E_DMN_CYCLE", "cyclic dependency: " + name)
  }
  if visiting.length() > 128 {
    raise @types.fail("E_LIMIT", "dependency depth")
  }
  let decision = match model.decisions.get(name) {
    Some(d) => d
    None => raise @types.fail("E_DECISION", "unknown decision: " + name)
  }
  visiting.push(name)
  let ctx = inputs.copy()
  if !ctx.contains("$types") {
    ctx["$types"] = Context(type_registry(model))
  }
  for req in decision.all("informationRequirement") {
    for reference in req.children {
      let href = reference.attr("href")
      let dep = href_id(model, href)
      if reference.name == "requiredDecision" {
        if inputs.get(dep) is Some(value) {
          let node = match model.decisions.get(dep) {
            Some(n) => n
            None => raise @types.fail("E_REFERENCE", "unknown decision: " + dep)
          }
          validate_variable(model, node, value)
          ctx[dep] = value
        } else {
          ctx[dep] = evaluate_named(model, dep, inputs, cache, visiting)
        }
      } else if reference.name == "requiredInput" {
        if !inputs.contains(dep) {
          raise @types.fail("E_INPUT_MISSING", "missing input: " + dep)
        }
      } else {
        raise @types.fail("E_DMN_UNSUPPORTED", "unknown requirement")
      }
    }
  }
  for req in decision.all("knowledgeRequirement") {
    for reference in req.children {
      let href = reference.attr("href")
      let dep = href_id(model, href)
      if model.bkms.get(dep) is Some(bkm) {
        ctx[dep] = bkm_function(model, bkm, ctx, 0)
      } else if model.services.get(dep) is Some(service) {
        ctx[dep] = service_function(model, service, ctx)
      } else {
        raise @types.fail("E_REFERENCE", "unknown knowledge: " + href)
      }
    }
  }
  let value = coerce_variable(
    model,
    decision,
    boxed(model, expression(decision), ctx),
  ) catch {
    @types.Failure("E_TYPE", _, _, _) => Null
    e => raise e
  }
  ignore(visiting.pop())
  cache[name] = value
  value
}

///|
fn expression(node : @xml.Node) -> @xml.Node raise @types.EngineError {
  expression_child(node)
}

///|
fn bkm_function(
  model : Model,
  node : @xml.Node,
  outer : Map[String, @types.Value],
  depth : Int32,
) -> @types.Value raise @types.EngineError {
  if depth > 32 {
    raise @types.fail("E_LIMIT", "recursive BKM definitions")
  }
  let logic = match node.child("encapsulatedLogic") {
    Some(n) => n
    None => raise @types.fail("E_DMN_BKM", "missing encapsulatedLogic")
  }
  let params = logic.all("formalParameter").map(p => p.attr("name"))
  let body = expression_child(logic)
  let captured = outer.copy()
  for req in node.all("knowledgeRequirement") {
    for reference in req.children {
      let href = reference.attr("href")
      let dep = href_id(model, href)
      if model.bkms.get(dep) is Some(bkm) {
        captured[dep] = bkm_function(model, bkm, outer, depth + 1)
      } else if model.services.get(dep) is Some(service) {
        captured[dep] = service_function(model, service, outer)
      } else {
        raise @types.fail("E_REFERENCE", "unknown knowledge: " + href)
      }
    }
  }
  let name = node.attr("name")
  Function({
    name,
    params,
    callback: args => {
      if args.length() != params.length() {
        raise @types.fail(
          "E_ARITY",
          name + " expects " + params.length().to_string() + " arguments",
        )
      }
      let scope = captured.copy()
      for i, param in params {
        scope[param] = coerce_type(
          model,
          logic.all("formalParameter")[i].attr("typeRef"),
          args[i],
          0,
        )
      }
      match
        coerce_variable(
          model,
          node,
          @types.Function({
            name: "result",
            params: [],
            callback: _ => boxed(model, body, scope),
          }),
        ) {
        Function(f) => (f.callback)([])
        _ => boxed(model, body, scope)
      }
    },
  })
}

///|
fn service_params(service : @xml.Node) -> Array[String] {
  let out = []
  for kind in ["inputData", "inputDecision"] {
    for node in service.all(kind) {
      out.push(node.attr("href"))
    }
  }
  out
}

///|
fn service_function(
  model : Model,
  service : @xml.Node,
  outer : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let parameter_hrefs = service_params(service)
  let params = parameter_hrefs.map(href => href_id(model, href))
  let name = service.attr("name")
  let captured = outer.copy()
  Function({
    name,
    params,
    callback: args => {
      if args.length() != params.length() {
        raise @types.fail(
          "E_ARITY",
          name + " expects " + params.length().to_string() + " arguments",
        )
      }
      let inputs = captured.copy()
      for i, param in params {
        inputs[param] = args[i]
      }
      let outputs = evaluate_service(model, service.attr("name"), inputs)
      if outputs.length() == 1 {
        let mut value : @types.Value = Null
        for _, v in outputs {
          value = v
        }
        value
      } else {
        @types.Context(outputs)
      }
    },
  })
}

///|
fn href_id(model : Model, href : String) -> String raise @types.EngineError {
  let href = if model.root.attr("namespace") != "" {
    href
    .strip_prefix(model.root.attr("namespace"))
    .unwrap_or(href[:])
    .to_owned()
  } else {
    href
  }
  if !href.has_prefix("#") {
    raise @types.fail("E_IMPORT_UNSUPPORTED", "external reference: " + href)
  }
  let id = href.strip_prefix("#").unwrap().to_owned()
  match model.ids.get(id) {
    Some(name) => name
    None => raise @types.fail("E_REFERENCE", "unknown reference: " + href)
  }
}

///|
fn evaluate_service_map(
  model : Model,
  service : @xml.Node,
  inputs : Map[String, @types.Value],
) -> Map[String, @types.Value] raise @types.EngineError {
  for kind in ["inputData", "inputDecision"] {
    for node in service.all(kind) {
      let name = href_id(model, node.attr("href"))
      if !inputs.contains(name) {
        raise @types.fail("E_INPUT_MISSING", "missing service input: " + name)
      }
      if kind == "inputData" {
        let mut found : @xml.Node? = None
        for data in model.root.all("inputData") {
          if data.attr("name") == name {
            found = Some(data)
          }
        }
        match found {
          Some(data) =>
            inputs[name] = coerce_variable(model, data, inputs[name])
          None =>
            raise @types.fail("E_REFERENCE", "unknown input data: " + name)
        }
      } else {
        match model.decisions.get(name) {
          Some(decision) =>
            inputs[name] = coerce_variable(model, decision, inputs[name])
          None => raise @types.fail("E_REFERENCE", "unknown decision: " + name)
        }
      }
    }
  }
  let cache = Map([])
  let visiting = []
  let out = Map([])
  for node in service.all("outputDecision") {
    let name = href_id(model, node.attr("href"))
    out[name] = evaluate_named(model, name, inputs, cache, visiting)
  }
  out
}

///|
/// Evaluate a business knowledge model directly for a TCK `type="bkm"` case.
pub fn evaluate_bkm(
  model : Model,
  name : String,
  arguments : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let node = match model.bkms.get(name) {
    Some(n) => n
    None =>
      raise @types.fail(
        "E_DMN_BKM",
        "unknown business knowledge model: " + name,
      )
  }
  let outer : Map[String, @types.Value] = Map([])
  outer["$types"] = Context(type_registry(model))
  let value = bkm_function(model, node, outer, 0)
  match value {
    Function(f) => {
      let args = []
      for param in f.params {
        match arguments.get(param) {
          Some(v) => args.push(v)
          None =>
            raise @types.fail(
              "E_INPUT_MISSING",
              "missing BK argument: " + param,
            )
        }
      }
      (f.callback)(args)
    }
    _ => value
  }
}

///|
/// Evaluate a decision service and return its output decisions by name.
pub fn evaluate_service(
  model : Model,
  name : String,
  inputs : Map[String, @types.Value],
) -> Map[String, @types.Value] raise @types.EngineError {
  let service = match model.services.get(name) {
    Some(n) => n
    None =>
      raise @types.fail("E_DMN_SERVICE", "unknown decision service: " + name)
  }
  let outputs = evaluate_service_map(model, service, inputs.copy())
  if outputs.length() == 1 {
    for key, value in outputs {
      let wrapped = coerce_variable(
        model,
        service,
        Function({ name: "result", params: [], callback: _ => value, }),
      )
      if wrapped is Function(f) {
        outputs[key] = (f.callback)([])
      }
    }
  }
  outputs
}

///|
fn boxed_raw(
  model : Model,
  node : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  match node.name {
    "decisionTable" => table(node, ctx)
    "literalExpression" => {
      let language = node.attr("expressionLanguage")
      if language != "" && !language.contains("FEEL") {
        raise @types.fail("E_LANGUAGE", "non-FEEL expression language")
      }
      @eval.feel_eval(node.content("text"), ctx~)
    }
    "list" =>
      List(
        node.children
        .filter(n => n.uri == node.uri)
        .map(n => boxed(model, n, ctx)),
      )
    "context" => {
      let scope = ctx.copy()
      let out = Map([])
      let entries = node.all("contextEntry")
      for i, entry in entries {
        let value = coerce_variable(
          model,
          entry,
          boxed(model, expression(entry), scope),
        )
        match entry.child("variable") {
          Some(v) => {
            let name = v.attr("name")
            scope[name] = value
            out[name] = value
          }
          None => {
            if i != entries.length() - 1 {
              raise @types.fail("E_DMN_CONTEXT", "unnamed entry must be last")
            }
            return value
          }
        }
      }
      Context(out)
    }
    "invocation" => {
      let callee_node = expression_child(node)
      let callee = boxed(model, callee_node, ctx)
      let args : Array[(String?, @types.Value)] = []
      for binding in node.all("binding") {
        let parameter = match binding.child("parameter") {
          Some(p) => p.attr("name")
          None =>
            raise @types.fail("E_DMN_INVOCATION", "missing binding parameter")
        }
        args.push(
          (Some(parameter), boxed(model, expression_child(binding), ctx)),
        )
      }
      match callee {
        Function(f) => @eval.call_function(f, args)
        _ =>
          raise @types.fail(
            "E_DMN_INVOCATION", "invocation target is not a function",
          )
      }
    }
    "relation" => {
      let columns = node.all("column").map(c => c.attr("name"))
      let rows = []
      for row in node.all("row") {
        let values = row.children.filter(n => {
          n.uri == node.uri && is_expression_name(n.name)
        })
        if values.length() != columns.length() {
          raise @types.fail("E_DMN_RELATION", "relation row length mismatch")
        }
        let record = Map([])
        for i, value in values {
          record[columns[i]] = boxed(model, value, ctx)
        }
        rows.push(@types.Context(record))
      }
      List(rows)
    }
    "functionDefinition" => {
      let params = node.all("formalParameter").map(p => p.attr("name"))
      let body = expression_child(node)
      let captured = ctx.copy()
      Function({
        name: "function",
        params,
        callback: args => {
          if args.length() != params.length() {
            raise @types.fail(
              "E_ARITY",
              "function expects " + params.length().to_string() + " arguments",
            )
          }
          let scope = captured.copy()
          for i, param in params {
            scope[param] = coerce_type(
              model,
              node.all("formalParameter")[i].attr("typeRef"),
              args[i],
              0,
            )
          }
          boxed(model, body, scope)
        },
      })
    }
    "conditional" => {
      let condition = expression_child(required_child(node, "if"))
      let yes = expression_child(required_child(node, "then"))
      let no = expression_child(required_child(node, "else"))
      match boxed(model, condition, ctx) {
        Boolean(true) => boxed(model, yes, ctx)
        Boolean(false) => boxed(model, no, ctx)
        _ => Null
      }
    }
    "filter" => {
      let expressions = if node.child("in") is Some(domain) &&
        node.child("match") is Some(predicate) {
        [expression_child(domain), expression_child(predicate)]
      } else {
        node.children.filter(n => {
          n.uri == node.uri && is_expression_name(n.name)
        })
      }
      if expressions.length() != 2 {
        raise @types.fail("E_DMN_FILTER", "filter needs a list and a predicate")
      }
      let target = boxed(model, expressions[0], ctx)
      let items = match target {
        List(xs) => xs
        Null => return Null
        _ => [target]
      }
      filter_items(model, items, expressions[1], ctx)
    }
    "for" | "some" | "every" => {
      let kind = node.name
      let bindings = node.all("binding")
      let mut in_node : @xml.Node? = None
      let mut body : @xml.Node? = None
      for child in node.children {
        if child.uri != node.uri {
          continue
        }
        if child.name == "in" {
          in_node = Some(child)
        } else if child.name == "return" || child.name == "satisfies" {
          body = Some(child)
        } else if is_expression_name(child.name) {
          body = Some(child)
        }
      }
      if in_node is Some(domain) {
        let variable = node.attr("iteratorVariable")
        if variable == "" {
          raise @types.fail("E_DMN_ITERATOR", "missing iterator variable")
        }
        let body = match body {
          Some(n) => n
          None => raise @types.fail("E_DMN_ITERATOR", "missing iterator body")
        }
        let source = boxed(model, expression_child(domain), ctx)
        let items = match source {
          List(xs) => xs
          _ => [source]
        }
        let body_expr = expression_child(body)
        return iterator_values(model, kind, variable, items, body_expr, ctx)
      }
      let body = match body {
        Some(n) => n
        None => raise @types.fail("E_DMN_ITERATOR", "missing iterator body")
      }
      iterator(model, kind, bindings, body, ctx)
    }
    _ =>
      raise @types.Failure(
        "E_DMN_UNSUPPORTED",
        node.line,
        node.column,
        "unsupported expression: " + node.name,
      )
  }
}

///|
fn filter_items(
  model : Model,
  items : Array[@types.Value],
  predicate : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let first = if items.is_empty() {
    boxed(model, predicate, ctx)
  } else {
    boxed(model, predicate, scope_with(ctx, "item", items[0]))
  }
  match first {
    Number(n) => {
      if !n.is_integer() {
        raise @types.fail("E_DMN_FILTER", "filter index must be an integer")
      }
      let at = n.to_int()
      if at < 1 || at > items.length() {
        Null
      } else {
        items[at - 1]
      }
    }
    _ => {
      let out = []
      for item in items {
        let keep = match
          boxed(model, predicate, scope_with(ctx, "item", item)) {
          Boolean(b) => b
          Null => false
          _ => return Null
        }
        if keep {
          out.push(item)
        }
      }
      List(out)
    }
  }
}

///|
fn scope_with(
  ctx : Map[String, @types.Value],
  name : String,
  value : @types.Value,
) -> Map[String, @types.Value] {
  let out = ctx.copy()
  out[name] = value
  out
}

///|
fn iterator_values(
  model : Model,
  kind : String,
  variable : String,
  items : Array[@types.Value],
  body : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  if kind == "for" {
    let out = []
    for item in items {
      out.push(boxed(model, body, scope_with(ctx, variable, item)))
    }
    return List(out)
  }
  let some = kind == "some"
  let mut has_null = false
  for item in items {
    match boxed(model, body, scope_with(ctx, variable, item)) {
      Boolean(false) => if !some { return Boolean(false) }
      Boolean(true) => if some { return Boolean(true) }
      Null => has_null = true
      _ => return Null
    }
  }
  if has_null {
    Null
  } else if some {
    Boolean(false)
  } else {
    Boolean(true)
  }
}

///|
fn iterator(
  model : Model,
  kind : String,
  bindings : Array[@xml.Node],
  body : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  let envs = iterator_envs(model, bindings, 0, ctx, 0)
  if kind == "for" {
    return List(envs.map(env => boxed(model, body, env)))
  }
  let some = kind == "some"
  let mut has_null = false
  for env in envs {
    match boxed(model, body, env) {
      Boolean(false) => if !some { return Boolean(false) }
      Boolean(true) => if some { return Boolean(true) }
      Null => has_null = true
      _ => return Null
    }
  }
  if has_null {
    Null
  } else if some {
    Boolean(false)
  } else {
    Boolean(true)
  }
}

///|
fn iterator_envs(
  model : Model,
  bindings : Array[@xml.Node],
  index : Int32,
  scope : Map[String, @types.Value],
  count : Int32,
) -> Array[Map[String, @types.Value]] raise @types.EngineError {
  if count > 100000 {
    raise @types.fail("E_LIMIT", "iterator produces too many values")
  }
  if index >= bindings.length() {
    return [scope.copy()]
  }
  let binding = bindings[index]
  let name = match binding.child("variable") {
    Some(v) => v.attr("name")
    None => raise @types.fail("E_DMN_ITERATOR", "missing iterator variable")
  }
  let value = boxed(model, expression_child(binding), scope)
  let items = match value {
    List(xs) => xs
    _ => [value]
  }
  let out = []
  for item in items {
    let inner = scope.copy()
    inner[name] = item
    out.append(
      iterator_envs(model, bindings, index + 1, inner, count + out.length()),
    )
  }
  out
}

///|
fn required_child(
  node : @xml.Node,
  name : String,
) -> @xml.Node raise @types.EngineError {
  let children = node.all(name)
  if children.length() != 1 {
    raise @types.Failure(
      "E_DMN_SHAPE",
      node.line,
      node.column,
      "expected exactly one " + name + " in " + node.name,
    )
  }
  children[0]
}

///|
fn boxed(
  model : Model,
  node : @xml.Node,
  ctx : Map[String, @types.Value],
) -> @types.Value raise @types.EngineError {
  coerce_type(model, node.attr("typeRef"), boxed_raw(model, node, ctx), 0)
}