///|
fn misc_args(
  transform : TransformConfig,
  key : String,
) -> Json raise BuildError {
  let out : Map[String, Json] = Map([])
  match transform.args {
    Array(values) => out[key] = Json::array(values)
    Object(fields) => {
      for name, value in fields {
        if name != "__args" {
          out[name] = value
        }
      }
      if fields.get("__args") is Some(value) {
        out[key] = value
      }
    }
    _ =>
      raise InvalidBuildConfig("Expected transform arguments to be an object")
  }
  Json::object(out)
}

///|
fn misc_validator_ids(model : @smithy.Model) -> Array[String] {
  let out : Array[String] = []
  if model.metadata.get("validators") is Some(@smithy.Array(values)) {
    for value in values {
      if value is @smithy.Object(fields) {
        let value = match fields.get("id") {
          Some(value) => Some(value)
          None => fields.get("name")
        }
        match value {
          Some(@smithy.String(name)) => out.push(name)
          _ => ()
        }
      }
    }
  }
  out
}

///|
fn misc_filter_suppressions(
  model : @smithy.Model,
  args : Json,
  original : @smithy.Model,
  original_events : Array[@smithy.ValidationEvent],
) -> @smithy.Model raise BuildError {
  let events_allow = config_strings(args, "eventIdAllowList")
  let events_deny = config_strings(args, "eventIdDenyList")
  let namespaces_allow = config_strings(args, "namespaceAllowList")
  let namespaces_deny = config_strings(args, "namespaceDenyList")
  if !events_allow.is_empty() && !events_deny.is_empty() {
    raise InvalidBuildConfig(
      "filterSuppressions cannot set both eventIdAllowList and eventIdDenyList",
    )
  }
  if !namespaces_allow.is_empty() && !namespaces_deny.is_empty() {
    raise InvalidBuildConfig(
      "filterSuppressions cannot set both namespaceAllowList and namespaceDenyList",
    )
  }
  let remove_unused = rewrite_bool(args, "removeUnused")
  let remove_reasons = rewrite_bool(args, "removeReasons")
  let current_validators = misc_validator_ids(model)
  let removed_validators = misc_validator_ids(original).filter(id => {
    !current_validators.contains(id)
  })
  let events = original_events.filter(event => {
    event.severity == "SUPPRESSED" && !removed_validators.contains(event.id)
  })
  fn allowed(
    value : String,
    allow : Array[String],
    deny : Array[String],
  ) -> Bool {
    (allow.is_empty() || allow.contains(value)) && !deny.contains(value)
  }
  let suppress_id = @smithy.ShapeId::parse("smithy.api#suppress").unwrap()
  let shapes = model.shapes.copy()
  for id, shape in model.shapes {
    guard shape.traits.get(suppress_id) is Some(@smithy.Array(values)) else {
      continue
    }
    let kept = values.filter(value => {
      match value {
        @smithy.String(rule) =>
          allowed(rule, events_allow, events_deny) &&
          (
            !remove_unused ||
            events
            .iter()
            .any(event => {
              event.shape == Some(id) &&
              @smithy.suppression_matches(rule, event.id)
            })
          )
        _ => false
      }
    })
    let traits = shape.traits.copy()
    if kept.is_empty() {
      traits.remove(suppress_id)
    } else {
      traits[suppress_id] = @smithy.Array(kept)
    }
    shapes[id] = { ..shape, traits, }
  }
  let metadata = model.metadata.copy()
  if model.metadata.get("suppressions") is Some(@smithy.Array(values)) {
    let kept : Array[@smithy.NodeValue] = []
    for value in values {
      guard value is @smithy.Object(fields) else {
        raise InvalidBuildConfig(
          "Expected metadata suppressions to contain objects",
        )
      }
      guard fields.get("id") is Some(@smithy.String(id)) &&
        fields.get("namespace") is Some(@smithy.String(scope)) else {
        raise InvalidBuildConfig(
          "Expected suppression id and namespace strings",
        )
      }
      if !allowed(id, events_allow, events_deny) ||
        !allowed(scope, namespaces_allow, namespaces_deny) {
        continue
      }
      if remove_unused &&
        !events
        .iter()
        .any(event => {
          @smithy.suppression_matches(id, event.id) &&
          (
            scope == "*" ||
            event.shape.map(id => id.namespace == scope).unwrap_or(false)
          )
        }) {
        continue
      }
      let fields = fields.copy()
      if remove_reasons {
        fields.remove("reason")
      }
      kept.push(@smithy.Object(fields))
    }
    if kept != values {
      if kept.is_empty() {
        metadata.remove("suppressions")
      } else {
        metadata["suppressions"] = @smithy.Array(kept)
      }
    }
  }
  { shapes, metadata, }
}

///|
fn misc_flatten_closure(
  model : @smithy.Model,
  args : Json,
) -> @smithy.Model raise BuildError {
  guard config_string(args, "namespace") is Some(target_namespace) &&
    config_string(args, "closure") is Some(name) else {
    raise InvalidBuildConfig(
      "Expected `closure` and `namespace` on flattenClosureNamespaces",
    )
  }
  let mut declared : Json? = None
  if model.metadata.get("shapeClosures") is Some(@smithy.Array(entries)) {
    for entry in entries {
      let entry = @json.to_json(entry)
      if config_string(entry, "id") == Some(name) {
        declared = Some(entry)
      }
    }
  }
  guard declared is Some(declared) else {
    raise InvalidBuildConfig(
      "No shape closure named `" + name + "` is defined in the model",
    )
  }
  let namespaces = config_strings(declared, "includeNamespaces")
  let roots : Array[@smithy.ShapeId] = []
  for id, _ in model.shapes {
    if namespaces.contains(id.namespace) {
      roots.push(id)
    }
  }
  if config_string(declared, "includeBySelector") is Some(selector) {
    let ids = shape_select(model, selector) catch { _ => [] }
    for id in ids {
      if @smithy.ShapeId::parse(id) is Some(id) {
        roots.push(id)
      }
    }
  }
  let closure = shape_closure(model, roots)
  let names = config_object(declared, "rename")
  let renamed : Map[@smithy.ShapeId, @smithy.ShapeId] = Map([])
  for id, _ in model.shapes {
    if id.member is Some(_) ||
      id.namespace == "smithy.api" ||
      !closure.contains(id) {
      continue
    }
    let name = match names.get(id.to_string()) {
      None => id.name
      Some(String(name)) => name
      _ =>
        raise InvalidBuildConfig(
          "Expected shape closure rename names to be strings",
        )
    }
    let target = rewrite_shape_id(target_namespace + "#" + name)
    if id != target {
      renamed[id] = target
    }
  }
  rewrite_rename(model, renamed)
}

///|
fn transform_misc(
  model : @smithy.Model,
  transform : TransformConfig,
  original_model~ : @smithy.Model? = None,
  original_events~ : Array[@smithy.ValidationEvent]? = None,
) -> @smithy.Model? raise BuildError {
  match transform.name {
    "includeTags" | "excludeTags" => {
      let tags = config_strings(misc_args(transform, "tags"), "tags")
      let tag_id = @smithy.ShapeId::parse("smithy.api#tags").unwrap()
      let shapes = model.shapes.copy()
      for id, shape in model.shapes {
        guard shape.traits.get(tag_id) is Some(@smithy.Array(values)) else {
          continue
        }
        let kept : Array[@smithy.NodeValue] = []
        for value in values {
          if value is @smithy.String(tag) &&
            tags.contains(tag) == (transform.name == "includeTags") &&
            !kept.contains(value) {
            kept.push(value)
          }
        }
        let traits = shape.traits.copy()
        traits[tag_id] = @smithy.Array(kept)
        shapes[id] = { ..shape, traits, }
      }
      Some({ shapes, metadata: model.metadata, })
    }
    "includeTraitsByTag" | "excludeTraitsByTag" => {
      let tags = config_strings(misc_args(transform, "tags"), "tags")
      Some(
        shape_filter(model, shape => {
          shape.trait_value("smithy.api#trait") is None ||
          shape_tagged(shape, tags) == (transform.name == "includeTraitsByTag")
        }),
      )
    }
    "includeTraits" | "excludeTraits" => {
      let patterns = config_strings(misc_args(transform, "traits"), "traits")
      let names : Array[String] = []
      let namespaces : Array[String] = []
      for pattern in patterns {
        if pattern.has_suffix("#") {
          namespaces.push(pattern[:pattern.length() - 1].to_owned())
        } else {
          names.push(rewrite_shape_id(transform_trait_id(pattern)).to_string())
        }
      }
      if transform.name == "includeTraits" {
        names.push("smithy.api#trait")
      }
      let removed : Map[@smithy.ShapeId, Bool] = Map([])
      for id, shape in model.shapes {
        let matched = names.contains(id.to_string()) ||
          namespaces.contains(id.namespace)
        if shape.trait_value("smithy.api#trait") is Some(_) &&
          matched == (transform.name == "excludeTraits") {
          removed[id] = true
        }
      }
      Some(shape_remove(model, removed))
    }
    "includeMetadata" | "excludeMetadata" => {
      let keys = config_strings(misc_args(transform, "keys"), "keys")
      let metadata : Map[String, @smithy.NodeValue] = Map([])
      for name, value in model.metadata {
        if keys.contains(name) == (transform.name == "includeMetadata") {
          metadata[name] = value
        }
      }
      Some({ shapes: model.shapes, metadata, })
    }
    "makeIdempotencyTokensClientOptional" => {
      let shapes = model.shapes.copy()
      let client_optional = @smithy.ShapeId::parse("smithy.api#clientOptional").unwrap()
      for id, shape in model.shapes {
        if shape.body is @smithy.Member(..) &&
          shape.trait_value("smithy.api#required") is Some(_) &&
          shape.trait_value("smithy.api#idempotencyToken") is Some(_) &&
          !shape.traits.contains(client_optional) {
          let traits = shape.traits.copy()
          traits[client_optional] = @smithy.Object(Map([]))
          shapes[id] = { ..shape, traits, }
        }
      }
      Some({ shapes, metadata: model.metadata, })
    }
    "flattenClosureNamespaces" =>
      Some(misc_flatten_closure(model, transform.args))
    "filterSuppressions" => {
      let original = original_model.unwrap_or(model)
      let events = match original_events {
        Some(events) => events
        None => original.validation_events()
      }
      Some(misc_filter_suppressions(model, transform.args, original, events))
    }
    _ => None
  }
}