///|
/// A validation rule for one configuration path.
pub enum Rule {
  Required(String)
  EnumValue(String, Array[String])
  IntRange(String, Int?, Int?)
  Deprecated(String, String)
  Requires(String, String)
  Conflicts(String, String)
  SecretLike(String)
} derive(@debug.Debug, Eq)

///|
pub fn required(path : String) -> Rule {
  Required(path)
}

///|
pub fn enum_value(path : String, allowed : Array[String]) -> Rule {
  EnumValue(path, allowed)
}

///|
pub fn int_range(path : String, min : Int?, max : Int?) -> Rule {
  IntRange(path, min, max)
}

///|
pub fn deprecated(path : String, replacement : String) -> Rule {
  Deprecated(path, replacement)
}

///|
pub fn requires(path : String, required_path : String) -> Rule {
  Requires(path, required_path)
}

///|
pub fn conflicts(path : String, other_path : String) -> Rule {
  Conflicts(path, other_path)
}

///|
pub fn secret_like(path : String) -> Rule {
  SecretLike(path)
}

///|
/// Applies validation rules to an already-merged view.
pub fn validate(view : ConfigView, rules : Array[Rule]) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for rule in rules {
    match rule {
      Required(path) => validate_required(view, diagnostics, path)
      EnumValue(path, allowed) =>
        validate_enum(view, diagnostics, path, allowed)
      IntRange(path, min, max) =>
        validate_int_range(view, diagnostics, path, min, max)
      Deprecated(path, replacement) =>
        validate_deprecated(view, diagnostics, path, replacement)
      Requires(path, required_path) =>
        validate_requires(view, diagnostics, path, required_path)
      Conflicts(path, other_path) =>
        validate_conflicts(view, diagnostics, path, other_path)
      SecretLike(path) => validate_secret_like(view, diagnostics, path)
    }
  }
  diagnostics
}

///|
fn validate_required(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
) -> Unit {
  match view.get_path(path) {
    None =>
      diagnostics.push(
        error(
          "required",
          "missing required configuration key '" + path + "'",
          hint=Some(
            "add a non-empty value to a base or environment-specific config layer",
          ),
        ),
      )
    Some(value) =>
      if value.trim() == "" {
        diagnostics.push(
          error(
            "required",
            "required configuration key '" + path + "' is empty",
            hint=Some("set a non-empty value in the effective config layer"),
          ),
        )
      }
  }
}

///|
fn validate_enum(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
  allowed : Array[String],
) -> Unit {
  match view.get_path(path) {
    None => ()
    Some(value) =>
      if !contains_string_value(allowed, value) {
        diagnostics.push(
          error(
            "enum",
            "key '" + path + "' has unsupported value '" + value + "'",
            hint=Some("allowed values: " + allowed.join(", ")),
          ),
        )
      }
  }
}

///|
fn validate_int_range(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
  min : Int?,
  max : Int?,
) -> Unit {
  match view.get_path(path) {
    None => ()
    Some(raw) => {
      match view.get_int(path) {
        None =>
          diagnostics.push(
            error("int", "key '" + path + "' must be an integer"),
          )
        Some(value) => {
          match min {
            Some(min_value) =>
              if value < min_value {
                diagnostics.push(
                  error(
                    "range",
                    "key '" +
                    path +
                    "' is below minimum " +
                    min_value.to_string(),
                  ),
                )
              }
            None => ()
          }
          match max {
            Some(max_value) =>
              if value > max_value {
                diagnostics.push(
                  error(
                    "range",
                    "key '" +
                    path +
                    "' is above maximum " +
                    max_value.to_string(),
                  ),
                )
              }
            None => ()
          }
        }
      }
      ignore(raw)
    }
  }
}

///|
fn validate_deprecated(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
  replacement : String,
) -> Unit {
  if view.get_path(path) != None {
    diagnostics.push(
      warning(
        "deprecated",
        "key '" + path + "' is deprecated",
        hint=Some("use '" + replacement + "' instead"),
      ),
    )
  }
}

///|
fn validate_requires(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
  required_path : String,
) -> Unit {
  if view.get_path(path) != None && view.get_path(required_path) == None {
    diagnostics.push(
      error("requires", "key '" + path + "' requires '" + required_path + "'"),
    )
  }
}

///|
fn validate_conflicts(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
  other_path : String,
) -> Unit {
  if view.get_path(path) != None && view.get_path(other_path) != None {
    diagnostics.push(
      error(
        "conflicts",
        "key '" + path + "' conflicts with '" + other_path + "'",
      ),
    )
  }
}

///|
fn validate_secret_like(
  view : ConfigView,
  diagnostics : Array[Diagnostic],
  path : String,
) -> Unit {
  match view.get_path(path) {
    None => ()
    Some(value) =>
      if value.length() == 0 {
        diagnostics.push(
          error("secret-empty", "secret-like key '" + path + "' is empty"),
        )
      } else if value.length() < 8 {
        diagnostics.push(
          warning(
            "secret-short",
            "secret-like key '" + path + "' looks too short",
          ),
        )
      }
  }
}

///|
fn contains_string_value(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}

///|
/// Returns true if any diagnostic is an error.
pub fn has_errors(diagnostics : Array[Diagnostic]) -> Bool {
  !diagnostics_ok(diagnostics)
}