///|
fn find_attr_key_case_insensitive(node : @dom.Node, target : String) -> String? {
  for key, _ in node.attrs {
    if @syn.lower_ascii(key) == target {
      return Some(key)
    }
  }
  None
}

///|
fn attr_value_is_refresh(value : String?) -> Bool {
  match value {
    Some(raw) => @syn.lower_ascii(raw[:].trim()) == "refresh"
    None => false
  }
}

///|
fn transform_attr_is_url_like(
  attr_name : String,
  raw_value : String?,
  effectively_foreign : Bool,
) -> Bool {
  let foreign_url_function_loads = match raw_value {
    Some(value) => @san.css_value_may_load_external_resource(value)
    None => false
  }
  @san.is_single_url_like_attr(attr_name) ||
  @san.is_srcset_like_attr(attr_name) ||
  @san.is_space_separated_url_list_attr(attr_name) ||
  (
    effectively_foreign &&
    @san.is_foreign_url_function_like_attr(attr_name) &&
    foreign_url_function_loads
  )
}

///|
fn transform_report_unsafe_url_attr(
  node : @dom.Node,
  transform : TransformSpec,
  attr_name : String,
  reason? : String,
) -> Unit {
  let suffix = match reason {
    Some(value) => " (" + value + ")"
    None => ""
  }
  transform_report(
    transform,
    "Unsafe URL in attribute '" + attr_name + "'" + suffix,
    Some(node),
  )
}

///|
fn apply_drop_url_attrs_transform(
  node : @dom.Node,
  transform : TransformSpec,
) -> Bool {
  guard transform.sanitize_policy is Some(policy) else { return false }
  if node.attrs.length() == 0 {
    return false
  }
  let to_drop : Array[String] = []
  let to_set : Map[String, String?] = {}
  let tag_name = @syn.lower_ascii(node.name)
  match
    (
      find_attr_key_case_insensitive(node, "http-equiv"),
      find_attr_key_case_insensitive(node, "content"),
    ) {
    (Some(http_equiv_key), Some(content_key)) => {
      let http_equiv_value = node.attrs.get(http_equiv_key).unwrap()
      if attr_value_is_refresh(http_equiv_value) {
        transform_report_unsafe_url_attr(
          node,
          transform,
          "content",
          reason="meta refresh",
        )
        @san.push_unique_string(to_drop, content_key)
      }
    }
    _ => ()
  }
  let effectively_foreign = @san.node_is_effectively_foreign(node)
  let keys = node.attrs.keys().to_array()
  for key in keys {
    let attr_name = @syn.lower_ascii(key)
    let current_value = node.attrs.get(key).unwrap()
    if !transform_attr_is_url_like(
        attr_name, current_value, effectively_foreign,
      ) {
      continue
    }
    if tag_name == "base" && attr_name == "href" {
      transform_report_unsafe_url_attr(
        node,
        transform,
        "href",
        reason="base tag",
      )
      @san.push_unique_string(to_drop, key)
      continue
    }
    match current_value {
      Some(raw_value) =>
        if !policy.has_url_rule(tag_name, attr_name) {
          transform_report_unsafe_url_attr(
            node,
            transform,
            attr_name,
            reason="no rule",
          )
          @san.push_unique_string(to_drop, key)
        } else {
          match
            policy.sanitize_attribute_value(
              tag_name,
              attr_name,
              raw_value,
              effectively_foreign~,
            ) {
            Some(cleaned) =>
              if cleaned != raw_value || attr_name != key {
                @san.push_unique_string(to_drop, key)
                to_set[attr_name] = Some(cleaned)
              }
            None => {
              transform_report_unsafe_url_attr(node, transform, attr_name)
              @san.push_unique_string(to_drop, key)
            }
          }
        }
      None => {
        transform_report_unsafe_url_attr(node, transform, attr_name)
        @san.push_unique_string(to_drop, key)
      }
    }
  }
  if to_drop.is_empty() && to_set.is_empty() {
    false
  } else {
    transform_call_hook(node, transform)
    let out = node.attrs.copy()
    for key in to_drop {
      out.remove(key)
    }
    for key, value in to_set {
      out[key] = value
    }
    attrs_replace_with(node, out)
    true
  }
}

///|
fn apply_allow_style_attrs_transform(
  node : @dom.Node,
  transform : TransformSpec,
) -> Bool {
  guard transform.sanitize_policy is Some(policy) else { return false }
  guard find_attr_key_case_insensitive(node, "style") is Some(style_key) else {
    return false
  }
  let tag_name = @syn.lower_ascii(node.name)
  match node.attrs.get(style_key).unwrap() {
    Some(raw_value) =>
      match policy.sanitize_inline_style_value(tag_name, raw_value) {
        Some(cleaned) =>
          if cleaned != raw_value || style_key != "style" {
            transform_call_hook(node, transform)
            node.attrs.remove(style_key)
            node.attrs["style"] = Some(cleaned)
            true
          } else {
            false
          }
        None => {
          transform_report(
            transform,
            "Unsafe inline style in attribute 'style'",
            Some(node),
          )
          transform_call_hook(node, transform)
          node.attrs.remove(style_key)
          true
        }
      }
    None => {
      transform_report(
        transform,
        "Unsafe inline style in attribute 'style'",
        Some(node),
      )
      transform_call_hook(node, transform)
      node.attrs.remove(style_key)
      true
    }
  }
}