///|
fn import_directory(path : String) -> String {
  let parts = path.split("/").to_array()
  ignore(parts.pop())
  if parts.is_empty() {
    ""
  } else {
    parts.join("/") + "/"
  }
}

///|
fn collect_import_inputs(
  model : Model,
  inputs : Map[String, @types.Value],
  shared : Map[String, @types.Value],
) -> Unit {
  let ns = model.root.attr("namespace")
  let merged = match shared.get(ns) {
    Some(Context(c)) => c
    _ => Map([])
  }
  for k, v in inputs {
    if k != "$imports" {
      merged[k] = v
    }
  }
  shared[ns] = Context(merged)
  for (qualifier, child) in model.imports {
    let scope = if qualifier == "" {
      inputs
    } else {
      match inputs.get(qualifier) {
        Some(Context(c)) => c
        _ => Map([])
      }
    }
    collect_import_inputs(child, scope, shared)
  }
}

///|
fn imported_inputs(
  child : Model,
  qualifier : String,
  inputs : Map[String, @types.Value],
) -> Map[String, @types.Value] {
  let scope = match inputs.get("$imports") {
    Some(Context(shared)) =>
      match shared.get(child.root.attr("namespace")) {
        Some(Context(c)) => c.copy()
        _ => Map([])
      }
    _ =>
      if qualifier == "" {
        inputs.copy()
      } else {
        match inputs.get(qualifier) {
          Some(Context(c)) => c.copy()
          _ => Map([])
        }
      }
  }
  for key in ["$imports", "$external", "now", "today"] {
    if inputs.get(key) is Some(v) {
      scope[key] = v
    }
  }
  scope
}

///|
fn bind_import(
  ctx : Map[String, @types.Value],
  qualifier : String,
  name : String,
  value : @types.Value,
) -> Unit {
  if qualifier == "" {
    ctx[name] = value
  } else {
    let fields = match ctx.get(qualifier) {
      Some(Context(c)) => c.copy()
      _ => Map([])
    }
    fields[name] = value
    ctx[qualifier] = Context(fields)
  }
}

///|
fn import_scope(
  model : Model,
  inputs : Map[String, @types.Value],
  ctx : Map[String, @types.Value],
) -> Unit raise @types.EngineError {
  for (qualifier, child) in model.imports {
    let scope = imported_inputs(child, qualifier, inputs)
    for node in child.root.all("inputData") {
      let name = node.attr("name")
      if import_visible(model, qualifier, child, name) &&
        scope.get(name) is Some(v) {
        bind_import(ctx, qualifier, name, coerce_variable(child, node, v))
      }
    }
    // A direct import also exposes its imported input contexts.
    let nested = scope.copy()
    import_scope(child, scope, nested)
    for (nested_alias, _) in child.imports {
      if nested_alias != "" &&
        import_visible(model, qualifier, child, nested_alias) &&
        nested.get(nested_alias) is Some(v) {
        bind_import(ctx, qualifier, nested_alias, v)
      }
    }
  }
}

///|
fn imported_requirement(
  model : Model,
  reference : @xml.Node,
  inputs : Map[String, @types.Value],
  ctx : Map[String, @types.Value],
) -> Bool raise @types.EngineError {
  let href = reference.attr("href")
  for (qualifier, child) in model.imports {
    let prefix = child.root.attr("namespace") + "#"
    if !href.has_prefix(prefix) {
      continue
    }
    let name = href_id(child, href) catch {
      @types.Failure(code, _, _, message) =>
        raise @types.Failure(code, reference.line, reference.column, message)
    }
    if !import_visible(model, qualifier, child, name) {
      return true
    }
    let scope = imported_inputs(child, qualifier, inputs)
    let value = match reference.name {
      "requiredInput" =>
        match scope.get(name) {
          Some(v) => {
            let mut value = v
            for node in child.root.all("inputData") {
              if node.attr("name") == name {
                value = coerce_variable(child, node, v)
              }
            }
            value
          }
          None =>
            raise @types.Failure(
              "E_INPUT_MISSING",
              reference.line,
              reference.column,
              "missing imported input: " + name,
            )
        }
      "requiredDecision" =>
        match scope.get(name) {
          Some(v) => coerce_variable(child, child.decisions[name], v)
          None => evaluate_decision(child, name, scope)
        }
      "requiredKnowledge" =>
        if child.bkms.get(name) is Some(n) {
          bkm_function(child, n, scope, 0)
        } else if child.services.get(name) is Some(n) {
          service_function(child, n, scope)
        } else {
          raise @types.Failure(
            "E_REFERENCE",
            reference.line,
            reference.column,
            "unknown imported knowledge: " + href,
          )
        }
      _ =>
        raise @types.Failure(
          "E_REFERENCE",
          reference.line,
          reference.column,
          "unknown requirement: " + reference.name,
        )
    }
    bind_import(ctx, qualifier, name, value)
    return true
  }
  if !href.has_prefix("#") &&
    !href.has_prefix(model.root.attr("namespace") + "#") {
    raise @types.Failure(
      "E_REFERENCE",
      reference.line,
      reference.column,
      "unresolved namespace reference: " + href,
    )
  }
  false
}

///|
fn imported_type(model : Model, name : String) -> (Model, String)? {
  for (qualifier, child) in model.imports {
    if qualifier == "" {
      if import_visible(model, qualifier, child, name) &&
        child.root.all("itemDefinition").any(n => n.attr("name") == name) {
        return Some((child, name))
      }
    } else if name.strip_prefix(qualifier + ".") is Some(rest) {
      return Some((child, rest.to_owned()))
    }
  }
  None
}

///|
/// Bind a TCK/host input to its declaring namespace, or the root scope.
pub fn bind_input(
  inputs : Map[String, @types.Value],
  name : String,
  value : @types.Value,
  model_namespace~ : String = "",
) -> Unit {
  if model_namespace == "" {
    inputs[name] = value
    return
  }
  let shared = match inputs.get("$imports") {
    Some(Context(c)) => c
    _ => Map([])
  }
  let scope = match shared.get(model_namespace) {
    Some(Context(c)) => c
    _ => Map([])
  }
  scope[name] = value
  shared[model_namespace] = Context(scope)
  inputs["$imports"] = Context(shared)
}

///|
fn direct_exported_names(model : Model) -> Map[String, String] {
  let out = Map([])
  for node in model.root.children {
    if node.uri == model.root.uri &&
      [
        "decision", "inputData", "businessKnowledgeModel", "decisionService", "itemDefinition",
      ].contains(node.name) {
      let name = if node.attr("name") == "" {
        node.attr("id")
      } else {
        node.attr("name")
      }
      out[name] = model.root.attr("namespace")
    }
  }
  for (qualifier, child) in model.imports {
    if qualifier != "" {
      out[qualifier] = "alias:" + child.root.attr("namespace")
    }
  }
  out
}

///|
fn exported_names(model : Model) -> Map[String, String] {
  let out = direct_exported_names(model)
  for (qualifier, child) in model.imports {
    if qualifier == "" {
      for name, owner in direct_exported_names(child) {
        if !out.contains(name) {
          out[name] = owner
        }
      }
    }
  }
  out
}

///|
fn import_visible(
  model : Model,
  qualifier : String,
  child : Model,
  name : String,
) -> Bool {
  qualifier != "" ||
  (
    direct_exported_names(child).get(name) is Some(owner) &&
    exported_names(model).get(name) == Some(owner)
  )
}