///|
pub(all) struct Policy {
  options : Options
  overrides : Map[String, Severity]
  disabled : Array[String]
} derive(Debug)

///|
pub fn default_policy() -> Policy {
  { options: default_options(), overrides: Map([]), disabled: [], }
}

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

///|
fn json_string(value : Json) -> String? {
  match value {
    String(text) => Some(text)
    _ => None
  }
}

///|
fn json_integer(value : Json) -> Int32? {
  match value {
    Number(number, ..) => {
      if number < -2147483648.0 || number > 2147483647.0 {
        return None
      }
      let integer = number.to_int()
      if integer.to_double() == number {
        Some(integer)
      } else {
        None
      }
    }
    _ => None
  }
}

///|
fn json_bool(value : Json) -> Bool? {
  match value {
    True => Some(true)
    False => Some(false)
    _ => None
  }
}

///|
pub fn parse_severity(value : String) -> Severity? {
  match value {
    "error" => Some(Error)
    "warning" => Some(Warning)
    "info" => Some(Info)
    _ => None
  }
}

///|
pub fn options_from_json(
  base : Options,
  value : Json,
) -> Result[Options, String] {
  let fields = match value {
    Object(fields) => fields
    _ => return Err("options must be a JSON object")
  }
  let mut options = base
  for name, value in fields {
    match name {
      "strict" =>
        match json_bool(value) {
          Some(strict) => options = { ..options, strict, }
          None => return Err("strict must be a boolean")
        }
      "empty_severity" | "fuzzy_severity" => {
        let severity = match json_string(value).bind(parse_severity) {
          Some(severity) => severity
          None => return Err(name + " must be error, warning, or info")
        }
        options = if name == "empty_severity" {
          { ..options, empty_severity: severity, }
        } else {
          { ..options, fuzzy_severity: severity, }
        }
      }
      "plural_max"
      | "expression_depth"
      | "expression_budget"
      | "max_entries"
      | "max_line_chars" => {
        let integer = match json_integer(value) {
          Some(integer) => integer
          None => return Err(name + " must be a signed 32-bit integer")
        }
        options = match name {
          "plural_max" => { ..options, plural_max: integer, }
          "expression_depth" => { ..options, expression_depth: integer, }
          "expression_budget" => { ..options, expression_budget: integer, }
          "max_entries" => { ..options, max_entries: integer, }
          _ => { ..options, max_line_chars: integer, }
        }
      }
      _ => return Err("Unknown option: " + name)
    }
  }
  let errors = validate_options(options)
  if errors.is_empty() {
    Ok(options)
  } else {
    Err(join_strings(errors, "; "))
  }
}

///|
fn configurable_rule(code : String) -> Bool {
  [
    "H001", "H006", "H007", "H008", "H009", "C006", "C007", "C011", "C013", "L005",
    "T002", "F007", "X001", "X002", "X004", "X005",
  ].contains(code)
}

///|
/// Only review-policy findings can be disabled or reclassified.
pub fn parse_policy(text : String) -> Result[Policy, String] {
  let json = match bounded_json(text, 16) {
    Ok(value) => value
    Err(error) => return Err("Invalid configuration JSON: " + error)
  }
  let fields = match json {
    Object(fields) => fields
    _ => return Err("Configuration must be an object")
  }
  let mut policy = default_policy()
  for name, value in fields {
    match name {
      "schema_version" =>
        if json_integer(value) != Some(1) {
          return Err("Configuration schema_version must be 1")
        }
      "options" =>
        match options_from_json(policy.options, value) {
          Ok(options) => policy = { ..policy, options, }
          Err(error) => return Err(error)
        }
      "rules" => {
        let rules = match value {
          Object(rules) => rules
          _ =>
            return Err("rules must be an object of diagnostic code to severity")
        }
        for code, setting in rules {
          if !configurable_rule(code) {
            return Err("Rule is unknown or cannot be reclassified: " + code)
          }
          let setting = match json_string(setting) {
            Some(setting) => setting
            None => return Err("Rule setting must be a string: " + code)
          }
          if setting == "off" {
            policy.disabled.push(code)
          } else {
            match parse_severity(setting) {
              Some(severity) => policy.overrides.set(code, severity)
              None =>
                return Err(
                  "Rule setting must be error, warning, info, or off: " + code,
                )
            }
          }
        }
      }
      _ => return Err("Unknown configuration field: " + name)
    }
  }
  Ok(policy)
}

///|
pub fn apply_policy(report : Report, policy : Policy) -> Report {
  let diagnostics : Array[Diagnostic] = []
  for diagnostic in report.diagnostics {
    if policy.disabled.contains(diagnostic.code) &&
      configurable_rule(diagnostic.code) {
      continue
    }
    if configurable_rule(diagnostic.code) &&
      policy.overrides.get(diagnostic.code) is Some(severity) {
      diagnostics.push({ ..diagnostic, severity, })
    } else {
      diagnostics.push(diagnostic)
    }
  }
  report_with(
    report.catalog,
    diagnostics,
    policy.options,
    report.plural_checked_through,
  )
}