///|
pub(all) enum ExposureLevel {
  Clean
  Low
  Medium
  High
  Severe
} derive(Debug, Eq, Compare)

///|
pub(all) struct SeverityTally {
  mut info : Int
  mut warnings : Int
  mut dangerous : Int
  mut critical : Int
} derive(Debug, Eq)

///|
pub(all) struct DecisionTally {
  mut kept : Int
  mut dropped : Int
  mut marked : Int
} derive(Debug, Eq)

///|
pub(all) struct RiskTally {
  risk : RiskKind
  mut count : Int
  mut info : Int
  mut warnings : Int
  mut dangerous : Int
  mut critical : Int
  mut kept : Int
  mut dropped : Int
  mut marked : Int
  mut first_offset : Int
  mut last_offset : Int
} derive(Debug, Eq)

///|
pub(all) struct RuleTally {
  rule_id : String
  risk : RiskKind
  mut count : Int
  mut info : Int
  mut warnings : Int
  mut dangerous : Int
  mut critical : Int
  mut kept : Int
  mut dropped : Int
  mut marked : Int
  mut first_offset : Int
  mut last_offset : Int
} derive(Debug, Eq)

///|
pub(all) struct ProfileScore {
  points : Int
  level : ExposureLevel
  label : String
} derive(Debug, Eq)

///|
pub(all) struct AuditProfile {
  total_findings : Int
  total_events : Int
  text_chars : Int
  control_events : Int
  severity : SeverityTally
  decisions : DecisionTally
  risks : Array[RiskTally]
  rules : Array[RuleTally]
  highest_severity : Severity
  score : ProfileScore
} derive(Debug, Eq)

///|
pub fn ExposureLevel::name(self : ExposureLevel) -> String {
  match self {
    Clean => "clean"
    Low => "low"
    Medium => "medium"
    High => "high"
    Severe => "severe"
  }
}

///|
pub fn exposure_level_name(level : ExposureLevel) -> String {
  level.name()
}

///|
pub fn ExposureLevel::description(self : ExposureLevel) -> String {
  match self {
    Clean => "no terminal-control risk was detected"
    Low => "only low-impact terminal controls were detected"
    Medium => "reviewable controls were detected and should be sanitized"
    High => "dangerous terminal behavior was detected"
    Severe => "critical terminal behavior was detected and should block release"
  }
}

///|
pub fn exposure_level_description(level : ExposureLevel) -> String {
  level.description()
}

///|
fn empty_severity_tally() -> SeverityTally {
  { info: 0, warnings: 0, dangerous: 0, critical: 0 }
}

///|
fn empty_decision_tally() -> DecisionTally {
  { kept: 0, dropped: 0, marked: 0 }
}

///|
pub fn SeverityTally::total(self : SeverityTally) -> Int {
  self.info + self.warnings + self.dangerous + self.critical
}

///|
pub fn SeverityTally::highest(self : SeverityTally) -> Severity {
  if self.critical > 0 {
    Critical
  } else if self.dangerous > 0 {
    Dangerous
  } else if self.warnings > 0 {
    Warning
  } else {
    Info
  }
}

///|
pub fn DecisionTally::total(self : DecisionTally) -> Int {
  self.kept + self.dropped + self.marked
}

///|
fn increment_severity(tally : SeverityTally, severity : Severity) -> Unit {
  match severity {
    Info => tally.info += 1
    Warning => tally.warnings += 1
    Dangerous => tally.dangerous += 1
    Critical => tally.critical += 1
  }
}

///|
fn increment_decision(tally : DecisionTally, decision : Decision) -> Unit {
  match decision {
    Keep => tally.kept += 1
    Drop => tally.dropped += 1
    Mark => tally.marked += 1
  }
}

///|
fn severity_points(severity : Severity) -> Int {
  match severity {
    Info => 1
    Warning => 5
    Dangerous => 20
    Critical => 50
  }
}

///|
fn decision_points(decision : Decision) -> Int {
  match decision {
    Keep => 3
    Drop => 0
    Mark => 1
  }
}

///|
fn risk_weight(risk : RiskKind) -> Int {
  match risk {
    Styling => 0
    CursorMovement => 4
    ScreenRewrite => 5
    ScreenErase => 6
    TerminalQuery => 2
    ModeChange => 5
    TitleChange => 4
    Hyperlink => 7
    ClipboardWrite => 10
    Notification => 3
    WorkingDirectory => 2
    FileTransfer => 10
    DeviceControl => 9
    BidirectionalText => 8
    UnknownControl => 5
    MalformedSequence => 6
  }
}

///|
fn score_level(points : Int, highest : Severity) -> ExposureLevel {
  if highest == Critical || points >= 80 {
    Severe
  } else if highest == Dangerous || points >= 35 {
    High
  } else if highest == Warning || points >= 10 {
    Medium
  } else if points > 0 {
    Low
  } else {
    Clean
  }
}

///|
fn score_label(level : ExposureLevel) -> String {
  match level {
    Clean => "clean"
    Low => "low terminal-noise risk"
    Medium => "review required"
    High => "dangerous log manipulation"
    Severe => "critical terminal injection"
  }
}

///|
fn make_score(points : Int, highest : Severity) -> ProfileScore {
  let level = score_level(points, highest)
  { points, level, label: score_label(level) }
}

///|
fn empty_risk_tally(risk : RiskKind, offset : Int) -> RiskTally {
  {
    risk,
    count: 0,
    info: 0,
    warnings: 0,
    dangerous: 0,
    critical: 0,
    kept: 0,
    dropped: 0,
    marked: 0,
    first_offset: offset,
    last_offset: offset,
  }
}

///|
fn empty_rule_tally(
  rule_id : String,
  risk : RiskKind,
  offset : Int,
) -> RuleTally {
  {
    rule_id,
    risk,
    count: 0,
    info: 0,
    warnings: 0,
    dangerous: 0,
    critical: 0,
    kept: 0,
    dropped: 0,
    marked: 0,
    first_offset: offset,
    last_offset: offset,
  }
}

///|
fn update_risk_tally(tally : RiskTally, finding : Finding) -> RiskTally {
  let next = tally
  next.count += 1
  match finding.severity {
    Info => next.info += 1
    Warning => next.warnings += 1
    Dangerous => next.dangerous += 1
    Critical => next.critical += 1
  }
  match finding.decision {
    Keep => next.kept += 1
    Drop => next.dropped += 1
    Mark => next.marked += 1
  }
  if finding.offset < next.first_offset {
    next.first_offset = finding.offset
  }
  if finding.offset > next.last_offset {
    next.last_offset = finding.offset
  }
  next
}

///|
fn update_rule_tally(tally : RuleTally, finding : Finding) -> RuleTally {
  let next = tally
  next.count += 1
  match finding.severity {
    Info => next.info += 1
    Warning => next.warnings += 1
    Dangerous => next.dangerous += 1
    Critical => next.critical += 1
  }
  match finding.decision {
    Keep => next.kept += 1
    Drop => next.dropped += 1
    Mark => next.marked += 1
  }
  if finding.offset < next.first_offset {
    next.first_offset = finding.offset
  }
  if finding.offset > next.last_offset {
    next.last_offset = finding.offset
  }
  next
}

///|
fn risk_index(items : Array[RiskTally], risk : RiskKind) -> Int {
  let mut i = 0
  while i < items.length() {
    if items[i].risk == risk {
      return i
    }
    i += 1
  }
  -1
}

///|
fn rule_index(items : Array[RuleTally], rule_id : String) -> Int {
  let mut i = 0
  while i < items.length() {
    if items[i].rule_id == rule_id {
      return i
    }
    i += 1
  }
  -1
}

///|
fn add_risk_finding(items : Array[RiskTally], finding : Finding) -> Unit {
  let index = risk_index(items, finding.risk)
  if index < 0 {
    items.push(
      update_risk_tally(empty_risk_tally(finding.risk, finding.offset), finding),
    )
  } else {
    items[index] = update_risk_tally(items[index], finding)
  }
}

///|
fn add_rule_finding(items : Array[RuleTally], finding : Finding) -> Unit {
  let index = rule_index(items, finding.rule_id)
  if index < 0 {
    items.push(
      update_rule_tally(
        empty_rule_tally(finding.rule_id, finding.risk, finding.offset),
        finding,
      ),
    )
  } else {
    items[index] = update_rule_tally(items[index], finding)
  }
}

///|
fn compute_profile_points(findings : Array[Finding]) -> Int {
  let mut points = 0
  for finding in findings {
    points += severity_points(finding.severity)
    points += risk_weight(finding.risk)
    points += decision_points(finding.decision)
  }
  points
}

///|
pub fn profile_audit(result : AuditResult) -> AuditProfile {
  let severity = empty_severity_tally()
  let decisions = empty_decision_tally()
  let risks : Array[RiskTally] = []
  let rules : Array[RuleTally] = []
  for finding in result.findings {
    increment_severity(severity, finding.severity)
    increment_decision(decisions, finding.decision)
    add_risk_finding(risks, finding)
    add_rule_finding(rules, finding)
  }
  let highest = severity.highest()
  let points = compute_profile_points(result.findings)
  {
    total_findings: result.findings.length(),
    total_events: result.events.length(),
    text_chars: result.stats.text_chars,
    control_events: result.stats.control_events,
    severity,
    decisions,
    risks,
    rules,
    highest_severity: highest,
    score: make_score(points, highest),
  }
}

///|
pub fn AuditResult::profile(self : AuditResult) -> AuditProfile {
  profile_audit(self)
}

///|
pub fn risk_tally(profile : AuditProfile, risk : RiskKind) -> RiskTally? {
  for item in profile.risks {
    if item.risk == risk {
      return Some(item)
    }
  }
  None
}

///|
pub fn rule_tally(profile : AuditProfile, rule_id : String) -> RuleTally? {
  for item in profile.rules {
    if item.rule_id == rule_id {
      return Some(item)
    }
  }
  None
}

///|
pub fn risk_count(profile : AuditProfile, risk : RiskKind) -> Int {
  match risk_tally(profile, risk) {
    Some(item) => item.count
    None => 0
  }
}

///|
pub fn rule_count(profile : AuditProfile, rule_id : String) -> Int {
  match rule_tally(profile, rule_id) {
    Some(item) => item.count
    None => 0
  }
}

///|
fn tally_highest(
  _info : Int,
  warnings : Int,
  dangerous : Int,
  critical : Int,
) -> Severity {
  if critical > 0 {
    Critical
  } else if dangerous > 0 {
    Dangerous
  } else if warnings > 0 {
    Warning
  } else {
    Info
  }
}

///|
pub fn RiskTally::highest(self : RiskTally) -> Severity {
  tally_highest(self.info, self.warnings, self.dangerous, self.critical)
}

///|
pub fn RuleTally::highest(self : RuleTally) -> Severity {
  tally_highest(self.info, self.warnings, self.dangerous, self.critical)
}

///|
fn risk_tally_score(item : RiskTally) -> Int {
  item.count * 10 + severity_rank(item.highest()) * 100 + risk_weight(item.risk)
}

///|
fn rule_tally_score(item : RuleTally) -> Int {
  item.count * 10 + severity_rank(item.highest()) * 100 + risk_weight(item.risk)
}

///|
pub fn top_risk(profile : AuditProfile) -> RiskTally? {
  if profile.risks.length() == 0 {
    return None
  }
  let mut best = profile.risks[0]
  let mut best_score = risk_tally_score(best)
  let mut i = 1
  while i < profile.risks.length() {
    let score = risk_tally_score(profile.risks[i])
    if score > best_score {
      best = profile.risks[i]
      best_score = score
    }
    i += 1
  }
  Some(best)
}

///|
pub fn top_rule(profile : AuditProfile) -> RuleTally? {
  if profile.rules.length() == 0 {
    return None
  }
  let mut best = profile.rules[0]
  let mut best_score = rule_tally_score(best)
  let mut i = 1
  while i < profile.rules.length() {
    let score = rule_tally_score(profile.rules[i])
    if score > best_score {
      best = profile.rules[i]
      best_score = score
    }
    i += 1
  }
  Some(best)
}

///|
pub fn AuditProfile::has_critical(self : AuditProfile) -> Bool {
  self.severity.critical > 0
}

///|
pub fn AuditProfile::has_dangerous(self : AuditProfile) -> Bool {
  self.severity.dangerous > 0 || self.severity.critical > 0
}

///|
pub fn AuditProfile::is_clean(self : AuditProfile) -> Bool {
  self.total_findings == 0
}

///|
pub fn AuditProfile::blocking(self : AuditProfile) -> Bool {
  self.has_critical() || self.score.level == Severe
}

///|
pub fn AuditProfile::summary(self : AuditProfile) -> String {
  if self.is_clean() {
    return "clean: no terminal-control findings"
  }
  let top = match top_risk(self) {
    Some(item) => risk_name(item.risk)
    None => "none"
  }
  "\{self.score.label}: \{self.total_findings} finding(s), " +
  "highest=\{severity_name(self.highest_severity)}, top-risk=\{top}, " +
  "score=\{self.score.points}"
}

///|
pub fn profile_summary(profile : AuditProfile) -> String {
  profile.summary()
}

///|
fn write_profile_tally_json(out : StringBuilder, tally : SeverityTally) -> Unit {
  out.write_string("{\"info\":")
  out.write_string(tally.info.to_string())
  out.write_string(",\"warnings\":")
  out.write_string(tally.warnings.to_string())
  out.write_string(",\"dangerous\":")
  out.write_string(tally.dangerous.to_string())
  out.write_string(",\"critical\":")
  out.write_string(tally.critical.to_string())
  out.write_char('}')
}

///|
fn write_decision_tally_json(
  out : StringBuilder,
  tally : DecisionTally,
) -> Unit {
  out.write_string("{\"kept\":")
  out.write_string(tally.kept.to_string())
  out.write_string(",\"dropped\":")
  out.write_string(tally.dropped.to_string())
  out.write_string(",\"marked\":")
  out.write_string(tally.marked.to_string())
  out.write_char('}')
}

///|
fn write_risk_tally_json(out : StringBuilder, item : RiskTally) -> Unit {
  out.write_string("{\"risk\":")
  out.write_string(json_quote(risk_name(item.risk)))
  out.write_string(",\"count\":")
  out.write_string(item.count.to_string())
  out.write_string(",\"highest\":")
  out.write_string(json_quote(severity_name(item.highest())))
  out.write_string(",\"info\":")
  out.write_string(item.info.to_string())
  out.write_string(",\"warnings\":")
  out.write_string(item.warnings.to_string())
  out.write_string(",\"dangerous\":")
  out.write_string(item.dangerous.to_string())
  out.write_string(",\"critical\":")
  out.write_string(item.critical.to_string())
  out.write_string(",\"kept\":")
  out.write_string(item.kept.to_string())
  out.write_string(",\"dropped\":")
  out.write_string(item.dropped.to_string())
  out.write_string(",\"marked\":")
  out.write_string(item.marked.to_string())
  out.write_string(",\"first_offset\":")
  out.write_string(item.first_offset.to_string())
  out.write_string(",\"last_offset\":")
  out.write_string(item.last_offset.to_string())
  out.write_char('}')
}

///|
fn write_rule_tally_json(out : StringBuilder, item : RuleTally) -> Unit {
  out.write_string("{\"rule_id\":")
  out.write_string(json_quote(item.rule_id))
  out.write_string(",\"risk\":")
  out.write_string(json_quote(risk_name(item.risk)))
  out.write_string(",\"count\":")
  out.write_string(item.count.to_string())
  out.write_string(",\"highest\":")
  out.write_string(json_quote(severity_name(item.highest())))
  out.write_string(",\"info\":")
  out.write_string(item.info.to_string())
  out.write_string(",\"warnings\":")
  out.write_string(item.warnings.to_string())
  out.write_string(",\"dangerous\":")
  out.write_string(item.dangerous.to_string())
  out.write_string(",\"critical\":")
  out.write_string(item.critical.to_string())
  out.write_string(",\"kept\":")
  out.write_string(item.kept.to_string())
  out.write_string(",\"dropped\":")
  out.write_string(item.dropped.to_string())
  out.write_string(",\"marked\":")
  out.write_string(item.marked.to_string())
  out.write_string(",\"first_offset\":")
  out.write_string(item.first_offset.to_string())
  out.write_string(",\"last_offset\":")
  out.write_string(item.last_offset.to_string())
  out.write_char('}')
}

///|
pub fn profile_json(profile : AuditProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("{\"schema\":\"urn:termproof:profile-v1\"")
  out.write_string(",\"total_findings\":")
  out.write_string(profile.total_findings.to_string())
  out.write_string(",\"total_events\":")
  out.write_string(profile.total_events.to_string())
  out.write_string(",\"text_chars\":")
  out.write_string(profile.text_chars.to_string())
  out.write_string(",\"control_events\":")
  out.write_string(profile.control_events.to_string())
  out.write_string(",\"highest_severity\":")
  out.write_string(json_quote(severity_name(profile.highest_severity)))
  out.write_string(",\"score\":{\"points\":")
  out.write_string(profile.score.points.to_string())
  out.write_string(",\"level\":")
  out.write_string(json_quote(profile.score.level.name()))
  out.write_string(",\"label\":")
  out.write_string(json_quote(profile.score.label))
  out.write_char('}')
  out.write_string(",\"severity\":")
  write_profile_tally_json(out, profile.severity)
  out.write_string(",\"decisions\":")
  write_decision_tally_json(out, profile.decisions)
  out.write_string(",\"risks\":[")
  let mut first = true
  for item in profile.risks {
    if !first {
      out.write_char(',')
    }
    first = false
    write_risk_tally_json(out, item)
  }
  out.write_string("],\"rules\":[")
  first = true
  for item in profile.rules {
    if !first {
      out.write_char(',')
    }
    first = false
    write_rule_tally_json(out, item)
  }
  out.write_string("]}")
  out.to_string()
}

///|
fn write_markdown_metric(
  out : StringBuilder,
  label : String,
  value : String,
) -> Unit {
  out.write_string("| ")
  out.write_string(markdown_escape(label))
  out.write_string(" | ")
  out.write_string(markdown_escape(value))
  out.write_string(" |\n")
}

///|
fn write_risk_markdown_row(out : StringBuilder, item : RiskTally) -> Unit {
  out.write_string("| ")
  out.write_string(markdown_escape(risk_name(item.risk)))
  out.write_string(" | ")
  out.write_string(item.count.to_string())
  out.write_string(" | ")
  out.write_string(markdown_escape(severity_name(item.highest())))
  out.write_string(" | ")
  out.write_string(item.first_offset.to_string())
  out.write_string(" | ")
  out.write_string(item.last_offset.to_string())
  out.write_string(" |\n")
}

///|
fn write_rule_markdown_row(out : StringBuilder, item : RuleTally) -> Unit {
  out.write_string("| ")
  out.write_string(markdown_escape(item.rule_id))
  out.write_string(" | ")
  out.write_string(markdown_escape(risk_name(item.risk)))
  out.write_string(" | ")
  out.write_string(item.count.to_string())
  out.write_string(" | ")
  out.write_string(markdown_escape(severity_name(item.highest())))
  out.write_string(" | ")
  out.write_string(item.first_offset.to_string())
  out.write_string(" |\n")
}

///|
pub fn profile_markdown(profile : AuditProfile) -> String {
  let out = StringBuilder::new()
  out.write_string("## Risk profile\n\n")
  out.write_string("| Metric | Value |\n|---|---:|\n")
  write_markdown_metric(
    out,
    "Total findings",
    profile.total_findings.to_string(),
  )
  write_markdown_metric(
    out,
    "Control events",
    profile.control_events.to_string(),
  )
  write_markdown_metric(
    out,
    "Highest severity",
    severity_name(profile.highest_severity),
  )
  write_markdown_metric(out, "Exposure level", profile.score.level.name())
  write_markdown_metric(out, "Score", profile.score.points.to_string())
  write_markdown_metric(
    out,
    "Kept decisions",
    profile.decisions.kept.to_string(),
  )
  write_markdown_metric(
    out,
    "Dropped decisions",
    profile.decisions.dropped.to_string(),
  )
  write_markdown_metric(
    out,
    "Marked decisions",
    profile.decisions.marked.to_string(),
  )
  if profile.risks.length() > 0 {
    out.write_string("\n### Risks\n\n")
    out.write_string(
      "| Risk | Count | Highest | First offset | Last offset |\n",
    )
    out.write_string("|---|---:|---|---:|---:|\n")
    for item in profile.risks {
      write_risk_markdown_row(out, item)
    }
  }
  if profile.rules.length() > 0 {
    out.write_string("\n### Rules\n\n")
    out.write_string("| Rule | Risk | Count | Highest | First offset |\n")
    out.write_string("|---|---|---:|---|---:|\n")
    for item in profile.rules {
      write_rule_markdown_row(out, item)
    }
  }
  out.to_string()
}

///|
pub fn profile_text(profile : AuditProfile) -> String {
  let out = StringBuilder::new()
  out.write_string(profile.summary())
  out.write_char('\n')
  out.write_string("severity: info=")
  out.write_string(profile.severity.info.to_string())
  out.write_string(", warning=")
  out.write_string(profile.severity.warnings.to_string())
  out.write_string(", dangerous=")
  out.write_string(profile.severity.dangerous.to_string())
  out.write_string(", critical=")
  out.write_string(profile.severity.critical.to_string())
  out.write_char('\n')
  out.write_string("decisions: kept=")
  out.write_string(profile.decisions.kept.to_string())
  out.write_string(", dropped=")
  out.write_string(profile.decisions.dropped.to_string())
  out.write_string(", marked=")
  out.write_string(profile.decisions.marked.to_string())
  out.write_char('\n')
  if profile.risks.length() > 0 {
    out.write_string("risks:\n")
    for item in profile.risks {
      out.write_string("- ")
      out.write_string(risk_name(item.risk))
      out.write_string(": ")
      out.write_string(item.count.to_string())
      out.write_string(" (")
      out.write_string(severity_name(item.highest()))
      out.write_string(")\n")
    }
  }
  out.to_string()
}

///|
pub fn profile_has_risk(profile : AuditProfile, risk : RiskKind) -> Bool {
  risk_count(profile, risk) > 0
}

///|
pub fn profile_has_rule(profile : AuditProfile, rule_id : String) -> Bool {
  rule_count(profile, rule_id) > 0
}

///|
pub fn profile_release_gate(profile : AuditProfile) -> String {
  if profile.is_clean() {
    "pass"
  } else if profile.has_critical() {
    "block"
  } else if profile.has_dangerous() {
    "review"
  } else {
    "warn"
  }
}

///|
pub fn profile_short_advice(profile : AuditProfile) -> String {
  match profile_release_gate(profile) {
    "pass" => "safe to display without terminal-control remediation"
    "warn" => "sanitize before publishing and keep findings for audit trail"
    "review" => "review the affected lines before publishing generated logs"
    "block" => "block release until critical terminal injection is removed"
    _ => "review audit profile"
  }
}