///|
/// Importance of a static-analysis finding.
pub(all) enum AnalysisLevel {
  Note
  Warning
} derive(Eq, Debug, ToJson, FromJson)

///|
/// One actionable observation produced without evaluating input data.
pub(all) struct AnalysisFinding {
  level : AnalysisLevel
  code : String
  message : String
  span : Span
  hint : String?
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Counts for the expression forms contained in a compiled program.
pub(all) struct ExpressionStatistics {
  literals : Int
  variables : Int
  arrays : Int
  members : Int
  indexes : Int
  optional_members : Int
  optional_indexes : Int
  calls : Int
  unary_operations : Int
  binary_operations : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
/// Static description of a compiled MoonRule program.
pub(all) struct ProgramAnalysis {
  source_length : Int
  node_count : Int
  ast_depth : Int
  estimated_cost : Int
  statistics : ExpressionStatistics
  referenced_paths : Array[String]
  called_functions : Array[String]
  findings : Array[AnalysisFinding]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn ProgramAnalysis::warning_count(self : ProgramAnalysis) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.level == Warning {
      count = count + 1
    }
  }
  count
}

///|
pub fn ProgramAnalysis::note_count(self : ProgramAnalysis) -> Int {
  let mut count = 0
  for finding in self.findings {
    if finding.level == Note {
      count = count + 1
    }
  }
  count
}

///|
/// Return true when static analysis found at least one warning.
pub fn ProgramAnalysis::has_warnings(self : ProgramAnalysis) -> Bool {
  self.warning_count() > 0
}

///|
/// Return true when analysis produced no notes or warnings.
pub fn ProgramAnalysis::is_clean(self : ProgramAnalysis) -> Bool {
  self.findings.is_empty()
}

///|
/// Test whether the program calls a named built-in or unresolved function.
pub fn ProgramAnalysis::uses_function(
  self : ProgramAnalysis,
  name : String,
) -> Bool {
  self.called_functions.contains(name)
}

///|
/// Test whether the exact normalized JSON path is referenced.
pub fn ProgramAnalysis::references_path(
  self : ProgramAnalysis,
  path : String,
) -> Bool {
  self.referenced_paths.contains(path)
}

///|
/// Select findings of one importance level while preserving source order.
pub fn ProgramAnalysis::findings_at(
  self : ProgramAnalysis,
  level : AnalysisLevel,
) -> Array[AnalysisFinding] {
  let selected = []
  for finding in self.findings {
    if finding.level == level {
      selected.push(finding)
    }
  }
  selected
}

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

///|
priv struct AnalysisBuilder {
  mut literals : Int
  mut variables : Int
  mut arrays : Int
  mut members : Int
  mut indexes : Int
  mut optional_members : Int
  mut optional_indexes : Int
  mut calls : Int
  mut unary_operations : Int
  mut binary_operations : Int
  mut estimated_cost : Int
  paths : Array[String]
  functions : Array[String]
  findings : Array[AnalysisFinding]
}

///|
fn AnalysisBuilder::new() -> AnalysisBuilder {
  {
    literals: 0,
    variables: 0,
    arrays: 0,
    members: 0,
    indexes: 0,
    optional_members: 0,
    optional_indexes: 0,
    calls: 0,
    unary_operations: 0,
    binary_operations: 0,
    estimated_cost: 0,
    paths: [],
    functions: [],
    findings: [],
  }
}

///|
fn AnalysisBuilder::add_path(self : AnalysisBuilder, path : String) -> Unit {
  if !self.paths.contains(path) {
    self.paths.push(path)
  }
}

///|
fn AnalysisBuilder::add_function(self : AnalysisBuilder, name : String) -> Unit {
  if !self.functions.contains(name) {
    self.functions.push(name)
  }
}

///|
fn AnalysisBuilder::finding(
  self : AnalysisBuilder,
  level : AnalysisLevel,
  code : String,
  message : String,
  span : Span,
  hint? : String,
) -> Unit {
  self.findings.push({ level, code, message, span, hint })
}

///|
fn static_bool(expression : Expr) -> Bool? {
  match expression {
    Literal(True, _) => Some(true)
    Literal(False, _) => Some(false)
    Unary(Not, operand, _) =>
      match static_bool(operand) {
        Some(value) => Some(!value)
        None => None
      }
    _ => None
  }
}

///|
fn static_number(expression : Expr) -> Double? {
  match expression {
    Literal(Number(value, ..), _) => Some(value)
    Unary(Negate, operand, _) =>
      match static_number(operand) {
        Some(value) => Some(-value)
        None => None
      }
    _ => None
  }
}

///|
fn expression_path(expression : Expr) -> String? {
  match expression {
    Variable(name, _) => Some(name)
    Member(target, name, _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "." + name)
        None => None
      }
    OptionalMember(target, name, _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "?." + name)
        None => None
      }
    Index(target, Literal(String(key), _), _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "[\"" + key + "\"]")
        None => None
      }
    Index(target, Literal(Number(number, ..), _), _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "[" + number.to_string() + "]")
        None => None
      }
    OptionalIndex(target, Literal(String(key), _), _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "?[\"" + key + "\"]")
        None => None
      }
    OptionalIndex(target, Literal(Number(number, ..), _), _) =>
      match expression_path(target) {
        Some(prefix) => Some(prefix + "?[" + number.to_string() + "]")
        None => None
      }
    _ => None
  }
}

///|
fn analyze_call(
  builder : AnalysisBuilder,
  name : String,
  arguments : Array[Expr],
  span : Span,
) -> Unit {
  builder.calls = builder.calls + 1
  builder.estimated_cost = builder.estimated_cost + 4
  builder.add_function(name)
  match find_builtin_function(name) {
    None =>
      builder.finding(
        Warning,
        "A001",
        "unknown function `\{name}`",
        span,
        hint="Use builtin_functions() to inspect the supported function catalog.",
      )
    Some(specification) =>
      if !specification.accepts_arity(arguments.length()) {
        builder.finding(
          Warning,
          "A002",
          "function `\{name}` accepts \{specification.arity_description()} argument(s), received \{arguments.length()}",
          span,
          hint="Adjust the call to match \{specification.signature}.",
        )
      }
  }
  if name == "matches" {
    match arguments.get(1) {
      Some(Literal(String(pattern), pattern_span)) => {
        let valid = try {
          @string.Regex::Regex(pattern) |> ignore
          true
        } catch {
          _ => false
        }
        if !valid {
          builder.finding(
            Warning,
            "A003",
            "regular-expression literal is invalid",
            pattern_span,
            hint="Correct the pattern before the rule is evaluated.",
          )
        }
        if pattern.length() > 256 {
          builder.finding(
            Warning,
            "A004",
            "regular-expression literal is unusually long",
            pattern_span,
            hint="Prefer a focused expression and review it for excessive backtracking.",
          )
        }
      }
      Some(_) =>
        builder.finding(
          Note,
          "A005",
          "dynamic regular expression cannot be validated during compilation",
          span,
          hint="Use a literal pattern when early validation is important.",
        )
      None => ()
    }
  }
  match arguments.get(0) {
    Some(Literal(String(text), literal_span)) =>
      match validate_literal_for_function(name, text) {
        Some(false) =>
          builder.finding(
            Warning,
            "A040",
            "literal value can never satisfy `\{name}`",
            literal_span,
            hint="Correct the literal or remove the validation that is known to fail.",
          )
        _ => ()
      }
    _ => ()
  }
}

///|
fn analyze_binary(
  builder : AnalysisBuilder,
  left : Expr,
  operator : BinaryOp,
  right : Expr,
  span : Span,
) -> Unit {
  builder.binary_operations = builder.binary_operations + 1
  builder.estimated_cost = builder.estimated_cost + 2
  match operator {
    Or =>
      match static_bool(left) {
        Some(true) =>
          builder.finding(
            Warning,
            "A010",
            "right side of `||` is unreachable",
            right.span(),
            hint="Remove the unreachable expression or replace the constant condition.",
          )
        Some(false) =>
          builder.finding(
            Note,
            "A011",
            "left side of `||` is always false",
            left.span(),
            hint="The expression can be simplified to its right side.",
          )
        None => ()
      }
    And =>
      match static_bool(left) {
        Some(false) =>
          builder.finding(
            Warning,
            "A012",
            "right side of `&&` is unreachable",
            right.span(),
            hint="Remove the unreachable expression or replace the constant condition.",
          )
        Some(true) =>
          builder.finding(
            Note,
            "A013",
            "left side of `&&` is always true",
            left.span(),
            hint="The expression can be simplified to its right side.",
          )
        None => ()
      }
    Divide | Remainder =>
      match static_number(right) {
        Some(0.0) =>
          builder.finding(
            Warning,
            "A014",
            "numeric operation always divides by zero",
            right.span(),
            hint="Use a non-zero divisor or guard the operation.",
          )
        _ => ()
      }
    Equal | NotEqual =>
      if left.span().start == right.span().start &&
        left.span().end == right.span().end {
        builder.finding(
          Note,
          "A015",
          "both sides of the comparison refer to the same source range",
          span,
        )
      }
    _ => ()
  }
}

///|
fn analyze_expression(builder : AnalysisBuilder, expression : Expr) -> Unit {
  builder.estimated_cost = builder.estimated_cost + 1
  match expression {
    Literal(_, _) => builder.literals = builder.literals + 1
    Variable(name, _) => {
      builder.variables = builder.variables + 1
      builder.add_path(name)
    }
    ArrayLiteral(items, span) => {
      builder.arrays = builder.arrays + 1
      builder.estimated_cost = builder.estimated_cost + items.length()
      if items.length() > 128 {
        builder.finding(
          Note,
          "A020",
          "large array literal contains \{items.length()} items",
          span,
          hint="Move large reference data into the input context when it changes independently.",
        )
      }
      for item in items {
        analyze_expression(builder, item)
      }
    }
    Member(target, _, _) => {
      builder.members = builder.members + 1
      match expression_path(expression) {
        Some(path) => builder.add_path(path)
        None => ()
      }
      analyze_expression(builder, target)
    }
    Index(target, index, _) => {
      builder.indexes = builder.indexes + 1
      builder.estimated_cost = builder.estimated_cost + 2
      match expression_path(expression) {
        Some(path) => builder.add_path(path)
        None => ()
      }
      analyze_expression(builder, target)
      analyze_expression(builder, index)
    }
    OptionalMember(target, _, _) => {
      builder.members = builder.members + 1
      builder.optional_members = builder.optional_members + 1
      match expression_path(expression) {
        Some(path) => builder.add_path(path)
        None => ()
      }
      analyze_expression(builder, target)
    }
    OptionalIndex(target, index, _) => {
      builder.indexes = builder.indexes + 1
      builder.optional_indexes = builder.optional_indexes + 1
      builder.estimated_cost = builder.estimated_cost + 2
      match expression_path(expression) {
        Some(path) => builder.add_path(path)
        None => ()
      }
      analyze_expression(builder, target)
      analyze_expression(builder, index)
    }
    Call(name, arguments, span) => {
      analyze_call(builder, name, arguments, span)
      for argument in arguments {
        analyze_expression(builder, argument)
      }
    }
    Unary(_, operand, _) => {
      builder.unary_operations = builder.unary_operations + 1
      analyze_expression(builder, operand)
    }
    Binary(left, operator, right, span) => {
      analyze_binary(builder, left, operator, right, span)
      analyze_expression(builder, left)
      analyze_expression(builder, right)
    }
  }
}

///|
/// Analyze a compiled program without executing it against user data.
///
/// Findings are advisory: compile-time analysis never changes evaluation
/// semantics and can therefore be introduced safely in CI and editors.
pub fn analyze(program : Program) -> ProgramAnalysis {
  let builder = AnalysisBuilder::new()
  analyze_expression(builder, program.root)
  if program.ast_depth() > 32 {
    builder.finding(
      Note,
      "A030",
      "expression nesting depth is \{program.ast_depth()}",
      program.span(),
      hint="Consider extracting intermediate fields or splitting the policy into named rules.",
    )
  }
  if builder.estimated_cost > 500 {
    builder.finding(
      Note,
      "A031",
      "estimated evaluation cost is \{builder.estimated_cost}",
      program.span(),
      hint="Benchmark this expression with representative input.",
    )
  }
  {
    source_length: program.source().length(),
    node_count: program.node_count(),
    ast_depth: program.ast_depth(),
    estimated_cost: builder.estimated_cost,
    statistics: {
      literals: builder.literals,
      variables: builder.variables,
      arrays: builder.arrays,
      members: builder.members,
      indexes: builder.indexes,
      optional_members: builder.optional_members,
      optional_indexes: builder.optional_indexes,
      calls: builder.calls,
      unary_operations: builder.unary_operations,
      binary_operations: builder.binary_operations,
    },
    referenced_paths: builder.paths,
    called_functions: builder.functions,
    findings: builder.findings,
  }
}