///|
/// The operation of a destination that failed.
pub(all) enum SinkOperation {
  /// Create the log directory.
  CreateDirectory
  /// Create the ownership marker of a file set.
  AcquireOwnership
  /// List the log directory.
  ListDirectory
  /// Plan the next file of a file set.
  PlanFile
  /// Open a log file.
  OpenFile
  /// Write a record.
  Write
  /// Close a log file.
  CloseFile
  /// Delete the ownership marker after a normal close.
  ReleaseOwnership
  /// Delete an old file of the file set.
  DeleteOldFile
  /// Wait for the accepted records in a final drain.
  Drain
} derive(Eq, Debug)

///|
pub extend SinkOperation with Eq::{equal, not_equal}

///|
pub extend SinkOperation with @debug.Debug::{to_repr}

///|
/// The operation name for people, for example `write`.
pub impl Show for SinkOperation with output(self, logger) {
  logger.write_string(
    match self {
      CreateDirectory => "create directory"
      AcquireOwnership => "acquire ownership"
      ListDirectory => "list directory"
      PlanFile => "plan file"
      OpenFile => "open file"
      Write => "write"
      CloseFile => "close file"
      ReleaseOwnership => "release ownership"
      DeleteOldFile => "delete old file"
      Drain => "drain"
    },
  )
}

///|
pub extend SinkOperation with Show::{output, to_string}

///|
/// The destination of a failure or a count.
pub(all) enum SinkTarget {
  /// A console stream.
  Console(ConsoleStream)
  /// A file set in a directory.
  Files(LogDirectory, @rolling.FileSet)
} derive(Eq, Debug)

///|
pub extend SinkTarget with Eq::{equal, not_equal}

///|
pub extend SinkTarget with @debug.Debug::{to_repr}

///|
/// The target for people, for example `stderr` or `files /var/log/acme.log.lock`.
pub impl Show for SinkTarget with output(self, logger) {
  match self {
    Console(stream) => logger.write_string(stream.to_string())
    Files(directory, files) =>
      logger.write_string("files \{directory.marker(files)}")
  }
}

///|
pub extend SinkTarget with Show::{output, to_string}

///|
/// A report value that breaks a rule.
pub suberror ReportError {
  /// A count is less than 0.
  NegativeCount(count~ : Int64)
  /// A report says that a drain timed out, but the session is not closed. Only
  /// close has a drain time limit.
  TimeoutBeforeClose
} derive(Debug)

///|
pub extend ReportError with @debug.Debug::{to_repr}

///|
/// Text for people that names the input and the broken rule.
pub fn ReportError::message(self : ReportError) -> String {
  match self {
    NegativeCount(count~) => "Invalid count \{count}: it must be 0 or more"
    TimeoutBeforeClose => "A drain timeout needs a closed session"
  }
}

///|
fn check_counts(counts : Array[Int64]) -> Unit raise ReportError {
  for count in counts {
    guard count >= 0L else { raise NegativeCount(count~) }
  }
}

///|
/// One failure of a destination: the operation, the target, the path or name
/// that the operation used, if any, and the text of the error.
pub struct SinkFailure {
  priv operation : SinkOperation
  priv target : SinkTarget
  priv subject : String?
  priv detail : String
} derive(Eq, Debug)

///|
pub extend SinkFailure with Eq::{equal, not_equal}

///|
pub extend SinkFailure with @debug.Debug::{to_repr}

///|
/// Make a failure report. The runtime and test adapters make these.
pub fn SinkFailure::new(
  operation : SinkOperation,
  target : SinkTarget,
  subject? : String,
  detail : String,
) -> SinkFailure {
  { operation, target, subject, detail }
}

///|
/// The operation that failed.
pub fn SinkFailure::operation(self : SinkFailure) -> SinkOperation {
  self.operation
}

///|
/// The destination of the operation.
pub fn SinkFailure::target(self : SinkFailure) -> SinkTarget {
  self.target
}

///|
/// The path or file name that the operation used, if any.
pub fn SinkFailure::subject(self : SinkFailure) -> String? {
  self.subject
}

///|
/// The text of the error.
pub fn SinkFailure::detail(self : SinkFailure) -> String {
  self.detail
}

///|
/// Text for people, for example
/// `logging: write failed for files /logs/acme.log.lock (acme-20261027.log): disk full`.
pub fn SinkFailure::message(self : SinkFailure) -> String {
  let subject = match self.subject {
    Some(text) => " (\{text})"
    None => ""
  }
  "logging: \{self.operation} failed for \{self.target}\{subject}: \{self.detail}"
}

///|
/// The counts and the latest failures of one destination.
pub struct DestinationReport {
  priv target : SinkTarget
  priv written : Int64
  priv undelivered : Int64
  priv failure : SinkFailure?
  priv retention_failure : SinkFailure?
} derive(Eq, Debug)

///|
pub extend DestinationReport with Eq::{equal, not_equal}

///|
pub extend DestinationReport with @debug.Debug::{to_repr}

///|
/// Make a destination report. The runtime and test adapters make these. It
/// raises `NegativeCount` for a count less than 0.
pub fn DestinationReport::new(
  target : SinkTarget,
  written~ : Int64,
  undelivered~ : Int64,
  failure? : SinkFailure,
  retention_failure? : SinkFailure,
) -> DestinationReport raise ReportError {
  check_counts([written, undelivered])
  { target, written, undelivered, failure, retention_failure }
}

///|
/// The destination.
pub fn DestinationReport::target(self : DestinationReport) -> SinkTarget {
  self.target
}

///|
/// The count of records that the destination wrote.
pub fn DestinationReport::written(self : DestinationReport) -> Int64 {
  self.written
}

///|
/// The count of accepted records for this destination that it did not write,
/// because the destination failed or the final drain ended first.
pub fn DestinationReport::undelivered(self : DestinationReport) -> Int64 {
  self.undelivered
}

///|
/// The failure that stopped the destination, if any. The destination writes no
/// more records after it.
pub fn DestinationReport::failure(self : DestinationReport) -> SinkFailure? {
  self.failure
}

///|
/// The latest retention or ownership-release failure, if any. It does not stop
/// the destination.
pub fn DestinationReport::retention_failure(
  self : DestinationReport,
) -> SinkFailure? {
  self.retention_failure
}

///|
/// The counts of records that the session did not accept, one for each reason.
pub struct DropCounts {
  priv queue_full : Int64
  priv oversized : Int64
  priv closed : Int64
  priv reentrant : Int64
} derive(Eq, Debug)

///|
pub extend DropCounts with Eq::{equal, not_equal}

///|
pub extend DropCounts with @debug.Debug::{to_repr}

///|
/// Make drop counts. The runtime and test adapters make these. It raises
/// `NegativeCount` for a count less than 0.
pub fn DropCounts::new(
  queue_full~ : Int64,
  oversized~ : Int64,
  closed~ : Int64,
  reentrant~ : Int64,
) -> DropCounts raise ReportError {
  check_counts([queue_full, oversized, closed, reentrant])
  { queue_full, oversized, closed, reentrant }
}

///|
/// Records discarded because a queue limit was full.
pub fn DropCounts::queue_full(self : DropCounts) -> Int64 {
  self.queue_full
}

///|
/// Records discarded because they were larger than the record limit.
pub fn DropCounts::oversized(self : DropCounts) -> Int64 {
  self.oversized
}

///|
/// Events received after the session closed.
pub fn DropCounts::closed(self : DropCounts) -> Int64 {
  self.closed
}

///|
/// Events received while the session logger or its failure observer ran, for
/// example a log call from the `Show` of an error or from the observer.
pub fn DropCounts::reentrant(self : DropCounts) -> Int64 {
  self.reentrant
}

///|
/// The sum of all drop counts.
pub fn DropCounts::total(self : DropCounts) -> Int64 {
  self.queue_full + self.oversized + self.closed + self.reentrant
}

///|
/// An immutable snapshot of a session: accepted records, drops, the result of
/// each destination, and whether a drain used its full time limit.
pub struct SessionReport {
  priv accepted : Int64
  priv dropped : DropCounts
  priv destinations : Array[DestinationReport]
  priv drain_timed_out : Bool
  priv closed : Bool
} derive(Eq, Debug)

///|
pub extend SessionReport with Eq::{equal, not_equal}

///|
pub extend SessionReport with @debug.Debug::{to_repr}

///|
/// Make a session report. The runtime and test adapters make these. The report
/// keeps a copy of `destinations`. It raises `NegativeCount` for a count less
/// than 0, and `TimeoutBeforeClose` for a drain timeout of an open session.
pub fn SessionReport::new(
  accepted~ : Int64,
  dropped~ : DropCounts,
  destinations~ : Array[DestinationReport],
  drain_timed_out~ : Bool,
  closed~ : Bool,
) -> SessionReport raise ReportError {
  check_counts([accepted])
  guard !drain_timed_out || closed else { raise TimeoutBeforeClose }
  {
    accepted,
    dropped,
    destinations: destinations.copy(),
    drain_timed_out,
    closed,
  }
}

///|
/// The count of accepted records.
pub fn SessionReport::accepted(self : SessionReport) -> Int64 {
  self.accepted
}

///|
/// The counts of records that the session did not accept.
pub fn SessionReport::dropped(self : SessionReport) -> DropCounts {
  self.dropped
}

///|
/// The report of each destination, console first, then files in settings
/// order. The array is a copy.
pub fn SessionReport::destinations(
  self : SessionReport,
) -> Array[DestinationReport] {
  self.destinations.copy()
}

///|
/// Whether a drain ended at its time limit before all accepted records were
/// written.
pub fn SessionReport::drain_timed_out(self : SessionReport) -> Bool {
  self.drain_timed_out
}

///|
/// Whether the session is closed.
pub fn SessionReport::closed(self : SessionReport) -> Bool {
  self.closed
}

///|
/// Whether delivery failed: a destination failed, a record stayed undelivered,
/// or a drain timed out. Drops at admission and retention failures do not count.
pub fn SessionReport::delivery_failed(self : SessionReport) -> Bool {
  self.drain_timed_out ||
  self.destinations.iter().any(destination => {
    destination.failure is Some(_) || destination.undelivered > 0L
  })
}

///|
/// Each failure in the report, for people, one text for each failure: the
/// destination failures, then the retention failures, then a drain timeout.
pub fn SessionReport::failure_messages(self : SessionReport) -> Array[String] {
  let messages = []
  for destination in self.destinations {
    if destination.failure is Some(failure) {
      messages.push(failure.message())
    }
  }
  for destination in self.destinations {
    if destination.retention_failure is Some(failure) {
      messages.push(failure.message())
    }
  }
  if self.drain_timed_out {
    messages.push(
      "logging: the final drain ended at its time limit before all accepted records were written",
    )
  }
  messages
}