///|
/// A Smithy validation finding after configured severity and suppression rules.
pub(all) struct ValidationEvent {
  id : String
  severity : String
  shape : ShapeId?
  message : String
  source : SourceLocation?
  suppression_reason : String?
} derive(Eq, Debug)

///|
/// Event IDs form a dot-delimited hierarchy.
pub fn suppression_matches(rule_id : String, event_id : String) -> Bool {
  event_id == rule_id || event_id.has_prefix(rule_id + ".")
}

///|
fn validation_field(value : NodeValue, name : String) -> NodeValue? {
  match value {
    Object(fields) => fields.get(name)
    _ => None
  }
}

///|
fn validation_string(value : NodeValue, name : String) -> String? {
  match validation_field(value, name) {
    Some(String(s)) => Some(s)
    _ => None
  }
}

///|
fn validation_array(value : NodeValue?) -> Array[NodeValue] {
  match value {
    Some(Array(values)) => values
    _ => []
  }
}

///|
fn validation_event(
  id : String,
  severity : String,
  shape : Shape?,
  message : String,
) -> ValidationEvent {
  {
    id,
    severity,
    shape: shape.map(s => s.id),
    message,
    source: shape.map(s => s.source),
    suppression_reason: None,
  }
}

///|
fn validator_config_error(name : String, detail : String) -> ValidationEvent {
  validation_event(
    "Model",
    "ERROR",
    None,
    "Error creating `" + name + "` validator: " + detail,
  )
}

///|
fn validation_node_type(value : NodeValue) -> String {
  match value {
    Null => "null"
    Boolean(_) => "boolean"
    Number(_) => "number"
    String(_) => "string"
    Array(_) => "array"
    Object(_) => "object"
  }
}

///|
fn Model::validation_metadata_events(self : Model) -> Array[ValidationEvent] {
  let events : Array[ValidationEvent] = []
  if self.metadata.get("validators") is Some(value) {
    match value {
      Array(definitions) =>
        for definition in definitions {
          if !(definition is Object(_)) {
            events.push(
              validation_event(
                "Model",
                "ERROR",
                None,
                "Each element of `validators` must be an object. Found " +
                validation_node_type(definition) +
                ".",
              ),
            )
          }
        }
      _ =>
        events.push(
          validation_event(
            "Model",
            "ERROR",
            None,
            "validators must be an array. Found " +
            validation_node_type(value) +
            ".",
          ),
        )
    }
  }
  for name in ["suppressions", "severityOverrides"] {
    guard self.metadata.get(name) is Some(value) else { continue }
    guard value is Array(rules) else {
      events.push(
        validation_event(
          "Model",
          "ERROR",
          None,
          "Expected array, but found " + validation_node_type(value) + ".",
        ),
      )
      continue
    }
    let mut invalid_array = false
    for i = 0; i < rules.length(); i = i + 1 {
      if !(rules[i] is Object(_)) {
        events.push(
          validation_event(
            "Model",
            "ERROR",
            None,
            "Expected array element " +
            i.to_string() +
            " to be a object but found " +
            validation_node_type(rules[i]) +
            ".",
          ),
        )
        invalid_array = true
        break
      }
    }
    if invalid_array {
      continue
    }
    let required = if name == "severityOverrides" {
      ["id", "namespace", "severity"]
    } else {
      ["id", "namespace"]
    }
    for rule in rules {
      let mut invalid_rule = false
      for key in required {
        let detail = match validation_field(rule, key) {
          None => Some("Missing expected member `" + key + "`.")
          Some(String(_)) => None
          Some(value) =>
            Some(
              "Expected string, but found " + validation_node_type(value) + ".",
            )
        }
        if detail is Some(message) {
          events.push(validation_event("Model", "ERROR", None, message))
          invalid_rule = true
          break
        }
      }
      if invalid_rule {
        continue
      }
      if name == "severityOverrides" &&
        validation_string(rule, "severity") is Some(severity) &&
        severity != "WARNING" &&
        severity != "DANGER" {
        events.push(
          validation_event(
            "Model",
            "ERROR",
            None,
            "Expected one of `DANGER`, `WARNING`; got `" + severity + "`.",
          ),
        )
      }
    }
  }
  events
}

///|
fn validation_namespace_matches(rule : String, event : ValidationEvent) -> Bool {
  rule == "*" ||
  (match event.shape {
    Some(shape) => shape.namespace == rule
    None => false
  })
}

///|
fn validation_severity_rank(severity : String) -> Int32 {
  match severity {
    "WARNING" => 2
    "DANGER" => 3
    "ERROR" => 4
    _ => 1
  }
}

///|
priv enum ValidationTemplatePart {
  TemplateText(String)
  TemplateAttribute(Array[String])
}

///|
fn validation_template_parts(
  template : String,
) -> Array[ValidationTemplatePart] raise SelectorError {
  let parser : SelectorParser = {
    chars: template.to_array(),
    pos: 0,
    depth: 0,
  }
  let parts : Array[ValidationTemplatePart] = []
  let mut literal = ""
  while parser.peek() != '\u{0}' {
    if parser.peek() != '@' {
      literal += parser.take().to_string()
    } else if parser.pos + 1 < parser.chars.length() &&
      parser.chars[parser.pos + 1] == '@' {
      parser.pos += 2
      literal += "@"
    } else {
      if !literal.is_empty() {
        parts.push(TemplateText(literal))
        literal = ""
      }
      guard parser.scoped_value() is Path(path) else {
        raise InvalidSelector("I expected a message template attribute.")
      }
      parts.push(TemplateAttribute(path))
    }
  }
  if !literal.is_empty() {
    parts.push(TemplateText(literal))
  }
  parts
}

///|
fn validation_template_value(
  path : Array[String],
  shape : Shape,
  variables : Map[String, Array[String]],
  model : Model,
) -> String {
  if path[0] == "var" && path.length() >= 2 {
    let values : Array[String] = []
    for id in variables.get(path[1]).unwrap_or([]) {
      if path.length() == 2 {
        values.push(id)
      } else if model.get(id) is Some(captured) {
        values.append(selector_attribute(captured, path[2:].to_owned()))
      }
    }
    return "[" + values.join(", ") + "]"
  }
  if path[0] == "trait" &&
    path.length() >= 2 &&
    selector_trait(shape, path[1]) is Some(initial) {
    let values = selector_node_path([initial], path, 2).map(v => {
      @json.to_json(v).stringify()
    })
    return if values.length() == 1 {
      values[0]
    } else {
      "[" + values.join(", ") + "]"
    }
  }
  selector_attribute(shape, path).join(", ")
}

///|
fn validation_template(
  parts : Array[ValidationTemplatePart],
  shape : Shape,
  variables : Map[String, Array[String]],
  model : Model,
) -> String {
  let out = StringBuilder()
  for part in parts {
    match part {
      TemplateText(text) => out.write_string(text)
      TemplateAttribute(path) =>
        out.write_string(
          validation_template_value(path, shape, variables, model),
        )
    }
  }
  out.to_string()
}

///|
fn Model::decorate_validation_event(
  self : Model,
  event : ValidationEvent,
) -> ValidationEvent {
  if event.severity == "ERROR" || event.severity == "SUPPRESSED" {
    return event
  }
  if event.shape is Some(id) &&
    self.shapes.get(id) is Some(shape) &&
    shape.trait_value("smithy.api#suppress") is Some(Array(rules)) {
    for rule in rules {
      if rule is String(rule_id) && suppression_matches(rule_id, event.id) {
        return { ..event, severity: "SUPPRESSED", }
      }
    }
  }
  for rule in validation_array(self.metadata.get("suppressions")) {
    if validation_string(rule, "id") is Some(rule_id) &&
      validation_string(rule, "namespace") is Some(namespace) &&
      suppression_matches(rule_id, event.id) &&
      validation_namespace_matches(namespace, event) {
      return {
        ..event,
        severity: "SUPPRESSED",
        suppression_reason: validation_string(rule, "reason"),
      }
    }
  }
  let mut severity = event.severity
  for rule in validation_array(self.metadata.get("severityOverrides")) {
    if validation_string(rule, "id") is Some(rule_id) &&
      validation_string(rule, "namespace") is Some(namespace) &&
      validation_string(rule, "severity") is Some(override) &&
      suppression_matches(rule_id, event.id) &&
      validation_namespace_matches(namespace, event) &&
      (override == "WARNING" || override == "DANGER") &&
      validation_severity_rank(override) > validation_severity_rank(severity) {
      severity = override
    }
  }
  { ..event, severity, }
}

///|
fn validator_definition_error(definition : NodeValue) -> String? {
  match validation_field(definition, "name") {
    None => return Some("Missing expected member `name`.")
    Some(String(_)) => ()
    Some(value) =>
      return Some(
        "Expected `name` to be a string; found " + validation_node_type(value),
      )
  }
  for key in ["id", "message", "severity"] {
    if validation_field(definition, key) is Some(value) && !(value is String(_)) {
      return Some(
        "Expected `" +
        key +
        "` to be a string; found " +
        validation_node_type(value),
      )
    }
  }
  if validation_string(definition, "severity") is Some(severity) &&
    !["DANGER", "WARNING", "NOTE"].contains(severity) {
    return Some(
      "Expected one of `DANGER`, `WARNING`, `NOTE`; got `" + severity + "`.",
    )
  }
  if validation_field(definition, "namespaces") is Some(value) {
    guard value is Array(namespaces) else {
      return Some(
        "Expected `namespaces` to be an array of strings. Found " +
        validation_node_type(value) +
        ".",
      )
    }
    for namespace in namespaces {
      if !(namespace is String(_)) {
        return Some(
          "Expected string, but found " + validation_node_type(namespace) + ".",
        )
      }
    }
  }
  if validation_field(definition, "configuration") is Some(value) &&
    !(value is Object(_)) {
    return Some(
      "Expected `configuration` to be an object; found " +
      validation_node_type(value),
    )
  }
  if validation_field(definition, "selector") is Some(value) {
    guard value is String(selector) else {
      return Some(
        "Expected `selector` to be a string; found " +
        validation_node_type(value),
      )
    }
    ignore(Selector::parse(selector)) catch {
      InvalidSelector(message) => return Some(message)
    }
  }
  None
}

///|
fn validator_factory_configuration_error(
  name : String,
  configuration : NodeValue,
) -> String? {
  if name == "UnreferencedShape" {
    if configuration is Object(fields) &&
      fields.keys().any(key => key != "rootShapeSelector") {
      return Some(
        "Unexpected property in UnreferencedShape configuration; expected only `rootShapeSelector`.",
      )
    }
    if validation_field(configuration, "rootShapeSelector") is Some(value) &&
      !(value is String(_)) {
      return Some(
        "Expected `rootShapeSelector` to be a string; found " +
        validation_node_type(value),
      )
    }
  }
  if name == "EmitEachSelector" || name == "EmitNoneSelector" {
    if validation_field(configuration, "selector") is Some(value) &&
      !(value is String(_)) {
      return Some(
        "Expected `selector` to be a string; found " +
        validation_node_type(value),
      )
    }
  }
  if name == "EmitEachSelector" {
    if validation_field(configuration, "bindToTrait") is Some(value) &&
      value != Null {
      guard value is String(id) else {
        return Some(
          "Expected `bindToTrait` to be a string; found " +
          validation_node_type(value),
        )
      }
      if ShapeId::parse(id) is None {
        return Some("Invalid shape ID: " + id)
      }
    }
    if validation_field(configuration, "messageTemplate") is Some(value) &&
      !(value is String(_)) {
      return Some(
        "Expected `messageTemplate` to be a string; found " +
        validation_node_type(value),
      )
    }
  }
  None
}

///|
fn Model::configured_validation_events(
  self : Model,
  definition : NodeValue,
) -> Array[ValidationEvent] {
  let events : Array[ValidationEvent] = []
  if !(definition is Object(_)) {
    return events
  }
  if validator_definition_error(definition) is Some(message) {
    events.push(validation_event("Model", "ERROR", None, message))
    return events
  }
  guard validation_string(definition, "name") is Some(name) else {
    return events
  }
  let id = validation_string(definition, "id").unwrap_or(name)
  let configuration = validation_field(definition, "configuration").unwrap_or(
    Object(Map([])),
  )
  if validator_factory_configuration_error(name, configuration) is Some(message) {
    events.push(validator_config_error(name, message))
    return events
  }
  let selector_text = validation_string(configuration, "selector")
  if name == "UnreferencedShape" {
    let expression = validation_string(configuration, "rootShapeSelector").unwrap_or(
      "service",
    )
    let selector = Selector::parse(expression) catch {
      _ => {
        events.push(
          validator_config_error(name, "Error parsing rootShapeSelector"),
        )
        return events
      }
    }
    let reachable : Map[String, Bool] = Map([])
    let pending = selector.select_ids(self)
    for shape_id, shape in self.shapes {
      if shape.trait_value("smithy.api#trait") is Some(_) ||
        shape.trait_value("smithy.api#metadata") is Some(_) {
        pending.push(shape_id.to_string())
      }
    }
    let edges = selector_edges(self)
    while !pending.is_empty() {
      let current = pending.pop().unwrap()
      if reachable.contains(current) {
        continue
      }
      reachable[current] = true
      for edge in edges {
        if edge.from.to_string() == current {
          pending.push(edge.to.to_string())
        }
      }
    }
    for shape_id, shape in self.shapes {
      if shape.source.file != "" &&
        !shape_id.to_string().has_prefix("smithy.api#") &&
        !(shape.body is Member(..)) &&
        !reachable.contains(shape_id.to_string()) {
        events.push(
          validation_event(
            name,
            "NOTE",
            Some(shape),
            "This shape is unreferenced. It has no modeled connections to shapes that match the following selector: `" +
            expression +
            "`",
          ),
        )
      }
    }
  } else if name == "EmitEachSelector" || name == "EmitNoneSelector" {
    guard selector_text is Some(expression) else {
      events.push(validator_config_error(name, "selector is required"))
      return events
    }
    let selector = Selector::parse(expression) catch {
      _ => {
        events.push(validator_config_error(name, "Error parsing selector"))
        return events
      }
    }
    let mut matches = selector.select_ids(self)
    // The reference ignores prelude matches when deciding whether EmitNone fires.
    if name == "EmitNoneSelector" {
      matches = matches.filter(shape_id => !shape_id.has_prefix("smithy.api#"))
      if matches.is_empty() {
        events.push(
          validation_event(
            name,
            "DANGER",
            None,
            "Expected at least one shape to match selector: " + expression,
          ),
        )
      }
    } else {
      let bound = validation_string(configuration, "bindToTrait")
      match validation_string(configuration, "messageTemplate") {
        Some(template) => {
          let parts = validation_template_parts(template) catch {
            InvalidSelector(message) => {
              events.push(
                validator_config_error(
                  name,
                  "Invalid message template: " + message,
                ),
              )
              return events
            }
          }
          for selection in selector.select_matches(self) {
            guard self.get(selection.id) is Some(shape) else { continue }
            if bound is Some(trait_id) && shape.trait_value(trait_id) is None {
              continue
            }
            events.push(
              validation_event(
                name,
                "DANGER",
                Some(shape),
                validation_template(parts, shape, selection.variables, self),
              ),
            )
          }
        }
        None =>
          for shape_id in matches {
            guard self.get(shape_id) is Some(shape) else { continue }
            if bound is Some(trait_id) && shape.trait_value(trait_id) is None {
              continue
            }
            events.push(
              validation_event(
                name,
                "DANGER",
                Some(shape),
                "Selector capture matched selector: " + expression,
              ),
            )
          }
      }
    }
  } else {
    events.push(
      validation_event(
        "UnknownValidator_" + name,
        "WARNING",
        None,
        "Unable to locate a validator named `" + name + "`",
      ),
    )
    return events
  }
  let candidate_ids = match validation_string(definition, "selector") {
    Some(expression) => {
      let selector = Selector::parse(expression) catch { _ => return [] }
      Some(selector.select_ids(self))
    }
    None => None
  }
  let namespaces = validation_array(validation_field(definition, "namespaces"))
  let configured_severity = validation_string(definition, "severity")
  let configured_message = validation_string(definition, "message")
  let result : Array[ValidationEvent] = []
  for event in events {
    if event.shape is Some(shape) {
      if !namespaces.is_empty() &&
        !namespaces.iter().any(ns => ns == String(shape.namespace)) {
        continue
      }
      if candidate_ids is Some(ids) && !ids.contains(shape.to_string()) {
        continue
      }
    }
    let suffix = match event.id.find(".") {
      Some(at) => event.id[at:].to_owned()
      None => ""
    }
    let severity = match configured_severity {
      Some("WARNING") => "WARNING"
      Some("NOTE") => "NOTE"
      Some("DANGER") => "DANGER"
      _ => event.severity
    }
    let message = match configured_message {
      Some(template) => template.replace_all(old="{super}", new=event.message)
      None => event.message
    }
    result.push({ ..event, id: id + suffix, severity, message, })
  }
  result
}

///|
fn Model::trait_target_events(self : Model) -> Array[ValidationEvent] {
  let events : Array[ValidationEvent] = []
  for _, shape in self.shapes {
    if shape.source.file == "" || shape.id.namespace == "smithy.api" {
      continue
    }
    for trait_id, _ in shape.traits {
      guard self.shapes.get(trait_id) is Some(definition) else { continue }
      let trait_name = if trait_id.namespace == "smithy.api" {
        trait_id.name
      } else {
        trait_id.to_string()
      }
      if definition.trait_value("smithy.api#mixin") is Some(_) {
        events.push(
          validation_event(
            "TraitTarget",
            "ERROR",
            Some(shape),
            "Trait `" +
            trait_name +
            "` is a mixin and cannot be applied to `" +
            shape.id.to_string() +
            "`.",
          ),
        )
      }
      guard definition.trait_value("smithy.api#trait") is Some(config) &&
        validation_string(config, "selector") is Some(expression) &&
        expression != "*" else {
        continue
      }
      let selector = Selector::parse(expression) catch { _ => continue }
      if !selector.select_ids(self).contains(shape.id.to_string()) {
        events.push(
          validation_event(
            "TraitTarget",
            "ERROR",
            Some(shape),
            "Trait `" +
            trait_name +
            "` cannot be applied to `" +
            shape.id.to_string() +
            "`. This trait may only be applied to shapes that match the following selector: " +
            expression,
          ),
        )
      }
    }
  }
  events
}

///|
fn Model::trait_validator_events(self : Model) -> Array[ValidationEvent] {
  let events : Array[ValidationEvent] = []
  for trait_id, trait_shape in self.shapes {
    guard trait_shape.trait_value("smithy.api#traitValidators")
      is Some(Object(validators)) else {
      continue
    }
    let mut valid = true
    for event_id, _ in validators {
      if ShapeId::parse(event_id + "#Event") is None {
        valid = false
        events.push(
          validation_event(
            "TraitValidators",
            "ERROR",
            Some(trait_shape),
            "`smithy.api#traitValidators` key is not a valid event ID: `" +
            event_id +
            "`",
          ),
        )
      }
    }
    if !valid {
      continue
    }
    for _, applied in self.shapes {
      if applied.traits.get(trait_id) is None {
        continue
      }
      for event_id, definition in validators {
        guard validation_string(definition, "selector") is Some(expression) else {
          continue
        }
        let selector = Selector::parse(expression) catch { _ => continue }
        for shape_id in selector.select_ids_from(self, applied.id.to_string()) {
          guard self.get(shape_id) is Some(shape) else { continue }
          let suffix = match validation_string(definition, "message") {
            Some(message) => ": " + message
            None => ""
          }
          let severity = validation_string(definition, "severity").unwrap_or(
            "ERROR",
          )
          events.push(
            validation_event(
              event_id,
              severity,
              Some(shape),
              "Found an incompatible shape when validating the constraints of the `" +
              trait_id.to_string() +
              "` trait attached to `" +
              applied.id.to_string() +
              "`" +
              suffix,
            ),
          )
        }
      }
    }
  }
  events
}

///|
/// Run validators configured in model metadata and apply event decoration.
pub fn Model::validation_events(self : Model) -> Array[ValidationEvent] {
  let assembly = Assembly::new()
  assembly.validate(self, check_trait_definitions=false)
  self.collect_validation_events(assembly.diagnostics.map(diagnostic_event))
}

///|
fn validation_is_correctness(event : ValidationEvent) -> Bool {
  suppression_matches("Target", event.id) ||
  suppression_matches("ResourceCycle", event.id) ||
  suppression_matches("DeprecatedShape", event.id)
}

///|
fn validation_is_critical(event : ValidationEvent) -> Bool {
  [
    "EnumShape", "ExclusiveStructureMemberTrait", "TraitTarget", "TraitValue", "TraitConflict",
    "DefaultTrait", "ShapeIdConflict", "SingleOperationBinding", "SingleResourceBinding",
  ]
  .iter()
  .any(id => suppression_matches(id, event.id))
}

///|
fn validation_is_loading_event(event : ValidationEvent) -> Bool {
  event.id == "Model" ||
  event.id == "ModelDeprecation" ||
  event.id.has_prefix("UnknownValidator_") ||
  event.id.has_prefix("SM-")
}

///|
fn Model::collect_validation_events(
  self : Model,
  initial : Array[ValidationEvent],
) -> Array[ValidationEvent] {
  if initial.iter().any(e => e.severity == "ERROR" && e.id == "Model") {
    return initial
      .filter(validation_is_loading_event)
      .map(e => self.decorate_validation_event(e))
  }
  let events : Array[ValidationEvent] = []
  events.append(initial)
  events.append(self.validation_metadata_events())
  events.append(self.trait_target_events())
  events.append(self.trait_validator_events())
  events.append(self.constraint_trait_events())
  events.append(self.basic_trait_events())
  events.append(self.default_trait_events())
  events.append(self.trait_conflict_events())
  events.append(self.exclusive_trait_events())
  events.append(self.json_name_events())
  events.append(self.service_events())
  events.append(self.operation_events())
  events.append(self.resource_events())
  events.append(self.http_events())
  events.append(self.protocol_events())
  events.append(self.metadata_trait_events())
  events.append(self.semantic_trait_events())
  events.append(self.examples_events())
  events.append(self.recursion_and_format_events())
  for definition in validation_array(self.metadata.get("validators")) {
    events.append(self.configured_validation_events(definition))
  }
  let decorated = events
    .filter(event => {
      event.severity == "ERROR" ||
      event.shape.map(id => id.namespace != "smithy.api").unwrap_or(true)
    })
    .map(event => self.decorate_validation_event(event))
  if decorated.iter().any(e => e.severity == "ERROR" && e.id == "Model") {
    return decorated.filter(e => {
      validation_is_loading_event(e) || validation_is_correctness(e)
    })
  }
  if decorated
    .iter()
    .any(e => e.severity == "ERROR" && validation_is_correctness(e)) {
    return decorated.filter(e => {
      validation_is_correctness(e) || validation_is_loading_event(e)
    })
  }
  if decorated
    .iter()
    .any(e => e.severity == "ERROR" && validation_is_critical(e)) {
    return decorated.filter(e => {
      validation_is_correctness(e) ||
      validation_is_critical(e) ||
      validation_is_loading_event(e)
    })
  }
  decorated
}

///|
/// Include assembly findings even when no complete model could be returned.
pub fn LoadResult::validation_events(
  self : LoadResult,
) -> Array[ValidationEvent] {
  self.validation_findings
}

///|
fn diagnostic_event(d : ModelDiagnostic) -> ValidationEvent {
  {
    id: d.event_id,
    severity: if d.severity == Error {
      "ERROR"
    } else {
      "WARNING"
    },
    shape: d.shape,
    message: d.message,
    source: Some(d.source),
    suppression_reason: None,
  }
}