///|
/// Recommended handling decision for audited output.
pub(all) enum Decision {
  Allow
  Review
  Block
} derive(Eq, Debug)

///|
/// Returns a stable machine-friendly decision label.
pub fn Decision::label(self : Decision) -> String {
  match self {
    Allow => "allow"
    Review => "review"
    Block => "block"
  }
}

///|
/// Configures how audit findings become allow, review, or block decisions.
pub struct GatePolicy {
  block_on_dangerous : Bool
  block_score : Int
  review_score : Int
  review_finding_count : Int
  blocked_kinds : Array[FindingKind]
} derive(Eq, Debug)

///|
/// Conservative policy suitable for public CI output.
pub fn GatePolicy::strict() -> GatePolicy {
  {
    block_on_dangerous: true,
    block_score: 10,
    review_score: 1,
    review_finding_count: 1,
    blocked_kinds: [ClipboardWrite, FileTransfer],
  }
}

///|
/// Balanced default that reviews warnings and blocks dangerous output.
pub fn GatePolicy::balanced() -> GatePolicy {
  {
    block_on_dangerous: true,
    block_score: 10,
    review_score: 3,
    review_finding_count: 3,
    blocked_kinds: [ClipboardWrite, FileTransfer],
  }
}

///|
/// Policy for trusted internal terminals that still blocks critical payloads.
pub fn GatePolicy::permissive() -> GatePolicy {
  {
    block_on_dangerous: false,
    block_score: 20,
    review_score: 6,
    review_finding_count: 10,
    blocked_kinds: [ClipboardWrite, FileTransfer],
  }
}

///|
/// Creates a custom gate policy.
pub fn GatePolicy::new(
  block_on_dangerous? : Bool = true,
  block_score? : Int = 10,
  review_score? : Int = 3,
  review_finding_count? : Int = 3,
  blocked_kinds? : Array[FindingKind] = [],
) -> GatePolicy {
  let normalized_block = if block_score < 1 { 1 } else { block_score }
  let normalized_review = if review_score < 0 {
    0
  } else if review_score > normalized_block {
    normalized_block
  } else {
    review_score
  }
  {
    block_on_dangerous,
    block_score: normalized_block,
    review_score: normalized_review,
    review_finding_count: if review_finding_count < 1 {
      1
    } else {
      review_finding_count
    },
    blocked_kinds: blocked_kinds.copy(),
  }
}

///|
pub fn GatePolicy::blocks_dangerous(self : GatePolicy) -> Bool {
  self.block_on_dangerous
}

///|
pub fn GatePolicy::block_threshold(self : GatePolicy) -> Int {
  self.block_score
}

///|
pub fn GatePolicy::review_threshold(self : GatePolicy) -> Int {
  self.review_score
}

///|
pub fn GatePolicy::finding_threshold(self : GatePolicy) -> Int {
  self.review_finding_count
}

///|
pub fn GatePolicy::blocked_kinds(self : GatePolicy) -> Array[FindingKind] {
  self.blocked_kinds.copy()
}

///|
fn contains_kind(kinds : Array[FindingKind], kind : FindingKind) -> Bool {
  kinds.any(candidate => candidate == kind)
}

///|
/// Applies a configurable automation gate.
pub fn ScanResult::decide_with(
  self : ScanResult,
  policy : GatePolicy,
) -> Decision {
  if self.findings.any(finding => {
      contains_kind(policy.blocked_kinds, finding.kind)
    }) {
    Block
  } else if policy.block_on_dangerous && self.dangerous_count() > 0 {
    Block
  } else if self.risk_score() >= policy.block_score {
    Block
  } else if self.risk_score() >= policy.review_score ||
    self.findings.length() >= policy.review_finding_count {
    Review
  } else {
    Allow
  }
}

///|
/// Explains which gate conditions were triggered.
pub fn ScanResult::decision_reasons(
  self : ScanResult,
  policy : GatePolicy,
) -> Array[String] {
  let reasons : Array[String] = []
  for finding in self.findings {
    if contains_kind(policy.blocked_kinds, finding.kind) {
      let reason = "blocked finding kind: \{finding.kind.label()}"
      if !reasons.contains(reason) {
        reasons.push(reason)
      }
    }
  }
  if policy.block_on_dangerous && self.dangerous_count() > 0 {
    reasons.push("dangerous findings: \{self.dangerous_count()}")
  }
  if self.risk_score() >= policy.block_score {
    reasons.push(
      "risk score \{self.risk_score()} reached block threshold \{policy.block_score}",
    )
  } else if self.risk_score() >= policy.review_score {
    reasons.push(
      "risk score \{self.risk_score()} reached review threshold \{policy.review_score}",
    )
  }
  if self.findings.length() >= policy.review_finding_count {
    reasons.push(
      "finding count \{self.findings.length()} reached review threshold \{policy.review_finding_count}",
    )
  }
  if reasons.length() == 0 {
    reasons.push("no gate threshold was reached")
  }
  reasons
}

///|
/// Count for one finding category.
pub struct KindCount {
  kind : FindingKind
  count : Int
} derive(Eq, Debug)

///|
pub fn KindCount::kind(self : KindCount) -> FindingKind {
  self.kind
}

///|
pub fn KindCount::count(self : KindCount) -> Int {
  self.count
}

///|
/// Stable summary of an audit result.
pub struct AuditSummary {
  risk_level : String
  risk_score : Int
  decision : Decision
  total_findings : Int
  dangerous : Int
  warning : Int
  info : Int
  kind_counts : Array[KindCount]
} derive(Eq, Debug)

///|
pub fn AuditSummary::risk_level(self : AuditSummary) -> String {
  self.risk_level
}

///|
pub fn AuditSummary::risk_score(self : AuditSummary) -> Int {
  self.risk_score
}

///|
pub fn AuditSummary::decision(self : AuditSummary) -> Decision {
  self.decision
}

///|
pub fn AuditSummary::total_findings(self : AuditSummary) -> Int {
  self.total_findings
}

///|
pub fn AuditSummary::dangerous_count(self : AuditSummary) -> Int {
  self.dangerous
}

///|
pub fn AuditSummary::warning_count(self : AuditSummary) -> Int {
  self.warning
}

///|
pub fn AuditSummary::info_count(self : AuditSummary) -> Int {
  self.info
}

///|
pub fn AuditSummary::kind_counts(self : AuditSummary) -> Array[KindCount] {
  self.kind_counts.copy()
}

///|
/// Controls how much potentially sensitive content is included in reports.
pub struct ReportPolicy {
  include_safe_text : Bool
  include_sequences : Bool
  safe_text_limit : Int
  sequence_limit : Int
} derive(Eq, Debug)

///|
/// Includes complete sanitized text and captured control sequences.
pub fn ReportPolicy::full() -> ReportPolicy {
  {
    include_safe_text: true,
    include_sequences: true,
    safe_text_limit: 0,
    sequence_limit: 0,
  }
}

///|
/// Includes content but limits large fields to practical diagnostic sizes.
pub fn ReportPolicy::bounded() -> ReportPolicy {
  {
    include_safe_text: true,
    include_sequences: true,
    safe_text_limit: 2048,
    sequence_limit: 160,
  }
}

///|
/// Keeps sanitized text but removes captured terminal payloads.
pub fn ReportPolicy::redacted() -> ReportPolicy {
  {
    include_safe_text: true,
    include_sequences: false,
    safe_text_limit: 2048,
    sequence_limit: 0,
  }
}

///|
/// Emits only classification and aggregate metadata.
pub fn ReportPolicy::metadata_only() -> ReportPolicy {
  {
    include_safe_text: false,
    include_sequences: false,
    safe_text_limit: 0,
    sequence_limit: 0,
  }
}

///|
pub fn ReportPolicy::new(
  include_safe_text? : Bool = true,
  include_sequences? : Bool = false,
  safe_text_limit? : Int = 2048,
  sequence_limit? : Int = 160,
) -> ReportPolicy {
  {
    include_safe_text,
    include_sequences,
    safe_text_limit: if safe_text_limit < 0 {
      0
    } else {
      safe_text_limit
    },
    sequence_limit: if sequence_limit < 0 {
      0
    } else {
      sequence_limit
    },
  }
}

///|
pub fn ReportPolicy::includes_safe_text(self : ReportPolicy) -> Bool {
  self.include_safe_text
}

///|
pub fn ReportPolicy::includes_sequences(self : ReportPolicy) -> Bool {
  self.include_sequences
}

///|
pub fn ReportPolicy::safe_text_max_length(self : ReportPolicy) -> Int {
  self.safe_text_limit
}

///|
pub fn ReportPolicy::sequence_max_length(self : ReportPolicy) -> Int {
  self.sequence_limit
}

///|
/// Returns a typed handling decision.
pub fn ScanResult::recommended_decision(self : ScanResult) -> Decision {
  if self.dangerous_count() > 0 {
    Block
  } else if self.warning_count() > 0 {
    Review
  } else {
    Allow
  }
}

///|
/// Counts findings with a specific category.
pub fn ScanResult::count_kind(self : ScanResult, kind : FindingKind) -> Int {
  self.findings.fold(init=0, fn(total, finding) {
    if finding.kind == kind {
      total + 1
    } else {
      total
    }
  })
}

///|
fn all_finding_kinds() -> Array[FindingKind] {
  [
    StyleSequence,
    CursorControl,
    ScreenControl,
    ModeChange,
    TerminalQuery,
    WindowControl,
    TitleChange,
    Hyperlink,
    ClipboardWrite,
    WorkingDirectory,
    Notification,
    ShellIntegration,
    FileTransfer,
    PaletteChange,
    CharacterSet,
    DeviceControl,
    ControlCharacter,
    TruncatedSequence,
    OversizedSequence,
    UnknownEscape,
  ]
}

///|
/// Returns non-empty category counts in stable order.
pub fn ScanResult::kind_counts(self : ScanResult) -> Array[KindCount] {
  let counts : Array[KindCount] = []
  for kind in all_finding_kinds() {
    let count = self.count_kind(kind)
    if count > 0 {
      counts.push({ kind, count })
    }
  }
  counts
}

///|
/// Creates a compact structured summary.
pub fn ScanResult::summary(self : ScanResult) -> AuditSummary {
  {
    risk_level: self.risk_level(),
    risk_score: self.risk_score(),
    decision: self.recommended_decision(),
    total_findings: self.findings.length(),
    dangerous: self.dangerous_count(),
    warning: self.warning_count(),
    info: self.info_count(),
    kind_counts: self.kind_counts(),
  }
}

///|
fn hex_digit(value : Int) -> Char {
  if value < 10 {
    (value + '0'.to_int()).unsafe_to_char()
  } else {
    (value - 10 + 'a'.to_int()).unsafe_to_char()
  }
}

///|
fn json_escape(input : String) -> String {
  let output : Array[Char] = []
  for char in input {
    let code = char.to_int()
    if char == '"' {
      output.push('\\')
      output.push('"')
    } else if char == '\\' {
      output.push('\\')
      output.push('\\')
    } else if char == '\b' {
      output.push('\\')
      output.push('b')
    } else if char == '\u{0c}' {
      output.push('\\')
      output.push('f')
    } else if char == '\n' {
      output.push('\\')
      output.push('n')
    } else if char == '\r' {
      output.push('\\')
      output.push('r')
    } else if char == '\t' {
      output.push('\\')
      output.push('t')
    } else if code < 0x20 {
      output.push('\\')
      output.push('u')
      output.push('0')
      output.push('0')
      output.push(hex_digit(code / 16 % 16))
      output.push(hex_digit(code % 16))
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}

///|
fn json_string(value : String) -> String {
  "\"\{json_escape(value)}\""
}

///|
fn bounded_text(input : String, limit : Int) -> String {
  if limit == 0 {
    input
  } else {
    let chars = input.to_array()
    if chars.length() <= limit {
      input
    } else {
      String::from_array(chars[0:limit]) + "..."
    }
  }
}

///|
fn report_safe_text(result : ScanResult, policy : ReportPolicy) -> String {
  if policy.include_safe_text {
    json_string(bounded_text(result.text, policy.safe_text_limit))
  } else {
    "null"
  }
}

///|
fn report_sequence(finding : Finding, policy : ReportPolicy) -> String {
  if policy.include_sequences {
    json_string(bounded_text(finding.sequence_preview(), policy.sequence_limit))
  } else {
    "null"
  }
}

///|
/// Renders one finding as a stable JSON object.
pub fn Finding::to_json(self : Finding) -> String {
  self.to_json_with(ReportPolicy::full())
}

///|
/// Renders one finding according to a report redaction policy.
pub fn Finding::to_json_with(self : Finding, policy : ReportPolicy) -> String {
  "{" +
  "\"kind\":" +
  json_string(self.kind.label()) +
  ",\"severity\":" +
  json_string(self.severity.label()) +
  ",\"offset\":" +
  self.offset.to_string() +
  ",\"sequence\":" +
  report_sequence(self, policy) +
  ",\"message\":" +
  json_string(self.message) +
  "}"
}

///|
fn kind_counts_json(counts : Array[KindCount]) -> String {
  let mut output = "{"
  for index, count in counts {
    if index > 0 {
      output = output + ","
    }
    output = output +
      "\{json_string(count.kind.label())}:\{count.count.to_string()}"
  }
  output + "}"
}

///|
fn findings_json(findings : Array[Finding], policy : ReportPolicy) -> String {
  let mut output = "["
  for index, finding in findings {
    if index > 0 {
      output = output + ","
    }
    output = output + finding.to_json_with(policy)
  }
  output + "]"
}

///|
/// Renders a machine-readable JSON audit report.
pub fn ScanResult::json_report(self : ScanResult) -> String {
  self.json_report_with(ReportPolicy::full())
}

///|
/// Renders a JSON report with configurable content redaction and limits.
pub fn ScanResult::json_report_with(
  self : ScanResult,
  policy : ReportPolicy,
) -> String {
  let summary = self.summary()
  "{" +
  "\"schema\":\"moonansi-guard/v1\"" +
  ",\"risk_level\":" +
  json_string(summary.risk_level) +
  ",\"risk_score\":" +
  summary.risk_score.to_string() +
  ",\"decision\":" +
  json_string(summary.decision.label()) +
  ",\"removed_count\":" +
  self.removed_count.to_string() +
  ",\"counts\":{\"dangerous\":" +
  summary.dangerous.to_string() +
  ",\"warning\":" +
  summary.warning.to_string() +
  ",\"info\":" +
  summary.info.to_string() +
  "}" +
  ",\"kind_counts\":" +
  kind_counts_json(summary.kind_counts) +
  ",\"safe_text\":" +
  report_safe_text(self, policy) +
  ",\"findings\":" +
  findings_json(self.findings, policy) +
  "}"
}

///|
/// Renders a human-readable report without exposing captured sequence payloads.
pub fn ScanResult::redacted_audit_report(self : ScanResult) -> String {
  let mut report = "MoonAnsi Guard redacted audit report\n"
  report = report + "risk: \{self.risk_level()} (score=\{self.risk_score()})\n"
  report = report + "action: \{self.recommended_action()}\n"
  report = report +
    "findings: dangerous=\{self.dangerous_count()}, warning=\{self.warning_count()}, info=\{self.info_count()}\n"
  report = report + "safe text: \{bounded_text(self.text, 2048)}\n"
  if self.findings.length() == 0 {
    report = report + "details: no terminal control sequences found\n"
  } else {
    report = report + "details:\n"
    for finding in self.findings {
      report = report +
        "- \{finding.kind.label()} [\{finding.severity.label()}] offset \{finding.offset}: \{finding.message}; sequence=\n"
    }
  }
  report
}

///|
/// Aggregates results from multiple chunks or independent scan operations.
pub struct AuditSession {
  mut segment_count : Int
  mut safe_text : String
  findings : Array[Finding]
}

///|
pub fn AuditSession::new() -> AuditSession {
  { segment_count: 0, safe_text: "", findings: [] }
}

///|
/// Adds one scan result while preserving its finding offsets.
pub fn AuditSession::record(self : AuditSession, result : ScanResult) -> Unit {
  self.segment_count = self.segment_count + 1
  self.safe_text = self.safe_text + result.text
  for finding in result.findings {
    self.findings.push(finding)
  }
}

///|
pub fn AuditSession::segment_count(self : AuditSession) -> Int {
  self.segment_count
}

///|
pub fn AuditSession::finding_count(self : AuditSession) -> Int {
  self.findings.length()
}

///|
/// Returns an immutable snapshot compatible with the normal reporting API.
pub fn AuditSession::result(self : AuditSession) -> ScanResult {
  {
    text: self.safe_text,
    findings: self.findings.copy(),
    removed_count: self.findings.length(),
  }
}

///|
pub fn AuditSession::reset(self : AuditSession) -> Unit {
  self.segment_count = 0
  self.safe_text = ""
  self.findings.clear()
}