///| 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=[],
  )
}