//
// Syd: rock-solid unikernel
// src/log.rs: Simple logging on standard error using JSON lines
//
// Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

/* Simple logging with JSON lines */
use std::{
    env,
    ffi::CString,
    fs::{File, OpenOptions},
    io::{self, Write},
    os::{
        fd::{FromRawFd, RawFd},
        unix::fs::OpenOptionsExt,
    },
    path::Path,
    time::{SystemTime, UNIX_EPOCH},
};

use lexis::ToName;
use log::{Level, LevelFilter, Log};
use nix::unistd::{Pid, Uid};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde_json::{Map, Value};

use crate::{config::*, err::SydResult, fs::mask_path, proc::*};

pub(crate) static NO_SYSLOG: Lazy<bool> = Lazy::new(|| std::env::var_os(ENV_NO_SYSLOG).is_some());

/// info! logging macro
#[macro_export]
macro_rules! info {
    ($($key:literal : $value:expr),+) => {
        if log::log_enabled!(log::Level::Info) {
            let tick = $crate::log::tick();
            let timestamp = $crate::log::now();
            let mut map = serde_json::Map::with_capacity(16);
            $(
                if let Ok(value) = serde_json::to_value($value) {
                    map.insert($key.to_string(), value);
                } else {
                    map.insert($key.to_string(), serde_json::Value::Null);
                }
            )+
            if !map.is_empty() {
                $crate::log::log(log::Level::Info, timestamp, tick, map);
            }
        }
    }
}

/// error! logging macro
#[macro_export]
macro_rules! error {
    ($($key:literal : $value:expr),+) => {
        if log::log_enabled!(log::Level::Error) {
            let tick = $crate::log::tick();
            let timestamp = $crate::log::now();
            let mut map = serde_json::Map::with_capacity(16);
            $(
                if let Ok(value) = serde_json::to_value($value) {
                    map.insert($key.to_string(), value);
                } else {
                    map.insert($key.to_string(), serde_json::Value::Null);
                }
            )+
            if !map.is_empty() {
                $crate::log::log(log::Level::Error, timestamp, tick, map);
            }
        }
    }
}

/// warn! logging macro
#[macro_export]
macro_rules! warn {
    ($($key:literal : $value:expr),+) => {
        if log::log_enabled!(log::Level::Warn) {
            let tick = $crate::log::tick();
            let timestamp = $crate::log::now();
            let mut map = serde_json::Map::with_capacity(16);
            $(
                if let Ok(value) = serde_json::to_value($value) {
                    map.insert($key.to_string(), value);
                } else {
                    map.insert($key.to_string(), serde_json::Value::Null);
                }
            )+
            if !map.is_empty() {
                $crate::log::log(log::Level::Warn, timestamp, tick, map);
            }
        }
    }
}

/// debug! logging macro
#[macro_export]
macro_rules! debug {
    ($($key:literal : $value:expr),+) => {
        #[cfg(feature = "log")]
        if log::log_enabled!(log::Level::Debug) {
            let tick = $crate::log::tick();
            let timestamp = $crate::log::now();
            let mut map = serde_json::Map::with_capacity(16);
            $(
                if let Ok(value) = serde_json::to_value($value) {
                    map.insert($key.to_string(), value);
                } else {
                    map.insert($key.to_string(), serde_json::Value::Null);
                }
            )+
            if !map.is_empty() {
                $crate::log::log(log::Level::Debug, timestamp, tick, map);
            }
        }
    }
}

/// trace! logging macro
#[macro_export]
macro_rules! trace {
    ($($key:literal : $value:expr),+) => {
        #[cfg(feature = "log")]
        if log::log_enabled!(log::Level::Trace) {
            let tick = $crate::log::tick();
            let timestamp = $crate::log::now();
            let mut map = serde_json::Map::with_capacity(16);
            $(
                if let Ok(value) = serde_json::to_value($value) {
                    map.insert($key.to_string(), value);
                } else {
                    map.insert($key.to_string(), serde_json::Value::Null);
                }
            )+
            if !map.is_empty() {
                $crate::log::log(log::Level::Trace, timestamp, tick, map);
            }
        }
    }
}

type Writer = Box<dyn Write + Send>;

/// Simple logging on standard error using JSON lines
pub struct JsonLinesLogger {
    filter: LevelFilter,
    writer: Mutex<Writer>,
}

impl JsonLinesLogger {
    fn new(log: Option<&Path>, default_level: &str) -> SydResult<Self> {
        let level = env::var(ENV_LOG).unwrap_or_else(|_| default_level.to_string());
        let filter = level.parse::<LevelFilter>().unwrap_or(LevelFilter::Warn);

        let writer: Box<dyn io::Write + Send> = if let Some(log) = log {
            Box::new(
                OpenOptions::new()
                    .append(true)
                    .create(true)
                    .mode(0o600)
                    .custom_flags(nix::libc::O_CLOEXEC)
                    .open(log)?,
            )
        } else {
            match env::var(ENV_LOG_FD) {
                Ok(fd) => {
                    // SAFETY: We panic on parse errors.
                    #[allow(clippy::disallowed_methods)]
                    let fd: RawFd = fd.parse().expect("Invalid log file descriptor");
                    // SAFETY: We trust the user to pass a valid FD.
                    Box::new(unsafe { File::from_raw_fd(fd) })
                }
                Err(_) => Box::new(io::stderr()),
            }
        };

        Ok(Self {
            filter,
            writer: Mutex::new(writer),
        })
    }

    /// Initialize logging
    pub fn init(log: Option<&Path>, default_level: &str) -> SydResult<()> {
        let logger = Self::new(log, default_level)?;

        log::set_max_level(logger.filter);
        Ok(log::set_boxed_logger(Box::new(logger))?)
    }
}

impl Log for JsonLinesLogger {
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        metadata.level() <= self.filter
    }

    fn log(&self, record: &log::Record) {
        if self.enabled(record.metadata()) {
            let mut writer = self.writer.lock();
            let _ = writeln!(writer, "{}", record.args());
        }
    }

    fn flush(&self) {}
}

/// Helper for logging using JSON lines.
#[allow(clippy::cognitive_complexity)]
pub fn log(level: Level, timestamp: u64, tick: u64, mut msg: Map<String, Value>) {
    // Enrich the log message with the log level.
    msg.insert(
        "l".to_string(),
        serde_json::Value::Number((level as u64).into()),
    );

    // Enrich the log message with the current user.
    msg.insert(
        "uid".to_string(),
        serde_json::Value::Number(Uid::current().as_raw().into()),
    );

    // Enrich the log message with timing info.
    msg.insert("t".to_string(), serde_json::Value::Number(timestamp.into()));
    if tick != 0 {
        msg.insert("c".to_string(), serde_json::Value::Number(tick.into()));
    } // else hardware ticks are not available on this architecture.

    // Check if the log message has process ID info.
    if let Some(pid) = msg.get("pid").and_then(|v| v.as_i64()) {
        // Process ID is given.
        // If log feature is enabled enrichen the message with cmdline and cwd.
        // If log feature is not enabled enrichen the message with comm only.
        #[allow(clippy::cast_possible_truncation)]
        let pid = Pid::from_raw(pid as nix::libc::pid_t);
        if pid.as_raw() != 0 {
            #[allow(clippy::cast_sign_loss)]
            msg.insert(
                "id".to_string(),
                Value::String((pid.as_raw() as u64).to_name()),
            );

            #[cfg(not(feature = "log"))]
            {
                if let Ok(cmd) = proc_comm(pid).map(|p| mask_path(&p)) {
                    msg.insert("cmd".to_string(), Value::String(cmd));
                } else {
                    msg.insert("cmd".to_string(), Value::Null);
                }
            }

            #[cfg(feature = "log")]
            {
                if let Ok(cmd) = proc_cmdline(pid).map(|p| mask_path(&p)) {
                    msg.insert("cmd".to_string(), Value::String(cmd));
                } else {
                    msg.insert("cmd".to_string(), Value::Null);
                }
                if let Ok(dir) = proc_cwd(pid).map(|p| mask_path(&p)) {
                    msg.insert("cwd".to_string(), Value::String(dir));
                } else {
                    msg.insert("cwd".to_string(), Value::Null);
                }
            }
        }
    }

    // Serialize the message into a String.
    let msg = serde_json::to_string(&msg).unwrap_or_else(|e| {
        let e = serde_json::to_string(&format!("{e:?}")).unwrap_or("?".to_string());
        format!("{{\"ctx\":\"log\",\"op\":\"serialize\",\"error\": \"{e}\"}}")
    });

    // Send the log String to its destination.
    match level {
        Level::Error => {
            if !*NO_SYSLOG {
                syslog(&msg);
            }
            log::error!("{msg}");
        }
        Level::Warn => {
            if !*NO_SYSLOG {
                syslog(&msg);
            }
            log::warn!("{msg}");
        }
        Level::Info => log::info!("{msg}"),
        Level::Debug => log::debug!("{msg}"),
        Level::Trace => log::trace!("{msg}"),
    }
}

/// Logs a message to the system's syslog.
///
/// # Arguments
///
/// * `message` - The message string to be logged.
fn syslog(message: &str) {
    let c_msg = CString::new(message).unwrap_or_else(|_|
        // SAFETY: We ensure the string has no null bytes and the vector
        // we pass into the function is an actual byte vector.
        unsafe { CString::from_vec_unchecked(b"?".to_vec()) });

    // SAFETY: Use the syslog interface provided by libc.
    unsafe {
        nix::libc::syslog(
            nix::libc::LOG_WARNING,
            "%s\0".as_ptr().cast(),
            c_msg.as_ptr(),
        );
    }
}

/// Returns a current value of the tick counter to use as a staring point.
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
#[inline]
pub fn tick() -> u64 {
    tick_counter::start()
}

/// Return 0 for compat.
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
#[inline]
pub fn tick() -> u64 {
    0u64
}

/// Return the current time in seconds since the unix epoch.
#[inline]
pub fn now() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs()
}
