// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// How severe a message is, ordered from most verbose to least.
///
/// Six levels, which is the union of what the libraries here already had: a
/// server's `Trace` through `Critical` and a consensus core's `Debug` through
/// `Fatal`. `Critical` and `Fatal` are the same level under two names, so the
/// one kept is `Fatal`; `Panic`, which etcd's logger also aborts on, is not a
/// level but a thing the caller does after logging at `Fatal`.
pub(all) enum Level {
  Trace
  Debug
  Info
  Warning
  Error
  Fatal
} derive(Eq, Compare, Debug)

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

///|
pub extend Level with Compare::{compare, op_ge, op_gt, op_le, op_lt}

///|
pub extend Level with Debug::{to_repr}

///|
/// A level prints as its name, so a line carrying one reads as a line.
pub impl Show for Level with fn output(self, logger) {
  logger.write_string(self.name())
}

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

///|
/// The level's rank, low to high. A message is written when its level is at
/// least the logger's, which is the arrangement every logging library uses.
pub fn Level::rank(self : Level) -> Int {
  match self {
    Trace => 0
    Debug => 1
    Info => 2
    Warning => 3
    Error => 4
    Fatal => 5
  }
}

///|
/// The level's name as it appears in a line, upper case.
pub fn Level::name(self : Level) -> String {
  match self {
    Trace => "TRACE"
    Debug => "DEBUG"
    Info => "INFO"
    Warning => "WARNING"
    Error => "ERROR"
    Fatal => "FATAL"
  }
}

///|
/// A level by name, however it was capitalised.
///
/// It answers `None` rather than falling back to a level of its own choosing: a
/// configuration file with a typo in it should say so, not run at a verbosity
/// nobody asked for. The names other libraries use for the same level are
/// accepted — `warn`, `critical`, `fatal`, `err` — so a configuration written
/// for one of them keeps working.
pub fn Level::parse(name : StringView) -> Level? {
  match name.to_lower() {
    "trace" => Some(Trace)
    "debug" => Some(Debug)
    "info" => Some(Info)
    "warn" | "warning" => Some(Warning)
    "err" | "error" => Some(Error)
    "fatal" | "critical" => Some(Fatal)
    _ => None
  }
}

///|
/// One message on its way out.
///
/// There is no timestamp. A library has no clock — reading one is a side effect
/// and a logger that stamps the time cannot be tested for what it wrote — so the
/// instant, when it is wanted, comes from the sink that was handed a clock. The
/// same reasoning as `mooncred`'s verification taking its `now`.
pub(all) struct Record {
  level : Level
  message : String
  /// Structured fields, in the order they were given. `Json` is a builtin, so
  /// carrying values rather than pre-formatted text costs no dependency.
  fields : Array[(String, Json)]
}

///|
/// Where records go.
///
/// A trait rather than a function so a sink can hold state — a buffer, a file, a
/// clock, a counter — and so an embedder can route lines into its own logging
/// without this library knowing anything about it.
pub(open) trait Sink {
  fn write(Self, Record) -> Unit
}

///|
/// A logger: a level to filter by and somewhere to write.
///
/// The filter is a `Level?` rather than a `Level` because "write nothing at all"
/// is not a level — a message is never of severity "off" — and a silent logger
/// has to be able to say so to [`enabled`], so a caller skips building a message
/// it would only throw away.
pub struct Logger {
  filter : Level?
  sink : &Sink
}

///|
/// The level a logger runs at when nothing says otherwise.
///
/// `Info` is what every library here defaults to, and what Python's `logging`,
/// Go's `slog` and go-zero all settle on: a deployment wants to hear about what
/// happened, not about every step taken to get there.
pub let level : Level = Info

///|
/// A logger writing to `sink` at `level`.
pub fn Logger::new(sink : &Sink, level? : Level = level) -> Logger {
  { filter: Some(level), sink, }
}

///|
/// A logger that writes nothing, for an embedder that does its own reporting and
/// for a test that would rather not have output.
///
/// It is not a logger at level `Fatal`: [`enabled`] answers false at every level,
/// so a caller that builds an expensive message only when it will be used builds
/// none at all.
pub fn Logger::silent() -> Logger {
  { filter: None, sink: Discard::{ }, }
}

///|
/// The level this logger writes at, or `None` if it writes nothing.
pub fn Logger::level(self : Logger) -> Level? {
  self.filter
}

///|
/// The same logger at another level.
pub fn Logger::at(self : Logger, level : Level) -> Logger {
  { filter: Some(level), sink: self.sink, }
}

///|
/// Whether a message at `level` would be written.
///
/// Worth asking before assembling a message that costs something: a logger
/// cannot skip work its caller has already done.
pub fn Logger::enabled(self : Logger, level : Level) -> Bool {
  match self.filter {
    Some(filter) => level.rank() >= filter.rank()
    None => false
  }
}

///|
/// Write a message, with fields if there are any.
pub fn Logger::log(
  self : Logger,
  level : Level,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  if self.enabled(level) {
    self.sink.write({ level, message, fields, })
  }
}

///|
/// Write at `Trace`.
pub fn Logger::trace(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Trace, message, fields~)
}

///|
/// Write at `Debug`.
pub fn Logger::debug(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Debug, message, fields~)
}

///|
/// Write at `Info`.
pub fn Logger::info(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Info, message, fields~)
}

///|
/// Write at `Warning`.
pub fn Logger::warn(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Warning, message, fields~)
}

///|
/// Write at `Error`.
pub fn Logger::error(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Error, message, fields~)
}

///|
/// Write at `Fatal`. It does not abort — what to do after the last message is
/// the caller's to decide, and a library that ends the process takes that
/// decision away.
pub fn Logger::fatal(
  self : Logger,
  message : String,
  fields? : Array[(String, Json)] = [],
) -> Unit {
  self.log(Fatal, message, fields~)
}

///|
/// A sink that drops everything.
pub(all) struct Discard {}

///|
pub impl Sink for Discard with fn write(_self, _record) {

}

///|
pub extend Discard with Sink::{write}

///|
/// A sink that hands each line to a function.
///
/// This is the seam: `Lines::new(line => println(line))` prints, and
/// `Lines::new(line => held.push(line))` is how a test reads what was written.
pub struct Lines {
  write : (String) -> Unit
  format : (Record) -> String
}

///|
/// A sink writing one line per record through `write`.
///
/// `format` turns a record into that line; [`line`] is the default and writes
/// `LEVEL message key=value`, which is what a person reads. For something a
/// machine reads, [`json_line`] writes one JSON object instead.
pub fn Lines::new(
  write : (String) -> Unit,
  format? : (Record) -> String = line,
) -> Lines {
  { write, format, }
}

///|
pub impl Sink for Lines with fn write(self, record) {
  (self.write)((self.format)(record))
}

///|
pub extend Lines with Sink::{write}

///|
/// A record as `LEVEL message key=value key=value`.
///
/// Values are written as their JSON, so a string keeps its quotes and a field
/// containing a space cannot be mistaken for two fields.
pub fn line(record : Record) -> String {
  let out = StringBuilder()
  out.write_string(record.level.name())
  out.write_char(' ')
  out.write_string(record.message)
  for field in record.fields {
    out.write_char(' ')
    out.write_string(field.0)
    out.write_char('=')
    out.write_string(field.1.stringify())
  }
  out.to_string()
}

///|
/// The key the level is written under.
///
/// `level` and `msg` are what go-zero, zap and Go's `slog` all name them, so a
/// line from here drops into a log pipeline already built for one of those.
pub let level_key : String = "level"

///|
/// The key the message is written under.
pub let message_key : String = "msg"

///|
/// One record as a single JSON object, for a reader that is a machine.
///
/// The level and the message come first, then the fields in the order they were
/// given. A field named `level` or `msg` replaces the one this writes rather than
/// appearing twice: an object with a key twice is a document readers disagree
/// about, and the caller naming it meant it.
pub fn json_line(record : Record) -> String {
  object(record)
}

///|
/// The same, with the two keys named.
///
/// A pipeline that expects `severity` and `message` rather than `level` and
/// `msg` is common enough — Google Cloud Logging is one — that renaming them is
/// a parameter rather than a reason to write your own formatter.
pub fn object(
  record : Record,
  level? : String = level_key,
  message? : String = message_key,
) -> String {
  let out : Map[String, Json] = Map([])
  out[level] = Json::string(record.level.name())
  out[message] = Json::string(record.message)
  for field in record.fields {
    out[field.0] = field.1
  }
  // The object is written in insertion order, so the level and the message stay
  // in front where a person skimming the stream can find them.
  Json::object(out).stringify()
}