///|
/// CI-oriented limits applied to the result of static program analysis.
pub(all) struct AnalysisPolicy {
  max_ast_nodes : Int
  max_ast_depth : Int
  max_estimated_cost : Int
  reject_warnings : Bool
  allow_dynamic_regex : Bool
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn AnalysisPolicy::default() -> AnalysisPolicy {
  {
    max_ast_nodes: 2048,
    max_ast_depth: 64,
    max_estimated_cost: 10000,
    reject_warnings: true,
    allow_dynamic_regex: false,
  }
}

///|
/// Static analysis plus the policy violations that determine CI acceptance.
pub(all) struct AnalysisGateReport {
  passed : Bool
  analysis : ProgramAnalysis
  violations : Array[AnalysisFinding]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn AnalysisGateReport::to_json_string(
  self : AnalysisGateReport,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}

///|
fn validate_analysis_policy(
  policy : AnalysisPolicy,
) -> Result[Unit, Diagnostic] {
  if policy.max_ast_nodes <= 0 ||
    policy.max_ast_depth <= 0 ||
    policy.max_estimated_cost <= 0 {
    Err(
      Diagnostic::new(
        Configure,
        "C030",
        "analysis policy limits must be positive integers",
        Span::new(0, 0),
        hint="Set every numeric AnalysisPolicy field to at least 1.",
      ),
    )
  } else {
    Ok(())
  }
}

///|
fn gate_finding(
  code : String,
  message : String,
  span : Span,
  hint : String,
) -> AnalysisFinding {
  { level: Warning, code, message, span, hint: Some(hint) }
}

///|
/// Analyze one program and decide whether it satisfies a configured CI gate.
pub fn analyze_with_policy(
  program : Program,
  policy : AnalysisPolicy,
) -> Result[AnalysisGateReport, Diagnostic] {
  match validate_analysis_policy(policy) {
    Err(diagnostic) => return Err(diagnostic)
    Ok(_) => ()
  }
  let analysis = analyze(program)
  let violations = []
  if analysis.node_count > policy.max_ast_nodes {
    violations.push(
      gate_finding(
        "A050",
        "AST node count \{analysis.node_count} exceeds policy limit \{policy.max_ast_nodes}",
        program.span(),
        "Simplify the expression or explicitly raise max_ast_nodes.",
      ),
    )
  }
  if analysis.ast_depth > policy.max_ast_depth {
    violations.push(
      gate_finding(
        "A051",
        "AST depth \{analysis.ast_depth} exceeds policy limit \{policy.max_ast_depth}",
        program.span(),
        "Reduce nesting or explicitly raise max_ast_depth.",
      ),
    )
  }
  if analysis.estimated_cost > policy.max_estimated_cost {
    violations.push(
      gate_finding(
        "A052",
        "estimated cost \{analysis.estimated_cost} exceeds policy limit \{policy.max_estimated_cost}",
        program.span(),
        "Split the rule or explicitly raise max_estimated_cost after benchmarking.",
      ),
    )
  }
  for finding in analysis.findings {
    if policy.reject_warnings && finding.level == Warning {
      violations.push(finding)
    } else if !policy.allow_dynamic_regex && finding.code == "A005" {
      violations.push({
        ..finding,
        level: Warning,
        code: "A053",
        message: "dynamic regular expression is not allowed by analysis policy",
        hint: Some(
          "Use a literal pattern or explicitly allow dynamic regular expressions.",
        ),
      })
    }
  }
  Ok({ passed: violations.is_empty(), analysis, violations })
}

///|
/// Gate result associated with one named rule.
pub(all) struct RuleAnalysisGate {
  name : String
  severity : Severity
  report : AnalysisGateReport
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Aggregate CI gate result for a compiled rule set.
pub(all) struct RuleSetAnalysisGateReport {
  passed : Bool
  rule_count : Int
  failed_rule_count : Int
  rules : Array[RuleAnalysisGate]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn RuleSetAnalysisGateReport::to_json_string(
  self : RuleSetAnalysisGateReport,
  indent? : Int = 2,
) -> String {
  self.to_json().stringify(indent~)
}

///|
/// Apply one static-analysis policy to every rule in a compiled rule set.
pub fn RuleSet::analyze_with_policy(
  self : RuleSet,
  policy : AnalysisPolicy,
) -> Result[RuleSetAnalysisGateReport, Diagnostic] {
  match validate_analysis_policy(policy) {
    Err(diagnostic) => return Err(diagnostic)
    Ok(_) => ()
  }
  let rules = []
  let mut failed_rule_count = 0
  for rule in self.rules {
    let report = analyze_with_policy(rule.program, policy).unwrap()
    if !report.passed {
      failed_rule_count = failed_rule_count + 1
    }
    rules.push({
      name: rule.definition.name,
      severity: rule.definition.severity,
      report,
    })
  }
  Ok({
    passed: failed_rule_count == 0,
    rule_count: rules.length(),
    failed_rule_count,
    rules,
  })
}