///|
pub(all) enum DiagnosticSeverity {
  DiagnosticInfo
  DiagnosticWarning
  DiagnosticError
} derive(Eq, @debug.Debug)

///|
pub(all) struct DiagnosticEntry {
  severity : DiagnosticSeverity
  code : String
  subject : String
  message : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct DiagnosticReport {
  name : String
  checked : Int
  infos : Int
  warnings : Int
  errors : Int
  entries : Array[DiagnosticEntry]
} derive(Eq, @debug.Debug)

///|
pub fn diagnostic_report(
  name? : StringView = "diagnostics",
) -> DiagnosticReport {
  {
    name: name.to_owned(),
    checked: 0,
    infos: 0,
    warnings: 0,
    errors: 0,
    entries: [],
  }
}

///|
pub fn DiagnosticReport::add(
  self : DiagnosticReport,
  severity : DiagnosticSeverity,
  code : StringView,
  subject : StringView,
  message : StringView,
) -> DiagnosticReport {
  let entries = self.entries.copy()
  entries.push({
    severity,
    code: code.to_owned(),
    subject: subject.to_owned(),
    message: message.to_owned(),
  })
  let (infos, warnings, errors) = match severity {
    DiagnosticInfo => (self.infos + 1, self.warnings, self.errors)
    DiagnosticWarning => (self.infos, self.warnings + 1, self.errors)
    DiagnosticError => (self.infos, self.warnings, self.errors + 1)
  }
  { ..self, checked: self.checked + 1, infos, warnings, errors, entries }
}

///|
pub fn DiagnosticReport::info(
  self : DiagnosticReport,
  code : StringView,
  subject : StringView,
  message : StringView,
) -> DiagnosticReport {
  self.add(DiagnosticInfo, code, subject, message)
}

///|
pub fn DiagnosticReport::warning(
  self : DiagnosticReport,
  code : StringView,
  subject : StringView,
  message : StringView,
) -> DiagnosticReport {
  self.add(DiagnosticWarning, code, subject, message)
}

///|
pub fn DiagnosticReport::error(
  self : DiagnosticReport,
  code : StringView,
  subject : StringView,
  message : StringView,
) -> DiagnosticReport {
  self.add(DiagnosticError, code, subject, message)
}

///|
pub fn DiagnosticReport::merge(
  self : DiagnosticReport,
  other : DiagnosticReport,
) -> DiagnosticReport {
  let mut report = self
  for entry in other.entries {
    report = report.add(
      entry.severity,
      entry.code,
      entry.subject,
      entry.message,
    )
  }
  report
}

///|
pub fn DiagnosticReport::ok(self : DiagnosticReport) -> Bool {
  self.errors == 0
}

///|
pub fn DiagnosticReport::has_warnings(self : DiagnosticReport) -> Bool {
  self.warnings > 0
}

///|
pub fn DiagnosticReport::codes(self : DiagnosticReport) -> Array[String] {
  self.entries.map(entry => entry.code)
}

///|
pub fn DiagnosticReport::summary(self : DiagnosticReport) -> String {
  "diagnostics=\{escape_wire_text(self.name)} checked=\{self.checked} infos=\{self.infos} warnings=\{self.warnings} errors=\{self.errors}"
}

///|
pub fn DiagnosticReport::lines(self : DiagnosticReport) -> Array[String] {
  self.entries.map(entry => entry.to_wire())
}

///|
pub fn DiagnosticReport::manifest_lines(
  self : DiagnosticReport,
) -> Array[String] {
  let lines : Array[String] = []
  lines.push(self.summary())
  for entry in self.entries {
    lines.push(entry.to_wire())
  }
  lines
}

///|
pub fn DiagnosticReport::manifest(self : DiagnosticReport) -> String {
  self.manifest_lines().join("\n")
}

///|
pub fn DiagnosticEntry::to_wire(self : DiagnosticEntry) -> String {
  "severity=\{self.severity.to_wire()};code=\{escape_wire_text(self.code)};subject=\{escape_wire_text(self.subject)};message=\{escape_wire_text(self.message)}"
}

///|
pub fn DiagnosticSeverity::to_wire(self : DiagnosticSeverity) -> String {
  match self {
    DiagnosticInfo => "info"
    DiagnosticWarning => "warning"
    DiagnosticError => "error"
  }
}

///|
pub fn diagnose_bus(name : StringView, bus : Bus) -> DiagnosticReport {
  let audit = bus.audit()
  let stats = bus.delivery_stats()
  let mut report = diagnostic_report(name~)
  if audit.subscriptions == 0 {
    report = report.error(
      "bus.subscriptions", "bus", "bus has no subscriptions",
    )
  } else {
    report = report.info(
      "bus.subscriptions",
      "bus",
      "subscriptions=\{audit.subscriptions}",
    )
  }
  if audit.enabled == 0 {
    report = report.error(
      "bus.enabled", "bus", "bus has no enabled subscriptions",
    )
  } else {
    report = report.info("bus.enabled", "bus", "enabled=\{audit.enabled}")
  }
  if audit.guarded == 0 && audit.subscriptions > 0 {
    report = report.warning(
      "bus.guards", "bus", "no subscription guards configured",
    )
  } else {
    report = report.info("bus.guards", "bus", "guarded=\{audit.guarded}")
  }
  if bus.dead_letter_count() > 0 {
    report = report.warning(
      "bus.dead_letters",
      "bus",
      "dead_letters=\{bus.dead_letter_count()}",
    )
  } else {
    report = report.info("bus.dead_letters", "bus", "dead_letters=0")
  }
  if stats.failure_count() > 0 {
    report = report.warning("bus.delivery_stats", "bus", stats.to_wire())
  } else {
    report = report.info("bus.delivery_stats", "bus", stats.to_wire())
  }
  for issue in audit.issues {
    report = report.warning("bus.audit", "bus", issue)
  }
  report
}

///|
pub fn diagnose_batch(
  name : StringView,
  batch : EventBatch,
) -> DiagnosticReport {
  let mut report = diagnostic_report(name~)
  if batch.is_empty() {
    report = report.warning("batch.events", batch.name, "batch is empty")
  } else {
    report = report.info("batch.events", batch.name, "events=\{batch.len()}")
  }
  let invalid_topics = collect_invalid_event_topics(batch.events)
  if invalid_topics.length() == 0 {
    report = report.info(
      "batch.topics",
      batch.name,
      "all event topics are valid",
    )
  } else {
    for topic in invalid_topics {
      report = report.error("batch.topic.invalid", batch.name, topic)
    }
  }
  let duplicate_count = count_duplicate_fingerprints(batch.events)
  if duplicate_count == 0 {
    report = report.info(
      "batch.fingerprints",
      batch.name,
      "no duplicate fingerprints",
    )
  } else {
    report = report.warning(
      "batch.fingerprints",
      batch.name,
      "duplicate_fingerprints=\{duplicate_count}",
    )
  }
  let topics = batch.topic_counts()
  if topics.length() > 0 {
    report = report.info(
      "batch.topic_count",
      batch.name,
      "topics=\{topics.length()}",
    )
  }
  report
}

///|
pub fn diagnose_batch_schema(
  name : StringView,
  batch : EventBatch,
  schema : EventSchema,
) -> DiagnosticReport {
  let mut report = diagnose_batch(name, batch)
  let validation = batch.validate(schema)
  if validation.invalid == 0 {
    report = report.info("schema.validation", schema.name, validation.summary())
  } else {
    report = report.error(
      "schema.validation",
      schema.name,
      validation.summary(),
    )
    for issue in validation.issues {
      report = report.error("schema.issue", issue.path, issue.message)
    }
  }
  report
}

///|
pub fn diagnose_batch_rules(
  name : StringView,
  batch : EventBatch,
  rules : RuleSet,
) -> DiagnosticReport {
  let mut report = diagnose_batch(name, batch)
  let decisions = batch.evaluate_rules(rules)
  if decisions.rejected == 0 {
    report = report.info("rules.evaluation", rules.name, decisions.summary())
  } else {
    report = report.warning("rules.evaluation", rules.name, decisions.summary())
    for decision in decisions.decisions {
      if decision.decision is Reject(reason) {
        report = report.warning("rules.rejected", decision.event_id, reason)
      }
    }
  }
  report
}

///|
pub fn diagnose_snapshot(
  name : StringView,
  snapshot : EventSnapshot,
) -> DiagnosticReport {
  let mut report = diagnostic_report(name~)
  if snapshot.event_count == 0 {
    report = report.warning(
      "snapshot.events",
      snapshot.name,
      "snapshot is empty",
    )
  } else {
    report = report.info(
      "snapshot.events",
      snapshot.name,
      "events=\{snapshot.event_count}",
    )
  }
  if snapshot.topic_counts.length() == 0 {
    report = report.warning(
      "snapshot.topics",
      snapshot.name,
      "no topics recorded",
    )
  } else {
    report = report.info(
      "snapshot.topics",
      snapshot.name,
      "topics=\{snapshot.topic_counts.length()}",
    )
  }
  if snapshot.dead_letter_count > 0 {
    report = report.warning(
      "snapshot.dead_letters",
      snapshot.name,
      "dead_letters=\{snapshot.dead_letter_count}",
    )
  } else {
    report = report.info(
      "snapshot.dead_letters",
      snapshot.name,
      "dead_letters=0",
    )
  }
  if snapshot.delivery_stats.failure_count() > 0 {
    report = report.warning(
      "snapshot.delivery_stats",
      snapshot.name,
      snapshot.delivery_stats.to_wire(),
    )
  } else {
    report = report.info(
      "snapshot.delivery_stats",
      snapshot.name,
      snapshot.delivery_stats.to_wire(),
    )
  }
  report
}

///|
pub fn diagnose_pipeline_run(
  name : StringView,
  run : PipelineRun,
) -> DiagnosticReport {
  let mut report = diagnostic_report(name~)
  if run.stages.length() == 0 {
    report = report.warning(
      "pipeline.stages",
      run.name,
      "pipeline has no stages",
    )
  } else {
    report = report.info(
      "pipeline.stages",
      run.name,
      "stages=\{run.stages.length()}",
    )
  }
  for stage in run.stages {
    match stage.status {
      StagePassed =>
        report = report.info(
          "pipeline.stage",
          "stage:\{stage.index}",
          stage.summary,
        )
      StageWarning =>
        report = report.warning(
          "pipeline.stage",
          "stage:\{stage.index}",
          stage.summary,
        )
      StageFailed =>
        report = report.error(
          "pipeline.stage",
          "stage:\{stage.index}",
          stage.summary,
        )
    }
  }
  report
}

///|
pub fn diagnose_retention(
  name : StringView,
  retention : RetentionReport,
) -> DiagnosticReport {
  let mut report = diagnostic_report(name~)
  if retention.scanned == 0 {
    report = report.warning(
      "retention.scanned",
      retention.policy,
      "no events scanned",
    )
  } else {
    report = report.info(
      "retention.scanned",
      retention.policy,
      "scanned=\{retention.scanned}",
    )
  }
  if retention.kept == 0 && retention.scanned > 0 {
    report = report.warning(
      "retention.kept",
      retention.policy,
      "no events retained",
    )
  } else {
    report = report.info(
      "retention.kept",
      retention.policy,
      "kept=\{retention.kept}",
    )
  }
  if retention.dropped > 0 {
    report = report.info(
      "retention.dropped",
      retention.policy,
      "dropped=\{retention.dropped}",
    )
  } else {
    report = report.info("retention.dropped", retention.policy, "dropped=0")
  }
  for topic in retention.topic_summaries {
    if topic.kept == 0 && topic.dropped > 0 {
      report = report.warning("retention.topic", topic.topic, topic.to_wire())
    } else {
      report = report.info("retention.topic", topic.topic, topic.to_wire())
    }
  }
  report
}

///|
fn collect_invalid_event_topics(events : Array[Envelope]) -> Array[String] {
  let invalid : Array[String] = []
  for event in events {
    match topic_segments(event.topic) {
      Ok(_) => ()
      Err(err) =>
        invalid.push(
          "event=\{escape_wire_text(event.id)};topic=\{escape_wire_text(event.topic)};message=\{escape_wire_text(err.message())}",
        )
    }
  }
  invalid
}

///|
fn count_duplicate_fingerprints(events : Array[Envelope]) -> Int {
  let seen : Array[String] = []
  let duplicates : Array[String] = []
  for event in events {
    let fingerprint = event.fingerprint()
    if seen.contains(fingerprint) {
      if !duplicates.contains(fingerprint) {
        duplicates.push(fingerprint)
      }
    } else {
      seen.push(fingerprint)
    }
  }
  duplicates.length()
}