///|
/// Core data model for explainable text audits.
pub(all) enum Severity {
  Info
  Warning
  Error
} derive(Eq, @debug.Debug)

///|
/// Stable lowercase label used by reports and external tools.
pub fn Severity::label(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    Error => "error"
  }
}

///|
/// A small ordinal used when callers need to sort or gate findings.
pub fn Severity::rank(self : Severity) -> Int {
  match self {
    Info => 1
    Warning => 2
    Error => 3
  }
}

///|
/// Token category emitted by `tokenize_lines`.
pub(all) enum LineKind {
  Blank
  Heading
  Bullet
  CodeFence
  Paragraph
} derive(Eq, @debug.Debug)

///|
/// A line-level token that keeps the source line number.
pub(all) struct LineToken {
  line : Int
  kind : LineKind
  text : String
} derive(Eq, @debug.Debug)

///|
/// Rule strategies supported by the engine.
pub(all) enum RuleKind {
  MustContainAny(Array[String])
  MustNotContainAny(Array[String])
  MinOccurrences(String, Int)
  MaxLineLength(Int)
  RequiresPair(String, String)
  SectionOrder(Array[String])
} derive(@debug.Debug)

///|
/// A weighted rule with an explanation-oriented hint.
pub(all) struct Rule {
  id : String
  title : String
  category : String
  severity : Severity
  weight : Int
  kind : RuleKind
  hint : String
} derive(@debug.Debug)

///|
/// Evidence points at a concrete line and matching term.
pub(all) struct Evidence {
  line : Int
  snippet : String
  matched : String
} derive(Eq, @debug.Debug)

///|
/// Result of evaluating a single rule.
pub(all) struct Finding {
  rule_id : String
  title : String
  category : String
  severity : Severity
  passed : Bool
  weight : Int
  earned : Int
  message : String
  hint : String
  evidence : Array[Evidence]
} derive(@debug.Debug)

///|
/// Shape summary produced while tokenizing a document.
pub(all) struct DocumentShape {
  lines : Int
  headings : Int
  bullets : Int
  code_fences : Int
  paragraphs : Int
  blanks : Int
} derive(Eq, @debug.Debug)

///|
/// Aggregated counters for an audit run.
pub(all) struct AuditStats {
  line_count : Int
  char_count : Int
  matched_rules : Int
  failed_rules : Int
  error_count : Int
  warning_count : Int
  heading_count : Int
  bullet_count : Int
} derive(Eq, @debug.Debug)

///|
/// Complete audit result.
pub(all) struct Audit {
  title : String
  score : Int
  max_score : Int
  earned_score : Int
  grade : String
  findings : Array[Finding]
  stats : AuditStats
} derive(@debug.Debug)

///|
/// Construct a presence rule that passes when any term is found.
pub fn Rule::must_contain_any(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  terms : ArrayView[String],
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: MustContainAny(terms.to_owned()),
    hint: hint.to_owned(),
  }
}

///|
/// Construct a rule that fails when any forbidden term is found.
pub fn Rule::must_not_contain_any(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  terms : ArrayView[String],
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: MustNotContainAny(terms.to_owned()),
    hint: hint.to_owned(),
  }
}

///|
/// Construct a minimum occurrence rule.
pub fn Rule::min_occurrences(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  needle : StringView,
  min : Int,
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: MinOccurrences(needle.to_owned(), min),
    hint: hint.to_owned(),
  }
}

///|
/// Construct a maximum line length rule.
pub fn Rule::max_line_length(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  max : Int,
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: MaxLineLength(max),
    hint: hint.to_owned(),
  }
}

///|
/// Construct a rule where the right term is required when the left term exists.
pub fn Rule::requires_pair(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  left : StringView,
  right : StringView,
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: RequiresPair(left.to_owned(), right.to_owned()),
    hint: hint.to_owned(),
  }
}

///|
/// Construct an ordered-section rule.
pub fn Rule::section_order(
  id : StringView,
  title : StringView,
  category : StringView,
  severity : Severity,
  weight : Int,
  sections : ArrayView[String],
  hint : StringView,
) -> Rule {
  {
    id: id.to_owned(),
    title: title.to_owned(),
    category: category.to_owned(),
    severity,
    weight,
    kind: SectionOrder(sections.to_owned()),
    hint: hint.to_owned(),
  }
}

///|
/// True when a failed finding should block release.
pub fn Finding::is_blocking(self : Finding) -> Bool {
  !self.passed && self.severity is Error
}

///|
/// True when the audit has no failing error-level findings.
pub fn Audit::is_success(self : Audit) -> Bool {
  self.stats.error_count == 0
}

///|
/// Return only failed findings.
pub fn Audit::failed(self : Audit) -> Array[Finding] {
  self.findings.filter(finding => !finding.passed)
}

///|
/// Return only passed findings.
pub fn Audit::passed(self : Audit) -> Array[Finding] {
  self.findings.filter(finding => finding.passed)
}