///| 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 {
  if view.get_path(path) == None {
    diagnostics.push(error("required", "missing required configuration key '" + path + "'", hint=Some("add the key to a base or environment-specific 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)
}