///|
/// Defensive configuration limits for rule sets loaded from external JSON.
pub(all) struct RuleSetLimits {
  max_rules : Int
  max_name_length : Int
  max_message_length : Int
  max_total_source_length : Int
  require_unique_names : Bool
  compile_limits : CompileLimits
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleSetLimits::default() -> RuleSetLimits {
  {
    max_rules: 512,
    max_name_length: 128,
    max_message_length: 2048,
    max_total_source_length: 262144,
    require_unique_names: true,
    compile_limits: CompileLimits::default(),
  }
}

///|
fn validate_ruleset_limits(limits : RuleSetLimits) -> Result[Unit, Diagnostic] {
  if limits.max_rules <= 0 ||
    limits.max_name_length <= 0 ||
    limits.max_message_length <= 0 ||
    limits.max_total_source_length <= 0 {
    Err(
      Diagnostic::new(
        Configure,
        "C019",
        "rule-set limits must be positive integers",
        Span::new(0, 0),
        hint="Set every numeric RuleSetLimits field to at least 1.",
      ),
    )
  } else {
    validate_compile_limits(limits.compile_limits)
  }
}

///|
fn rule_configuration_diagnostic(
  code : String,
  rule_name : String,
  message : String,
  hint? : String,
) -> Diagnostic {
  Diagnostic::new(
    Configure,
    code,
    if rule_name.is_empty() {
      message
    } else {
      "rule `\{rule_name}`: \{message}"
    },
    Span::new(0, 0),
    hint?,
  )
}

///|
fn validate_rule_definition(
  definition : RuleDefinition,
  limits : RuleSetLimits,
  seen_names : Array[String],
) -> Array[Diagnostic] {
  let diagnostics = []
  if definition.name.is_empty() {
    diagnostics.push(
      rule_configuration_diagnostic(
        "C021",
        "",
        "rule name must not be empty",
        hint="Give each rule a stable, descriptive identifier.",
      ),
    )
  } else if definition.name.length() > limits.max_name_length {
    diagnostics.push(
      rule_configuration_diagnostic(
        "C022",
        definition.name,
        "rule name exceeds the configured length limit",
        hint="Shorten the identifier or explicitly raise max_name_length.",
      ),
    )
  }
  if definition.expression.is_empty() {
    diagnostics.push(
      rule_configuration_diagnostic(
        "C023",
        definition.name,
        "rule expression must not be empty",
        hint="Provide a boolean MoonRule expression.",
      ),
    )
  }
  if definition.message.length() > limits.max_message_length {
    diagnostics.push(
      rule_configuration_diagnostic(
        "C024",
        definition.name,
        "rule message exceeds the configured length limit",
        hint="Keep validation messages concise or raise max_message_length.",
      ),
    )
  }
  if limits.require_unique_names && seen_names.contains(definition.name) {
    diagnostics.push(
      rule_configuration_diagnostic(
        "C025",
        definition.name,
        "duplicate rule name",
        hint="Use unique names so reports and metrics can identify each rule.",
      ),
    )
  }
  diagnostics
}

///|
/// Compile a rule set with size, naming, and per-expression safety limits.
pub fn RuleSet::compile_with_limits(
  definitions : Array[RuleDefinition],
  limits : RuleSetLimits,
) -> Result[RuleSet, Array[Diagnostic]] {
  match validate_ruleset_limits(limits) {
    Err(diagnostic) => return Err([diagnostic])
    Ok(_) => ()
  }
  if definitions.length() > limits.max_rules {
    return Err([
      Diagnostic::new(
        Configure,
        "C020",
        "rule set contains \{definitions.length()} rules, exceeding the configured limit of \{limits.max_rules}",
        Span::new(0, 0),
        hint="Split the configuration or explicitly raise max_rules.",
      ),
    ])
  }
  let mut total_source_length = 0
  for definition in definitions {
    total_source_length = total_source_length + definition.expression.length()
  }
  if total_source_length > limits.max_total_source_length {
    return Err([
      Diagnostic::new(
        Configure,
        "C026",
        "combined rule source exceeds the configured length limit",
        Span::new(0, 0),
        hint="Split the rule set or explicitly raise max_total_source_length.",
      ),
    ])
  }
  let rules = []
  let diagnostics = []
  let seen_names = []
  for definition in definitions {
    let definition_diagnostics = validate_rule_definition(
      definition, limits, seen_names,
    )
    for diagnostic in definition_diagnostics {
      diagnostics.push(diagnostic)
    }
    if !definition.name.is_empty() {
      seen_names.push(definition.name)
    }
    if !definition.expression.is_empty() {
      match compile_with_limits(definition.expression, limits.compile_limits) {
        Ok(program) => rules.push({ definition, program })
        Err(diagnostic) =>
          diagnostics.push({
            ..diagnostic,
            message: "rule `\{definition.name}`: \{diagnostic.message}",
          })
      }
    }
  }
  if diagnostics.is_empty() {
    Ok({ rules, })
  } else {
    Err(diagnostics)
  }
}

///|
/// Decode and compile rule definitions using explicit configuration limits.
pub fn RuleSet::from_json_with_limits(
  config : Json,
  limits : RuleSetLimits,
) -> Result[RuleSet, Array[Diagnostic]] {
  let definitions : Array[RuleDefinition] = @json.from_json(config) catch {
    error =>
      return Err([
        Diagnostic::new(
          Configure,
          "C001",
          "invalid rule configuration: \{error}",
          Span::new(0, 0),
          hint="Expected a JSON array containing name, expression, message, and severity.",
        ),
      ])
  }
  RuleSet::compile_with_limits(definitions, limits)
}