///|
/// A named audit policy for a class of deployments.
pub struct AuditPolicy {
  name : String
  rules : Array[Rule]
  expected_keys : Array[String]
  forbidden_keys : Array[String]
  sensitive_keys : Array[String]
} derive(@debug.Debug, Eq)

///|
/// A compact health summary for a configuration view.
pub struct AuditSummary {
  policy_name : String
  key_count : Int
  error_count : Int
  warning_count : Int
  info_count : Int
  score : Int
  diagnostics : Array[Diagnostic]
} derive(@debug.Debug, Eq)

///|
pub fn audit_policy(
  name : String,
  rules : Array[Rule],
  expected_keys? : Array[String] = [],
  forbidden_keys? : Array[String] = [],
  sensitive_keys? : Array[String] = [],
) -> AuditPolicy {
  { name, rules, expected_keys, forbidden_keys, sensitive_keys }
}

///|
/// Runs a policy and computes a coarse 0-100 health score.
pub fn audit(view : ConfigView, policy : AuditPolicy) -> AuditSummary {
  let diagnostics : Array[Diagnostic] = []
  append_diagnostics(diagnostics, validate(view, policy.rules))
  append_diagnostics(
    diagnostics,
    audit_expected_keys(view, policy.expected_keys),
  )
  append_diagnostics(
    diagnostics,
    audit_forbidden_keys(view, policy.forbidden_keys),
  )
  append_diagnostics(
    diagnostics,
    audit_sensitive_keys(view, policy.sensitive_keys),
  )
  append_diagnostics(diagnostics, audit_parse_diagnostics(view))
  let errors = count_severity(diagnostics, Error)
  let warnings = count_severity(diagnostics, Warning)
  let infos = count_severity(diagnostics, Info)
  let score = clamp_score(100 - errors * 25 - warnings * 8 - infos * 1)
  {
    policy_name: policy.name,
    key_count: view.values.length(),
    error_count: errors,
    warning_count: warnings,
    info_count: infos,
    score,
    diagnostics,
  }
}

///|
fn audit_expected_keys(
  view : ConfigView,
  keys : Array[String],
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for path in keys {
    if view.get_path(path) == None {
      diagnostics.push(
        warning("expected-key", "expected key '" + path + "' is absent"),
      )
    }
  }
  diagnostics
}

///|
fn audit_forbidden_keys(
  view : ConfigView,
  keys : Array[String],
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for path in keys {
    match view.get_path(path) {
      None => ()
      Some(_) =>
        diagnostics.push(
          error("forbidden-key", "forbidden key '" + path + "' is present"),
        )
    }
  }
  diagnostics
}

///|
fn audit_sensitive_keys(
  view : ConfigView,
  keys : Array[String],
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for path in keys {
    match view.get_path(path) {
      None => ()
      Some(value) =>
        if value == "" {
          diagnostics.push(
            error("secret-empty", "sensitive key '" + path + "' is empty"),
          )
        } else if value.length() < 12 {
          diagnostics.push(
            warning(
              "secret-weak",
              "sensitive key '" + path + "' is short for production use",
            ),
          )
        }
    }
  }
  diagnostics
}

///|
fn audit_parse_diagnostics(view : ConfigView) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  for item in view.diagnostics {
    diagnostics.push(item)
  }
  diagnostics
}

///|
fn count_severity(diagnostics : Array[Diagnostic], severity : Severity) -> Int {
  let mut count = 0
  for item in diagnostics {
    if item.severity == severity {
      count = count + 1
    }
  }
  count
}

///|
fn clamp_score(score : Int) -> Int {
  if score < 0 {
    0
  } else if score > 100 {
    100
  } else {
    score
  }
}

///|
/// Renders the audit summary as Markdown.
pub fn render_audit_summary(summary : AuditSummary) -> String {
  let builder = StringBuilder()
  builder.write_string("# Audit Summary: " + summary.policy_name + "\n\n")
  builder.write_string("- Score: " + summary.score.to_string() + "/100\n")
  builder.write_string("- Keys: " + summary.key_count.to_string() + "\n")
  builder.write_string("- Errors: " + summary.error_count.to_string() + "\n")
  builder.write_string(
    "- Warnings: " + summary.warning_count.to_string() + "\n",
  )
  builder.write_string("- Info: " + summary.info_count.to_string() + "\n\n")
  if summary.diagnostics.length() == 0 {
    builder.write_string("No audit diagnostics.\n")
  } else {
    builder.write_string("## Findings\n\n")
    for diagnostic in summary.diagnostics {
      builder.write_string(
        "- " +
        audit_severity_label(diagnostic.severity) +
        " `" +
        diagnostic.code +
        "` " +
        diagnostic.message +
        "\n",
      )
    }
  }
  builder.to_string()
}

///|
fn audit_severity_label(severity : Severity) -> String {
  match severity {
    Error => "ERROR"
    Warning => "WARN"
    Info => "INFO"
  }
}

///|
/// A strict policy suitable for production services.
pub fn production_service_policy() -> AuditPolicy {
  audit_policy(
    "production-service",
    [
      required("server.host"),
      required("server.port"),
      required("database.url"),
      enum_value("server.mode", ["production"]),
      int_range("server.port", Some(1), Some(65535)),
      int_range("server.workers", Some(1), Some(256)),
      int_range("database.pool_min", Some(0), Some(1024)),
      int_range("database.pool_max", Some(1), Some(4096)),
      secret_like("database.password"),
      secret_like("auth.token"),
      conflicts("auth.oauth", "auth.basic"),
    ],
    expected_keys=[
      "server.request_timeout_ms", "database.pool_max", "auth.enabled",
    ],
    forbidden_keys=[
      "debug", "server.debug", "auth.legacy", "database.plaintext_password",
    ],
    sensitive_keys=["database.password", "auth.token", "auth.client_secret"],
  )
}

///|
/// A relaxed policy for local development.
pub fn development_service_policy() -> AuditPolicy {
  audit_policy(
    "development-service",
    [
      required("server.host"),
      required("server.port"),
      enum_value("server.mode", ["development", "test", "production"]),
      int_range("server.port", Some(1), Some(65535)),
    ],
    expected_keys=["server.mode", "database.url"],
    forbidden_keys=[],
    sensitive_keys=["auth.token"],
  )
}

///|
/// A policy for CI test fixture configs.
pub fn ci_fixture_policy() -> AuditPolicy {
  audit_policy(
    "ci-fixture",
    [
      required("fixture.root"),
      int_range("test.retries", Some(0), Some(10)),
      int_range("fixture.seed", Some(0), None),
      enum_value("output.format", ["text", "json", "markdown"]),
    ],
    expected_keys=["test.pattern", "fixture.reset"],
    forbidden_keys=["database.password", "auth.token"],
    sensitive_keys=[],
  )
}