///|
/// Operational metrics for measuring de-identification quality and privacy
/// posture. These values are computed from pipeline artifacts rather than
/// declared as targets, so dashboards can distinguish observations from goals.
pub(all) enum PrivacyMetricKind {
  MetricDocuments
  MetricCharacters
  MetricFindings
  MetricRedactions
  MetricReviews
  MetricFailures
  MetricLatencyMs
  MetricOutputBytes
} derive(Debug, Eq)

///|
pub(all) enum MetricAggregation {
  MetricSum
  MetricAverage
  MetricMinimum
  MetricMaximum
  MetricLast
} derive(Debug, Eq)

///|
pub(all) struct PrivacyMetricSample {
  metric_id : String
  batch_id : String
  kind : PrivacyMetricKind
  value : Int
  unit : String
  recorded_at : String
  source_checksum : String
  labels : Map[String, String]
} derive(Debug)

///|
pub(all) struct MetricWindow {
  start_at : String
  end_at : String
  mut samples : Array[PrivacyMetricSample]
  mut checksum : String
} derive(Debug)

///|
pub(all) struct PrivacyMetricSummary {
  kind : PrivacyMetricKind
  aggregation : MetricAggregation
  count : Int
  value : Int
  minimum : Int
  maximum : Int
  source_checksum : String
} derive(Debug, Eq)

///|
pub fn privacy_metric_kind_name(kind : PrivacyMetricKind) -> String {
  match kind {
    MetricDocuments => "documents"
    MetricCharacters => "characters"
    MetricFindings => "findings"
    MetricRedactions => "redactions"
    MetricReviews => "reviews"
    MetricFailures => "failures"
    MetricLatencyMs => "latency_ms"
    MetricOutputBytes => "output_bytes"
  }
}

///|
pub fn metric_aggregation_name(aggregation : MetricAggregation) -> String {
  match aggregation {
    MetricSum => "sum"
    MetricAverage => "average"
    MetricMinimum => "minimum"
    MetricMaximum => "maximum"
    MetricLast => "last"
  }
}

///|
pub fn privacy_metric_unit(kind : PrivacyMetricKind) -> String {
  match kind {
    MetricDocuments
    | MetricFindings
    | MetricRedactions
    | MetricReviews
    | MetricFailures => "count"
    MetricCharacters | MetricOutputBytes => "bytes"
    MetricLatencyMs => "milliseconds"
  }
}

///|
pub fn privacy_metric_sample(
  batch_id : String,
  kind : PrivacyMetricKind,
  value : Int,
  recorded_at : String,
) -> PrivacyMetricSample {
  let safe_value = if value < 0 { 0 } else { value }
  {
    metric_id: stable_hash(
      batch_id + ":" + privacy_metric_kind_name(kind) + ":" + recorded_at,
    ),
    batch_id,
    kind,
    value: safe_value,
    unit: privacy_metric_unit(kind),
    recorded_at,
    source_checksum: "",
    labels: Map([]),
  }
}

///|
pub fn PrivacyMetricSample::with_source(
  sample : PrivacyMetricSample,
  source_checksum : String,
) -> PrivacyMetricSample {
  { ..sample, source_checksum, }
}

///|
pub fn PrivacyMetricSample::with_label(
  sample : PrivacyMetricSample,
  key : String,
  value : String,
) -> PrivacyMetricSample {
  sample.labels[key] = value
  sample
}

///|
pub fn PrivacyMetricSample::is_valid(self : PrivacyMetricSample) -> Bool {
  self.metric_id.length() > 0 &&
  self.batch_id.length() > 0 &&
  self.recorded_at.length() > 0 &&
  self.value >= 0 &&
  self.unit == privacy_metric_unit(self.kind)
}

///|
pub fn metric_window(start_at : String, end_at : String) -> MetricWindow {
  {
    start_at,
    end_at,
    samples: [],
    checksum: stable_hash(start_at + ":" + end_at),
  }
}

///|
pub fn MetricWindow::add(
  self : MetricWindow,
  sample : PrivacyMetricSample,
) -> Bool {
  if sample.is_valid() {
    self.samples.push(sample)
    self.checksum = stable_hash(
      self.samples.map(fn(item) { item.metric_id }).join("\n"),
    )
    true
  } else {
    false
  }
}

///|
pub fn MetricWindow::for_kind(
  self : MetricWindow,
  kind : PrivacyMetricKind,
) -> Array[PrivacyMetricSample] {
  self.samples.filter(fn(sample) { sample.kind == kind })
}

///|
pub fn MetricWindow::for_batch(
  self : MetricWindow,
  batch_id : String,
) -> Array[PrivacyMetricSample] {
  self.samples.filter(fn(sample) { sample.batch_id == batch_id })
}

///|
pub fn MetricWindow::sample_count(self : MetricWindow) -> Int {
  self.samples.length()
}

///|
pub fn MetricWindow::is_valid(self : MetricWindow) -> Bool {
  self.start_at.length() > 0 &&
  self.end_at.length() > 0 &&
  self.samples.all(PrivacyMetricSample::is_valid)
}

///|
pub fn MetricWindow::batches(self : MetricWindow) -> Array[String] {
  let seen : Map[String, Bool] = Map([])
  let result : Array[String] = []
  for sample in self.samples {
    if !seen.contains(sample.batch_id) {
      seen[sample.batch_id] = true
      result.push(sample.batch_id)
    }
  }
  result
}

///|
fn metric_minimum(values : Array[Int]) -> Int {
  match values.get(0) {
    None => 0
    Some(first) =>
      values.fold(init=first, (best, value) => {
        if value < best {
          value
        } else {
          best
        }
      })
  }
}

///|
fn metric_maximum(values : Array[Int]) -> Int {
  match values.get(0) {
    None => 0
    Some(first) =>
      values.fold(init=first, (best, value) => {
        if value > best {
          value
        } else {
          best
        }
      })
  }
}

///|
fn metric_total(values : Array[Int]) -> Int {
  values.fold(init=0, (sum, value) => sum + value)
}

///|
pub fn summarize_metric(
  samples : Array[PrivacyMetricSample],
  kind : PrivacyMetricKind,
  aggregation : MetricAggregation,
) -> PrivacyMetricSummary {
  let selected = samples.filter(fn(sample) { sample.kind == kind })
  let values = selected.map(fn(sample) { sample.value })
  let count = values.length()
  let total = metric_total(values)
  let value = match aggregation {
    MetricSum => total
    MetricAverage => if count == 0 { 0 } else { total / count }
    MetricMinimum => metric_minimum(values)
    MetricMaximum => metric_maximum(values)
    MetricLast =>
      match values.get(count - 1) {
        Some(item) => item
        None => 0
      }
  }
  {
    kind,
    aggregation,
    count,
    value,
    minimum: metric_minimum(values),
    maximum: metric_maximum(values),
    source_checksum: stable_hash(
      selected.map(fn(sample) { sample.source_checksum }).join("\n"),
    ),
  }
}

///|
pub fn MetricWindow::summary(
  self : MetricWindow,
  kind : PrivacyMetricKind,
  aggregation : MetricAggregation,
) -> PrivacyMetricSummary {
  summarize_metric(self.samples, kind, aggregation)
}

///|
pub fn metric_rate(numerator : Int, denominator : Int) -> Int {
  if denominator <= 0 || numerator <= 0 {
    0
  } else {
    numerator * 100 / denominator
  }
}

///|
pub fn detection_recall_proxy(detected : Int, reviewed_positive : Int) -> Int {
  metric_rate(detected, reviewed_positive)
}

///|
pub fn review_precision_proxy(accepted : Int, reviewed : Int) -> Int {
  metric_rate(accepted, reviewed)
}

///|
pub fn output_reduction_ratio(input_bytes : Int, output_bytes : Int) -> Int {
  if input_bytes <= 0 || output_bytes >= input_bytes {
    0
  } else {
    (input_bytes - output_bytes) * 100 / input_bytes
  }
}

///|
pub fn privacy_budget_consumed(
  redactions : Int,
  high_risk_findings : Int,
  manual_overrides : Int,
) -> Int {
  let safe_redactions = if redactions < 0 { 0 } else { redactions }
  let safe_high = if high_risk_findings < 0 { 0 } else { high_risk_findings }
  let safe_overrides = if manual_overrides < 0 { 0 } else { manual_overrides }
  safe_redactions + safe_high * 5 + safe_overrides * 10
}

///|
pub fn privacy_risk_score(
  findings : Int,
  unresolved : Int,
  protected_skips : Int,
) -> Int {
  let safe_findings = if findings < 0 { 0 } else { findings }
  let safe_unresolved = if unresolved < 0 { 0 } else { unresolved }
  let safe_skips = if protected_skips < 0 { 0 } else { protected_skips }
  let denominator = if safe_findings == 0 { 1 } else { safe_findings }
  let score = (safe_unresolved * 100 + safe_skips * 25) / denominator
  if score > 100 {
    100
  } else {
    score
  }
}

///|
pub fn privacy_metric_health(
  documents : Int,
  failures : Int,
  pending_reviews : Int,
  latency_ms : Int,
) -> Int {
  if documents <= 0 {
    0
  } else {
    let failure_penalty = metric_rate(failures, documents)
    let review_penalty = metric_rate(pending_reviews, documents)
    let latency_penalty = if latency_ms > 10000 {
      20
    } else if latency_ms > 3000 {
      10
    } else {
      0
    }
    let score = 100 - failure_penalty - review_penalty / 2 - latency_penalty
    if score < 0 {
      0
    } else {
      score
    }
  }
}

///|
pub fn window_metric_table(window : MetricWindow) -> Map[String, Int] {
  let table : Map[String, Int] = Map([])
  for
    kind in [
      MetricDocuments,
      MetricCharacters,
      MetricFindings,
      MetricRedactions,
      MetricReviews,
      MetricFailures,
      MetricLatencyMs,
      MetricOutputBytes,
    ] {
    let summary = window.summary(kind, MetricSum)
    table[privacy_metric_kind_name(kind)] = summary.value
  }
  table
}

///|
pub fn MetricWindow::health(self : MetricWindow) -> Int {
  let table = window_metric_table(self)
  privacy_metric_health(
    table.get_or_default("documents", 0),
    table.get_or_default("failures", 0),
    table.get_or_default("reviews", 0),
    table.get_or_default("latency_ms", 0),
  )
}

///|
pub fn MetricWindow::to_json(self : MetricWindow) -> String {
  let table = window_metric_table(self)
  "{" +
  "\"start_at\":\"" +
  json_escape(self.start_at) +
  "\"," +
  "\"end_at\":\"" +
  json_escape(self.end_at) +
  "\"," +
  "\"samples\":" +
  self.samples.length().to_string() +
  "," +
  "\"documents\":" +
  table.get_or_default("documents", 0).to_string() +
  "," +
  "\"findings\":" +
  table.get_or_default("findings", 0).to_string() +
  "," +
  "\"redactions\":" +
  table.get_or_default("redactions", 0).to_string() +
  "," +
  "\"health\":" +
  self.health().to_string() +
  "," +
  "\"checksum\":\"" +
  json_escape(self.checksum) +
  "\"}"
}

///|
pub fn merge_metric_windows(
  left : MetricWindow,
  right : MetricWindow,
) -> MetricWindow {
  let merged = metric_window(left.start_at, right.end_at)
  for sample in left.samples {
    merged.samples.push(sample)
  }
  for sample in right.samples {
    merged.samples.push(sample)
  }
  merged.checksum = stable_hash(
    merged.samples.map(fn(item) { item.metric_id }).join("\n"),
  )
  merged
}

///|
pub fn metric_window_has_source(
  window : MetricWindow,
  source_checksum : String,
) -> Bool {
  window.samples.any(fn(sample) { sample.source_checksum == source_checksum })
}

///|
pub fn metric_sample_keys(window : MetricWindow) -> Array[String] {
  window.samples.map(fn(sample) {
    sample.batch_id +
    ":" +
    privacy_metric_kind_name(sample.kind) +
    ":" +
    sample.recorded_at
  })
}

///|
pub fn metric_window_is_monotonic(
  before : MetricWindow,
  after : MetricWindow,
) -> Bool {
  after.samples.length() >= before.samples.length() &&
  before.samples.all(fn(sample) {
    metric_window_has_source(after, sample.source_checksum) ||
    sample.source_checksum.is_empty()
  })
}

///|
pub fn privacy_metrics_csv(window : MetricWindow) -> String {
  let header = "metric_id,batch_id,kind,value,unit,recorded_at,source_checksum"
  let rows = window.samples.map(fn(sample) {
    [
      sample.metric_id,
      sample.batch_id,
      privacy_metric_kind_name(sample.kind),
      sample.value.to_string(),
      sample.unit,
      sample.recorded_at,
      sample.source_checksum,
    ]
    .map(metric_csv_escape)
    .join(",")
  })
  ([header] + rows).join("\n")
}

///|
fn metric_csv_escape(value : String) -> String {
  if value.contains(",") || value.contains("\"") || value.contains("\n") {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}