//! Logging/tracing initialization.
//!
//! qsu uses two different simultaneous logging facilities.  `log` is used for
//! "production logging" and `tracing` is used for "developer logging".
//!
//! # Production logging
//! The production logging has two key configurations: _log level_ and
//! _log filtering_.  The log level sets a global filtering for level for all
//! log events, while the log filter can be used to configure per-module log
//! levels.
//!
//! In all cases except when running as a Windows Service, the log level is
//! configured using the `LOG_LEVEL` environment variable.  When running as a
//! Windows Service, it is configured using the service's `Parameters` subkey,
//! using the `LogLevel` parameter.  Their value specifies the maximum log
//! level, and can be set to `off`, `error`, `warn`, `info`, `debug` or
//! `trace`.
//!
//! The log filtering uses the same filtering specification format as
//! [`env_logger`](https://docs.rs/env_logger/latest/env_logger/), and can be
//! configured using the `LOG_FILTER` environment variable (or `LogFilter`
//! registry value, when running as a Windows Service).
//!
//! If log filtering has not been configured by the end-user, qsu will use
//! application-defined default per-module filtering, configured by the
//! application by calling [`set_default_log_filters()`].
//!
//! Production logs will be written to standard error, except when running as a
//! Windows Service, in which case it will be written to the Windows Event Log.
//!
//! # Developer logging
//! The level of developer logging is configured using `TRACE_FILTER`
//! environment variable (or `TraceFilter` registry value when running as a
//! Windows Service).  The developer logs can be redirected to a file instead,
//! using the `TRACE_FILE` environment variable (or, when running as a Windows
//! Service, the `TraceFile` registry variable).

use std::{
  env, fmt, fs,
  io::Write,
  path::{Path, PathBuf},
  str::FromStr,
  sync::Mutex
};

use time::macros::format_description;

use tracing_subscriber::{EnvFilter, fmt::time::UtcTime};

#[cfg(feature = "clap")]
use clap::ValueEnum;

use crate::err::Error;


static DEFAULT_LOG_FILTERS: Mutex<Vec<(String, log::LevelFilter)>> =
  Mutex::new(Vec::new());


/// Configure default production log module filtering.
///
/// These settings will only take effect if the `LOG_FILTER` environment
/// variable (or `LogFilter` registry setting on Windows) is not set.
///
/// If used, this function must be called before the logging subsystem has been
/// initialized.
///
/// # Panics
/// If the default log filters mutex could not be locked this function will
/// panic.
pub fn set_default_log_filters(filters: &[(&str, log::LevelFilter)]) {
  let filters: Vec<_> = filters
    .iter()
    .map(|(module, level)| ((*module).to_string(), *level))
    .collect();
  if let Ok(mut g) = DEFAULT_LOG_FILTERS.lock() {
    *g = filters;
  } else {
    panic!("Unable to acquire default log filters lock");
  }
}

#[derive(Default)]
enum LogOut {
  #[default]
  Console,

  #[cfg(windows)]
  WinEvtLog { svcname: String }
}

/// Builder used for logging and tracing initialization.
pub struct LumberJack {
  init: bool,

  as_service: bool,

  /// Controls the target output of production logging.
  log_out: LogOut,

  /// Global log level for production logging.
  log_level: LogLevel,

  /// Production logging filtering rule.
  log_filter: Option<String>,
  trace_filter: Option<String>,
  trace_file: Option<PathBuf>
}

impl Default for LumberJack {
  /// Create a default log/trace initialization.
  ///
  /// This will set the `log` log level to the value of the `LOG_LEVEL`
  /// environment variable, or default to `warm` (if either not set or
  /// invalid).
  ///
  /// The `tracing` trace level will use the environment variable
  /// `TRACE_FILTER` in a similar manner, but defaults to `none`.
  ///
  /// If the environment variable `TRACE_FILE` is set the value will be the
  /// used as the file name to write the trace logs to.
  fn default() -> Self {
    // Get default global logging level.  Default to "warn" if LOG_LEVEL is not
    // set.
    let log_level =
      std::env::var("LOG_LEVEL").map_or(LogLevel::Warn, |level| {
        level
          .parse::<LogLevel>()
          .map_or(LogLevel::Warn, |level| level)
      });

    // Get logging filtering rules.
    //
    // If this is not set,
    let log_filter = std::env::var("LOG_FILTER").ok();

    let trace_file =
      std::env::var("TRACE_FILE").map_or(None, |v| Some(PathBuf::from(v)));
    let trace_filter = env::var("TRACE_FILTER").ok();

    Self {
      init: true,
      as_service: false,
      log_out: LogOut::default(),
      log_level,
      log_filter,
      trace_filter,
      trace_file
    }
  }
}

impl LumberJack {
  /// Create a [`LumberJack::default()`] object.
  #[must_use]
  pub fn new() -> Self {
    Self::default()
  }

  /// Do not initialize logging/tracing.
  ///
  /// This is useful when running tests.
  #[must_use]
  pub fn noinit() -> Self {
    Self {
      init: false,
      ..Default::default()
    }
  }

  #[must_use]
  pub const fn service(mut self) -> Self {
    self.service_r();
    self
  }

  pub const fn service_r(&mut self) -> &mut Self {
    self.as_service = true;
    self
  }


  #[must_use]
  pub const fn set_init(mut self, flag: bool) -> Self {
    self.init = flag;
    self
  }

  /// Load logging/tracing information from a service Parameters subkey.
  ///
  /// # Errors
  /// `Error::SubSystem`
  #[cfg(windows)]
  pub fn from_winsvc(svcname: &str) -> Result<Self, Error> {
    let params = crate::rt::winsvc::get_service_param(svcname)?;

    let loglevel = params
      .get_string("LogLevel")
      .unwrap_or_else(|_| String::from("warn"))
      .parse::<LogLevel>()
      .unwrap_or(LogLevel::Warn);

    let logfilter = params.get_string("LogFilter").ok();

    let tracefilter = params.get_string("TraceFilter");
    let tracefile = params.get_string("TraceFile");

    let mut this = Self::new().log_level(loglevel);
    this.log_out = LogOut::WinEvtLog {
      svcname: svcname.to_string()
    };
    if let Some(filter) = logfilter {
      this.log_filter_r(filter);
    }

    let this =
      if let (Ok(tracefilter), Ok(tracefile)) = (tracefilter, tracefile) {
        this.trace_filter(tracefilter).trace_file(tracefile)
      } else {
        this
      };

    Ok(this)
  }

  /// Set the `log` logging level.
  #[must_use]
  pub const fn log_level(mut self, level: LogLevel) -> Self {
    self.log_level = level;
    self
  }

  /// Set production logging filtering.
  #[must_use]
  pub fn log_filter(mut self, filters: String) -> Self {
    self.log_filter = Some(filters);
    self
  }

  /// Set production logging filtering.
  pub fn log_filter_r(&mut self, filters: String) -> &mut Self {
    self.log_filter = Some(filters);
    self
  }

  /// Set the `tracing` log level.
  #[must_use]
  #[allow(clippy::needless_pass_by_value)]
  pub fn trace_filter(mut self, filter: impl ToString) -> Self {
    self.trace_filter = Some(filter.to_string());
    self
  }

  /// Set a file to which `tracing` log entries are written (rather than to
  /// write to console).
  #[must_use]
  pub fn trace_file<P>(mut self, fname: P) -> Self
  where
    P: AsRef<Path>
  {
    self.trace_file = Some(fname.as_ref().to_path_buf());
    self
  }

  /// Commit requested settings to `log` and `tracing`.
  ///
  /// # Errors
  /// Any initialization error is translated into [`Error::LumberJack`].
  pub fn init(self) -> Result<(), Error> {
    if self.init {
      match self.log_out {
        LogOut::Console => {
          self.init_cons_logging()?;
        }
        #[cfg(windows)]
        LogOut::WinEvtLog { ref svcname } => {
          self.init_winsvc_logger(svcname)?;
        }
      }

      if let Some(fname) = self.trace_file {
        init_file_tracing(fname, self.trace_filter.as_deref());
      } else {
        init_console_tracing(self.trace_filter.as_deref());
      }
    }
    Ok(())
  }

  /// Initialize logging for output to console.
  #[expect(clippy::unnecessary_wraps)]
  fn init_cons_logging(&self) -> Result<(), Error> {
    let mut bldr = env_logger::Builder::new();

    // When running in "foreground mode", then include a timestamp.
    // When running as a service, assumed the logging backend will manage
    // timestamps.
    if self.as_service {
      bldr.format(|buf, record| {
        writeln!(buf, "[{}] {}", record.level(), record.args())
      });
    } else {
      bldr.format(|buf, record| {
        writeln!(
          buf,
          "{} [{}] {}",
          chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
          record.level(),
          record.args()
        )
      });
    }

    self.init_log_filtering(&mut bldr);

    // Initialize logging
    bldr.init();

    // Set global maximum log level in the `log` crate.
    //
    // This is basically the "fast" rule.  If something doesn't get past this
    // filter it will never each env_logger's filtering engine.
    //
    // This must be set _after_ the bldr.init().
    log::set_max_level(self.log_level.into());

    Ok(())
  }

  #[cfg(windows)]
  fn init_winsvc_logger(&self, svcname: &str) -> Result<(), Error> {
    let mut bldr = env_logger::Builder::new();

    self.init_log_filtering(&mut bldr);

    let loglvl = match self.log_level {
      LogLevel::Off => todo!(),
      LogLevel::Error => log::Level::Error,
      LogLevel::Warn => log::Level::Warn,
      LogLevel::Info => log::Level::Info,
      LogLevel::Debug => log::Level::Debug,
      LogLevel::Trace => log::Level::Trace
    };

    fltevtlog::init(bldr, svcname, loglvl)?;

    log::set_max_level(self.log_level.into());

    Ok(())
  }

  fn init_log_filtering(&self, bldr: &mut env_logger::Builder) {
    // If LOG_FILTER/LogFilter has been set, then use it to apply production
    // log filtering rules.  If it is not set, then use the
    // application-configured filtering rules.
    if let Some(ref filters) = self.log_filter {
      bldr.parse_filters(filters);
    } else if let Ok(mut g) = DEFAULT_LOG_FILTERS.lock() {
      let filters: Vec<_> = g.drain(..).collect();
      drop(g);
      if filters.is_empty() {
        // Get the configured global filtering level
        let lf = self.log_level.into();

        // If application hasn't specified any default filtering rules, then
        // just set the global filtering in `env_logger` to the same
        // level as `log`'s.
        bldr.filter(None, lf);
      } else {
        // Application asked for specific filtering rules, so apply them.
        for (module, level) in filters {
          bldr.filter_module(&module, level);
        }
      }
    }
  }
}


#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "clap", derive(ValueEnum))]
pub enum LogLevel {
  /// No logging.
  #[cfg_attr(feature = "clap", clap(name = "off"))]
  Off,

  /// Log errors.
  #[cfg_attr(feature = "clap", clap(name = "error"))]
  Error,

  /// Log warnings and errors.
  #[cfg_attr(feature = "clap", clap(name = "warn"))]
  #[default]
  Warn,

  /// Log info, warnings and errors.
  #[cfg_attr(feature = "clap", clap(name = "info"))]
  Info,

  /// Log debug, info, warnings and errors.
  #[cfg_attr(feature = "clap", clap(name = "debug"))]
  Debug,

  /// Log trace, debug, info, warninga and errors.
  #[cfg_attr(feature = "clap", clap(name = "trace"))]
  Trace
}

impl FromStr for LogLevel {
  type Err = String;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    match s {
      "off" => Ok(Self::Off),
      "error" => Ok(Self::Error),
      "warn" => Ok(Self::Warn),
      "info" => Ok(Self::Info),
      "debug" => Ok(Self::Debug),
      "trace" => Ok(Self::Trace),
      _ => Err(format!("Unknown log level '{s}'"))
    }
  }
}

impl fmt::Display for LogLevel {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    let s = match self {
      Self::Off => "off",
      Self::Error => "error",
      Self::Warn => "warn",
      Self::Info => "info",
      Self::Debug => "debug",
      Self::Trace => "trace"
    };
    write!(f, "{s}")
  }
}

impl From<LogLevel> for log::LevelFilter {
  fn from(ll: LogLevel) -> Self {
    match ll {
      LogLevel::Off => Self::Off,
      LogLevel::Error => Self::Error,
      LogLevel::Warn => Self::Warn,
      LogLevel::Info => Self::Info,
      LogLevel::Debug => Self::Debug,
      LogLevel::Trace => Self::Trace
    }
  }
}

impl From<LogLevel> for Option<tracing::Level> {
  fn from(ll: LogLevel) -> Self {
    match ll {
      LogLevel::Off => None,
      LogLevel::Error => Some(tracing::Level::ERROR),
      LogLevel::Warn => Some(tracing::Level::WARN),
      LogLevel::Info => Some(tracing::Level::INFO),
      LogLevel::Debug => Some(tracing::Level::DEBUG),
      LogLevel::Trace => Some(tracing::Level::TRACE)
    }
  }
}

fn init_console_tracing(filter: Option<&str>) {
  // When running on console, then disable tracing by default
  let filter = filter.map_or_else(|| EnvFilter::new("none"), EnvFilter::new);

  tracing_subscriber::fmt()
    .with_env_filter(filter)
    //.with_timer(timer)
    .init();
}

/// Optionally set up tracing to a file.
///
/// This function will attempt to set up tracing to a file.  There are three
/// conditions that must be true in order for tracing to a file to be enabled:
/// - A filename must be specified (either via the `fname` argument or the
///   `TRACE_FILE` environment variable).
/// - A trace filter must be specified (either via the `filter` argument or the
///   `TRACE_FILTER` environment variable).
/// - The file name must be openable for writing.
///
/// Because tracing is an optional feature and intended for development only,
/// this function will enable tracing if possible, and silently ignore and
/// errors.
fn init_file_tracing<P>(fname: P, filter: Option<&str>)
where
  P: AsRef<Path>
{
  //
  // If both a trace file name and a trace level
  //
  let timer = UtcTime::new(format_description!(
    "[year]-[month]-[day] [hour]:[minute]:[second]"
  ));

  let Ok(f) = fs::OpenOptions::new().create(true).append(true).open(fname)
  else {
    return;
  };

  //
  // If TRACING_FILE is set, then default to filter out at warn level.
  // Disable all tracing if TRACE_FILTER is not set
  //
  let filter = filter.map_or_else(|| EnvFilter::new("warn"), EnvFilter::new);

  tracing_subscriber::fmt()
    .with_env_filter(filter)
    .with_writer(f)
    .with_ansi(false)
    .with_timer(timer)
    .init();

  //tracing_subscriber::registry().with(filter).init();
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :
