///|
/// Aggregated operational metrics contain no original text, only counts and
/// checksums suitable for dashboards.
pub(all) struct DeidAnalytics {
  document_id : String
  input_length : Int
  output_length : Int
  finding_count : Int
  applied_count : Int
  risk_score : Int
  kind_counts : Map[String, Int]
  risk_counts : Map[String, Int]
  profile : String
  checksum : String
} derive(Debug, Eq)

///|
pub(all) struct AnalyticsSummary {
  documents : Int
  total_input_length : Int
  total_output_length : Int
  total_findings : Int
  total_applied : Int
  average_risk : Float
  kind_counts : Map[String, Int]
  risk_counts : Map[String, Int]
  checksum : String
} derive(Debug)

///|
pub(all) struct AnalyticsAccumulator {
  mut records : Array[DeidAnalytics]
  mut sequence : Int
} derive(Debug)

///|
pub fn analytics_from_result(
  document_id : String,
  result : DeidResult,
  profile : String,
) -> DeidAnalytics {
  let risk_score = result.findings.fold(init=0, (sum, finding) => {
    let value = match risk_level(finding) {
      RiskLevel::Low => 10
      RiskLevel::Medium => 30
      RiskLevel::High => 60
      RiskLevel::Critical => 100
    }
    sum + value
  })
  let kind_counts : Map[String, Int] = Map([])
  let risk_counts : Map[String, Int] = Map([])
  for finding in result.findings {
    let kind = phi_kind_name(finding.kind)
    let risk = "\{risk_level(finding)}"
    kind_counts[kind] = kind_counts.get_or_default(kind, 0) + 1
    risk_counts[risk] = risk_counts.get_or_default(risk, 0) + 1
  }
  let checksum = stable_hash(document_id + ":" + result.text)
  {
    document_id,
    input_length: result.audit.input_length,
    output_length: result.audit.output_length,
    finding_count: result.findings.length(),
    applied_count: result.offsets.length(),
    risk_score,
    kind_counts,
    risk_counts,
    profile,
    checksum,
  }
}

///|
pub fn DeidAnalytics::redaction_percent(self : DeidAnalytics) -> Int {
  if self.input_length == 0 {
    0
  } else {
    self.applied_count * 100 / self.input_length
  }
}

///|
pub fn DeidAnalytics::average_finding_size(self : DeidAnalytics) -> Float {
  if self.finding_count == 0 {
    0.0
  } else {
    Float::from_int(self.input_length - self.output_length) /
    Float::from_int(self.finding_count)
  }
}

///|
pub fn DeidAnalytics::has_critical(self : DeidAnalytics) -> Bool {
  self.risk_counts.get_or_default("critical", 0) > 0
}

///|
pub fn DeidAnalytics::summary(self : DeidAnalytics) -> String {
  [
    "document_id=\{self.document_id}",
    "profile=\{self.profile}",
    "input_length=\{self.input_length}",
    "output_length=\{self.output_length}",
    "findings=\{self.finding_count}",
    "applied=\{self.applied_count}",
    "risk_score=\{self.risk_score}",
    "critical=\{self.has_critical()}",
    "checksum=\{self.checksum}",
  ].join("\n")
}

///|
pub fn DeidAnalytics::to_json(self : DeidAnalytics) -> String {
  "{" +
  "\"document_id\":\{json_escape(self.document_id)}," +
  "\"profile\":\{json_escape(self.profile)}," +
  "\"input_length\":\{self.input_length}," +
  "\"output_length\":\{self.output_length}," +
  "\"findings\":\{self.finding_count}," +
  "\"applied\":\{self.applied_count}," +
  "\"risk_score\":\{self.risk_score}," +
  "\"critical\":\{self.has_critical()}," +
  "\"checksum\":\{json_escape(self.checksum)}" +
  "}"
}

///|
pub fn AnalyticsAccumulator::new() -> AnalyticsAccumulator {
  { records: [], sequence: 0 }
}

///|
pub fn AnalyticsAccumulator::add(
  self : AnalyticsAccumulator,
  value : DeidAnalytics,
) -> Unit {
  self.records.push(value)
  self.sequence += 1
}

///|
pub fn AnalyticsAccumulator::size(self : AnalyticsAccumulator) -> Int {
  self.records.length()
}

///|
pub fn AnalyticsAccumulator::find(
  self : AnalyticsAccumulator,
  document_id : String,
) -> DeidAnalytics? {
  let mut result : DeidAnalytics? = None
  for record in self.records {
    if record.document_id == document_id {
      result = Some(record)
    }
  }
  result
}

///|
pub fn AnalyticsAccumulator::profiles(
  self : AnalyticsAccumulator,
) -> Array[String] {
  let values : Map[String, Unit] = Map([])
  for record in self.records {
    values[record.profile] = ()
  }
  values.keys().to_array()
}

///|
pub fn AnalyticsAccumulator::summary(
  self : AnalyticsAccumulator,
) -> AnalyticsSummary {
  let kind_counts : Map[String, Int] = Map([])
  let risk_counts : Map[String, Int] = Map([])
  let mut input_length = 0
  let mut output_length = 0
  let mut findings = 0
  let mut applied = 0
  let mut risk = 0
  for record in self.records {
    input_length += record.input_length
    output_length += record.output_length
    findings += record.finding_count
    applied += record.applied_count
    risk += record.risk_score
    merge_counts(kind_counts, record.kind_counts)
    merge_counts(risk_counts, record.risk_counts)
  }
  {
    documents: self.records.length(),
    total_input_length: input_length,
    total_output_length: output_length,
    total_findings: findings,
    total_applied: applied,
    average_risk: if self.records.is_empty() {
      0.0
    } else {
      Float::from_int(risk) / Float::from_int(self.records.length())
    },
    kind_counts,
    risk_counts,
    checksum: self.checksum(),
  }
}

///|
pub fn AnalyticsAccumulator::checksum(self : AnalyticsAccumulator) -> String {
  stable_hash(self.records.map(fn(record) { record.checksum }).join("\n"))
}

///|
pub fn AnalyticsAccumulator::critical_documents(
  self : AnalyticsAccumulator,
) -> Array[String] {
  self.records
  .filter(DeidAnalytics::has_critical)
  .map(fn(record) { record.document_id })
}

///|
pub fn AnalyticsAccumulator::high_volume_documents(
  self : AnalyticsAccumulator,
  threshold : Int,
) -> Array[String] {
  self.records
  .filter(fn(record) { record.finding_count >= threshold })
  .map(fn(record) { record.document_id })
}

///|
pub fn AnalyticsAccumulator::risk_distribution(
  self : AnalyticsAccumulator,
) -> Map[String, Int] {
  let result : Map[String, Int] = Map([])
  for record in self.records {
    merge_counts(result, record.risk_counts)
  }
  result
}

///|
pub fn analytics_summary_text(summary : AnalyticsSummary) -> String {
  [
    "documents=\{summary.documents}",
    "input=\{summary.total_input_length}",
    "output=\{summary.total_output_length}",
    "findings=\{summary.total_findings}",
    "applied=\{summary.total_applied}",
    "average_risk=\{summary.average_risk}",
    "kinds=\{map_to_json(summary.kind_counts)}",
    "risks=\{map_to_json(summary.risk_counts)}",
    "checksum=\{summary.checksum}",
  ].join("\n")
}

///|
pub fn analytics_summary_json(summary : AnalyticsSummary) -> String {
  "{" +
  "\"documents\":\{summary.documents}," +
  "\"total_input_length\":\{summary.total_input_length}," +
  "\"total_output_length\":\{summary.total_output_length}," +
  "\"total_findings\":\{summary.total_findings}," +
  "\"total_applied\":\{summary.total_applied}," +
  "\"average_risk\":\{summary.average_risk}," +
  "\"kind_counts\":\{map_to_json(summary.kind_counts)}," +
  "\"risk_counts\":\{map_to_json(summary.risk_counts)}," +
  "\"checksum\":\{json_escape(summary.checksum)}" +
  "}"
}

///|
pub fn analytics_kind_ratio(summary : AnalyticsSummary, kind : String) -> Float {
  if summary.total_findings == 0 {
    0.0
  } else {
    Float::from_int(summary.kind_counts.get_or_default(kind, 0)) /
    Float::from_int(summary.total_findings)
  }
}

///|
pub fn analytics_risk_ratio(summary : AnalyticsSummary, risk : String) -> Float {
  if summary.total_findings == 0 {
    0.0
  } else {
    Float::from_int(summary.risk_counts.get_or_default(risk, 0)) /
    Float::from_int(summary.total_findings)
  }
}

///|
pub fn analytics_reduction_percent(summary : AnalyticsSummary) -> Int {
  if summary.total_input_length == 0 {
    0
  } else {
    (summary.total_input_length - summary.total_output_length) *
    100 /
    summary.total_input_length
  }
}

///|
pub fn analytics_average_findings(summary : AnalyticsSummary) -> Float {
  if summary.documents == 0 {
    0.0
  } else {
    Float::from_int(summary.total_findings) / Float::from_int(summary.documents)
  }
}

///|
pub fn analytics_ready(summary : AnalyticsSummary) -> Bool {
  summary.documents > 0 && summary.total_applied <= summary.total_findings
}

///|
pub fn AnalyticsAccumulator::to_json(self : AnalyticsAccumulator) -> String {
  let summary = self.summary()
  "{" +
  "\"summary\":\{analytics_summary_json(summary)}," +
  "\"records\":[" +
  self.records.map(DeidAnalytics::to_json).join(",") +
  "]}"
}

///|
pub fn AnalyticsAccumulator::to_csv(self : AnalyticsAccumulator) -> String {
  let lines = [
    "document_id,profile,input_length,output_length,findings,applied,risk_score,checksum",
  ]
  for record in self.records {
    lines.push(
      [
        csv_cell(record.document_id),
        csv_cell(record.profile),
        "\{record.input_length}",
        "\{record.output_length}",
        "\{record.finding_count}",
        "\{record.applied_count}",
        "\{record.risk_score}",
        record.checksum,
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
pub fn analytics_merge(
  left : AnalyticsAccumulator,
  right : AnalyticsAccumulator,
) -> AnalyticsAccumulator {
  let result = AnalyticsAccumulator::new()
  for record in left.records {
    result.add(record)
  }
  for record in right.records {
    result.add(record)
  }
  result
}

///|
pub fn analytics_consistent(summary : AnalyticsSummary) -> Bool {
  summary.total_applied <= summary.total_findings &&
  summary.total_output_length >= 0 &&
  summary.total_input_length >= 0
}

///|
pub fn analytics_record_checksum(
  document_id : String,
  output : String,
) -> String {
  stable_hash(document_id + "\u{1f}" + output)
}