///|
/// Output flavor for audit reports.
pub(all) enum ReportFlavor {
  ReportText
  ReportMarkdown
  ReportJson
} derive(Eq, Debug)

///|
pub fn ReportFlavor::name(self : ReportFlavor) -> String {
  match self {
    ReportText => "text"
    ReportMarkdown => "markdown"
    ReportJson => "json"
  }
}

///|
pub fn Severity::label(self : Severity) -> String {
  match self {
    Info => "info"
    Warning => "warning"
    High => "high"
  }
}

///|
pub fn Finding::directive_label(self : Finding) -> String {
  match self.directive {
    Some(name) => name
    None => "-"
  }
}

///|
/// Escape string content for the JSON reports emitted by CSPKit.
fn json_escape(input : String) -> String {
  let out = StringBuilder(size_hint=input.length())
  for c in input {
    let code = c.to_int()
    if code == 34 {
      out.write_string("\\\"")
    } else if code == 92 {
      out.write_string("\\\\")
    } else if code == 10 {
      out.write_string("\\n")
    } else if code == 13 {
      out.write_string("\\r")
    } else if code == 9 {
      out.write_string("\\t")
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}

///|
pub fn Finding::text_line(self : Finding) -> String {
  self.severity.label() +
  " " +
  self.code +
  " " +
  self.directive_label() +
  " " +
  self.message
}

///|
pub fn Finding::markdown_line(self : Finding) -> String {
  "- `" +
  self.severity.label() +
  "` `" +
  self.code +
  "` `" +
  self.directive_label() +
  "` " +
  self.message
}

///|
pub fn Finding::json_line(self : Finding) -> String {
  "{\"severity\":\"" +
  self.severity.label() +
  "\",\"code\":\"" +
  json_escape(self.code) +
  "\",\"directive\":\"" +
  json_escape(self.directive_label()) +
  "\",\"message\":\"" +
  json_escape(self.message) +
  "\"}"
}

///|
pub fn Directive::json_line(self : Directive) -> String {
  "{\"name\":\"" +
  json_escape(self.name) +
  "\",\"values\":\"" +
  json_escape(self.values.join(" ")) +
  "\",\"order\":\{self.order}}"
}

///|
pub fn SourceExpression::json_line(self : SourceExpression) -> String {
  "{\"raw\":\"" +
  json_escape(self.raw) +
  "\",\"kind\":\"" +
  self.kind.name() +
  "\",\"trust\":\"" +
  self.trust.name() +
  "\"}"
}

///|
pub fn PolicyScore::text_lines(self : PolicyScore) -> Array[String] {
  let lines : Array[String] = []
  lines.push("score: " + self.summary())
  lines.push("findings: " + self.findings.summary())
  lines.push("directives: " + self.directives.summary())
  lines.push("sources: " + self.sources.summary())
  lines
}

///|
pub fn PolicyScore::text_report(self : PolicyScore) -> String {
  self.text_lines().join("\n")
}

///|
pub fn PolicyScore::markdown_report(self : PolicyScore) -> String {
  let lines : Array[String] = []
  lines.push("## CSPKit Score")
  lines.push("")
  lines.push("- Score: `\{self.final_score}`")
  lines.push("- Grade: `\{self.grade.label()}`")
  lines.push("- Penalty: `\{self.penalty}`")
  lines.push("- Bonus: `\{self.bonus}`")
  lines.push("- Findings: `\{self.findings.summary()}`")
  lines.push("- Directives: `\{self.directives.summary()}`")
  lines.push("- Sources: `\{self.sources.summary()}`")
  lines.join("\n")
}

///|
pub fn PolicyScore::json_report(self : PolicyScore) -> String {
  "{\"score\":\{self.final_score},\"grade\":\"" +
  self.grade.label() +
  "\",\"penalty\":\{self.penalty},\"bonus\":\{self.bonus},\"high\":\{self.findings.high},\"warning\":\{self.findings.warning},\"info\":\{self.findings.info}}"
}

///|
pub fn HardeningRecommendation::markdown_line(
  self : HardeningRecommendation,
) -> String {
  let directive = match self.directive {
    Some(name) => name
    None => "-"
  }
  "- `" +
  self.priority.name() +
  "` `" +
  self.code +
  "` `" +
  directive +
  "` " +
  self.summary +
  " " +
  self.detail
}

///|
pub fn HardeningRecommendation::json_line(
  self : HardeningRecommendation,
) -> String {
  let directive = match self.directive {
    Some(name) => name
    None => "-"
  }
  "{\"priority\":\"" +
  self.priority.name() +
  "\",\"code\":\"" +
  json_escape(self.code) +
  "\",\"directive\":\"" +
  json_escape(directive) +
  "\",\"summary\":\"" +
  json_escape(self.summary) +
  "\"}"
}

///|
pub fn Policy::audit_text_report(self : Policy) -> String {
  let lines : Array[String] = []
  lines.push("CSPKit Audit Report")
  lines.push(self.score().summary())
  lines.push(self.directive_catalog_summary())
  lines.push(self.source_inventory_report())
  lines.push("Findings")
  for finding in self.audit() {
    lines.push(finding.text_line())
  }
  lines.push("Recommendations")
  for item in self.hardening_recommendations() {
    lines.push(item.summary_line())
  }
  lines.join("\n")
}

///|
pub fn Policy::audit_markdown_report(self : Policy) -> String {
  let lines : Array[String] = []
  lines.push("# CSPKit Audit Report")
  lines.push("")
  lines.push(self.score().markdown_report())
  lines.push("")
  lines.push("## Directive Catalog")
  lines.push("")
  lines.push("```text")
  lines.push(self.directive_catalog_summary())
  lines.push("```")
  lines.push("")
  lines.push("## Source Inventory")
  lines.push("")
  lines.push("```text")
  lines.push(self.source_inventory_report())
  lines.push("```")
  lines.push("")
  lines.push("## Findings")
  lines.push("")
  let findings = self.audit()
  for finding in findings {
    lines.push(finding.markdown_line())
  }
  if findings.length() == 0 {
    lines.push("- No findings.")
  }
  lines.push("")
  lines.push("## Recommendations")
  lines.push("")
  let recommendations = self.hardening_recommendations()
  for item in recommendations {
    lines.push(item.markdown_line())
  }
  if recommendations.length() == 0 {
    lines.push("- No recommendations.")
  }
  lines.join("\n")
}

///|
fn join_json_objects(items : Array[String]) -> String {
  let mut out = ""
  for i in 0.. String {
  let finding_rows : Array[String] = []
  let directive_rows : Array[String] = []
  let recommendation_rows : Array[String] = []
  let source_rows : Array[String] = []
  for finding in self.audit() {
    finding_rows.push(finding.json_line())
  }
  for directive in self.directives {
    directive_rows.push(directive.json_line())
  }
  for item in self.hardening_recommendations() {
    recommendation_rows.push(item.json_line())
  }
  for source in self.all_sources() {
    source_rows.push(source.json_line())
  }
  "{\"score\":" +
  self.score().json_report() +
  ",\"directives\":[" +
  join_json_objects(directive_rows) +
  "],\"sources\":[" +
  join_json_objects(source_rows) +
  "],\"findings\":[" +
  join_json_objects(finding_rows) +
  "],\"recommendations\":[" +
  join_json_objects(recommendation_rows) +
  "]}"
}

///|
pub fn Policy::report(self : Policy, flavor : ReportFlavor) -> String {
  match flavor {
    ReportText => self.audit_text_report()
    ReportMarkdown => self.audit_markdown_report()
    ReportJson => self.audit_json_report()
  }
}

///|
pub fn DirectiveChange::text_line(self : DirectiveChange) -> String {
  self.name +
  " before=[" +
  self.before.join(" ") +
  "] after=[" +
  self.after.join(" ") +
  "]"
}

///|
pub fn PolicyDiff::text_report(self : PolicyDiff) -> String {
  let lines : Array[String] = []
  lines.push("Policy Diff")
  lines.push("added=\{self.added.length()}")
  for directive in self.added {
    lines.push("+ " + directive.to_header())
  }
  lines.push("removed=\{self.removed.length()}")
  for directive in self.removed {
    lines.push("- " + directive.to_header())
  }
  lines.push("changed=\{self.changed.length()}")
  for change in self.changed {
    lines.push("~ " + change.text_line())
  }
  lines.join("\n")
}

///|
pub fn PolicyDiff::markdown_report(self : PolicyDiff) -> String {
  let lines : Array[String] = []
  lines.push("## Policy Diff")
  lines.push("")
  lines.push("### Added")
  for directive in self.added {
    lines.push("- `" + directive.to_header() + "`")
  }
  if self.added.length() == 0 {
    lines.push("- None")
  }
  lines.push("")
  lines.push("### Removed")
  for directive in self.removed {
    lines.push("- `" + directive.to_header() + "`")
  }
  if self.removed.length() == 0 {
    lines.push("- None")
  }
  lines.push("")
  lines.push("### Changed")
  for change in self.changed {
    lines.push("- `" + change.text_line() + "`")
  }
  if self.changed.length() == 0 {
    lines.push("- None")
  }
  lines.join("\n")
}

///|
pub fn PolicyDiff::has_breaking_change(self : PolicyDiff) -> Bool {
  for directive in self.removed {
    if directive.name == "default-src" ||
      directive.name == "script-src" ||
      directive.name == "object-src" ||
      directive.name == "base-uri" ||
      directive.name == "frame-ancestors" {
      return true
    }
  }
  for change in self.changed {
    if change.name == "script-src" || change.name == "default-src" {
      return true
    }
  }
  false
}

///|
pub fn PolicyDiff::change_summary(self : PolicyDiff) -> String {
  "added=\{self.added.length()} removed=\{self.removed.length()} changed=\{self.changed.length()} breaking=\{self.has_breaking_change()}"
}