///|
pub(all) struct LineRisk {
  line : Int
  mut findings : Int
  mut info : Int
  mut warnings : Int
  mut dangerous : Int
  mut critical : Int
  mut risks : Array[RiskKind]
} derive(Debug, Eq)

///|
pub(all) struct AuditOverview {
  result : AuditResult
  profile : AuditProfile
  remediation : RemediationPlan
  line_index : LineIndex
  located : Array[LocatedFinding]
  line_risks : Array[LineRisk]
} derive(Debug, Eq)

///|
fn empty_line_risk(line : Int) -> LineRisk {
  {
    line,
    findings: 0,
    info: 0,
    warnings: 0,
    dangerous: 0,
    critical: 0,
    risks: [],
  }
}

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

///|
fn line_risk_has_kind(item : LineRisk, risk : RiskKind) -> Bool {
  for existing in item.risks {
    if existing == risk {
      return true
    }
  }
  false
}

///|
fn update_line_risk(item : LineRisk, located : LocatedFinding) -> LineRisk {
  let next = item
  next.findings += 1
  match located.finding.severity {
    Info => next.info += 1
    Warning => next.warnings += 1
    Dangerous => next.dangerous += 1
    Critical => next.critical += 1
  }
  if !line_risk_has_kind(next, located.finding.risk) {
    next.risks.push(located.finding.risk)
  }
  next
}

///|
fn add_line_risk(items : Array[LineRisk], located : LocatedFinding) -> Unit {
  let line = located.position.line
  let index = line_risk_index(items, line)
  if index < 0 {
    items.push(update_line_risk(empty_line_risk(line), located))
  } else {
    items[index] = update_line_risk(items[index], located)
  }
}

///|
fn build_line_risks(located : Array[LocatedFinding]) -> Array[LineRisk] {
  let items : Array[LineRisk] = []
  for item in located {
    add_line_risk(items, item)
  }
  items
}

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

///|
fn line_risk_score(item : LineRisk) -> Int {
  severity_rank(item.highest()) * 100 + item.findings * 10 + item.risks.length()
}

///|
pub fn top_line_risk(overview : AuditOverview) -> LineRisk? {
  if overview.line_risks.length() == 0 {
    return None
  }
  let mut best = overview.line_risks[0]
  let mut score = line_risk_score(best)
  let mut i = 1
  while i < overview.line_risks.length() {
    let next_score = line_risk_score(overview.line_risks[i])
    if next_score > score {
      best = overview.line_risks[i]
      score = next_score
    }
    i += 1
  }
  Some(best)
}

///|
pub fn overview_from_result(
  source : String,
  result : AuditResult,
) -> AuditOverview {
  let index = build_line_index(source)
  let located = locate_findings(source, result.findings)
  {
    result,
    profile: profile_audit(result),
    remediation: remediation_plan(result),
    line_index: index,
    located,
    line_risks: build_line_risks(located),
  }
}

///|
pub fn audit_overview(
  source : String,
  policy? : Policy = Policy::strict(),
) -> AuditOverview {
  overview_from_result(source, audit(source, policy~))
}

///|
pub fn AuditOverview::gate(self : AuditOverview) -> String {
  self.remediation.gate
}

///|
pub fn AuditOverview::passed(self : AuditOverview) -> Bool {
  self.gate() == "pass"
}

///|
pub fn AuditOverview::failed(self : AuditOverview) -> Bool {
  self.result.failed || self.remediation.blocked
}

///|
pub fn AuditOverview::summary(self : AuditOverview) -> String {
  let top_line = match top_line_risk(self) {
    Some(line) => "line \{line.line}"
    None => "none"
  }
  "\{profile_summary(self.profile)}; gate=\{self.gate()}; top-line=\{top_line}"
}

///|
pub fn overview_summary(overview : AuditOverview) -> String {
  overview.summary()
}

///|
fn write_overview_text_findings(
  out : StringBuilder,
  overview : AuditOverview,
) -> Unit {
  if overview.located.length() == 0 {
    out.write_string("findings: none\n")
    return
  }
  out.write_string("findings:\n")
  for item in overview.located {
    out.write_string("- ")
    out.write_string(item.summary())
    out.write_char('\n')
  }
}

///|
fn write_overview_text_lines(
  out : StringBuilder,
  overview : AuditOverview,
) -> Unit {
  if overview.line_risks.length() == 0 {
    return
  }
  out.write_string("line risks:\n")
  for item in overview.line_risks {
    out.write_string("- line ")
    out.write_string(item.line.to_string())
    out.write_string(": ")
    out.write_string(item.findings.to_string())
    out.write_string(" finding(s), highest=")
    out.write_string(severity_name(item.highest()))
    out.write_char('\n')
  }
}

///|
pub fn overview_text(overview : AuditOverview) -> String {
  let out = StringBuilder::new()
  out.write_string(overview.summary())
  out.write_char('\n')
  out.write_string(profile_text(overview.profile))
  out.write_string(remediation_text(overview.remediation))
  write_overview_text_findings(out, overview)
  write_overview_text_lines(out, overview)
  out.to_string()
}

///|
pub fn audit_overview_text(
  source : String,
  policy? : Policy = Policy::strict(),
) -> String {
  overview_text(audit_overview(source, policy~))
}

///|
fn write_line_risk_markdown(out : StringBuilder, line_risk : LineRisk) -> Unit {
  out.write_string("| ")
  out.write_string(line_risk.line.to_string())
  out.write_string(" | ")
  out.write_string(line_risk.findings.to_string())
  out.write_string(" | ")
  out.write_string(markdown_escape(severity_name(line_risk.highest())))
  out.write_string(" | ")
  let mut first = true
  for risk in line_risk.risks {
    if !first {
      out.write_string(", ")
    }
    first = false
    out.write_string(markdown_escape(risk_name(risk)))
  }
  out.write_string(" |\n")
}

///|
fn write_overview_markdown_header(
  out : StringBuilder,
  overview : AuditOverview,
) -> Unit {
  out.write_string("# TermProof audit overview\n\n")
  out.write_string("| Field | Value |\n|---|---|\n")
  out.write_string("| Gate | ")
  out.write_string(markdown_escape(overview.gate()))
  out.write_string(" |\n")
  out.write_string("| Failed | ")
  out.write_string(if overview.failed() { "yes" } else { "no" })
  out.write_string(" |\n")
  out.write_string("| Lines | ")
  out.write_string(overview.line_index.line_count().to_string())
  out.write_string(" |\n")
  out.write_string("| Findings | ")
  out.write_string(overview.profile.total_findings.to_string())
  out.write_string(" |\n")
  out.write_string("| Highest severity | ")
  out.write_string(
    markdown_escape(severity_name(overview.profile.highest_severity)),
  )
  out.write_string(" |\n")
  out.write_string("| Exposure | ")
  out.write_string(markdown_escape(overview.profile.score.label))
  out.write_string(" |\n\n")
}

///|
fn write_overview_markdown_lines(
  out : StringBuilder,
  overview : AuditOverview,
) -> Unit {
  if overview.line_risks.length() == 0 {
    return
  }
  out.write_string("## Line risk map\n\n")
  out.write_string("| Line | Findings | Highest | Risks |\n")
  out.write_string("|---:|---:|---|---|\n")
  for item in overview.line_risks {
    write_line_risk_markdown(out, item)
  }
  out.write_char('\n')
}

///|
fn write_overview_markdown_context(
  out : StringBuilder,
  overview : AuditOverview,
) -> Unit {
  if overview.located.length() == 0 {
    return
  }
  out.write_string("## Context snippets\n\n")
  for item in overview.located {
    out.write_string("### ")
    out.write_string(markdown_escape(item.summary()))
    out.write_string("\n\n")
    out.write_string("```text\n")
    out.write_string(
      overview.line_index.window_at(item.finding.offset, context=1),
    )
    out.write_string("```\n\n")
  }
}

///|
pub fn overview_markdown(overview : AuditOverview) -> String {
  let out = StringBuilder::new()
  write_overview_markdown_header(out, overview)
  out.write_string(profile_markdown(overview.profile))
  out.write_char('\n')
  out.write_string(remediation_markdown(overview.remediation))
  out.write_char('\n')
  if overview.located.length() == 0 {
    out.write_string("## Findings\n\nNo terminal-control findings.\n")
  } else {
    out.write_string("## Findings with source locations\n\n")
    out.write_string(located_findings_markdown(overview.located))
    out.write_char('\n')
  }
  write_overview_markdown_lines(out, overview)
  write_overview_markdown_context(out, overview)
  out.to_string()
}

///|
pub fn audit_overview_markdown(
  source : String,
  policy? : Policy = Policy::strict(),
) -> String {
  overview_markdown(audit_overview(source, policy~))
}

///|
fn write_line_risk_json(out : StringBuilder, line_risk : LineRisk) -> Unit {
  out.write_string("{\"line\":")
  out.write_string(line_risk.line.to_string())
  out.write_string(",\"findings\":")
  out.write_string(line_risk.findings.to_string())
  out.write_string(",\"highest\":")
  out.write_string(json_quote(severity_name(line_risk.highest())))
  out.write_string(",\"risks\":[")
  let mut first = true
  for risk in line_risk.risks {
    if !first {
      out.write_char(',')
    }
    first = false
    out.write_string(json_quote(risk_name(risk)))
  }
  out.write_string("]}")
}

///|
pub fn overview_json(overview : AuditOverview) -> String {
  let out = StringBuilder::new()
  out.write_string("{\"schema\":\"urn:termproof:overview-v1\"")
  out.write_string(",\"summary\":")
  out.write_string(json_quote(overview.summary()))
  out.write_string(",\"gate\":")
  out.write_string(json_quote(overview.gate()))
  out.write_string(",\"failed\":")
  out.write_string(if overview.failed() { "true" } else { "false" })
  out.write_string(",\"line_count\":")
  out.write_string(overview.line_index.line_count().to_string())
  out.write_string(",\"profile\":")
  out.write_string(profile_json(overview.profile))
  out.write_string(",\"remediation\":")
  out.write_string(remediation_json(overview.remediation))
  out.write_string(",\"located_findings\":")
  out.write_string(located_findings_json(overview.located))
  out.write_string(",\"line_risks\":[")
  let mut first = true
  for item in overview.line_risks {
    if !first {
      out.write_char(',')
    }
    first = false
    write_line_risk_json(out, item)
  }
  out.write_string("]}")
  out.to_string()
}

///|
pub fn audit_overview_json(
  source : String,
  policy? : Policy = Policy::strict(),
) -> String {
  overview_json(audit_overview(source, policy~))
}