use std::io;

fn main() -> io::Result<()> {
    #[cfg(unix)]
    unix::run()?;

    Ok(())
}

#[cfg(unix)]
mod unix {
    use std::{
        io::{self, IsTerminal, Write},
        os::unix::net::UnixDatagram,
    };

    use arrayvec::ArrayVec;
    use parking_lot::Mutex;
    use syslog_fmt::{
        Facility, Severity,
        v5424::{self, Timestamp},
    };

    const SYSLOG_MSG_BUFFER_LEN: usize = 1024;

    struct DatagramLogger {
        socket: UnixDatagram,
        formatter: v5424::Formatter,
        buf: Mutex<ArrayVec<u8, SYSLOG_MSG_BUFFER_LEN>>,
        log_level: log::LevelFilter,
    }

    impl log::Log for DatagramLogger {
        fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
            metadata.level() <= self.log_level
        }

        fn log(&self, record: &log::Record<'_>) {
            if self.enabled(record.metadata()) {
                let mut buf = self.buf.lock();

                let res = self.formatter.write_without_data(
                    &mut *buf,
                    Severity::Info,
                    Timestamp::CreateChronoLocal,
                    record.args(),
                    None,
                );

                if let Err(e) = res {
                    // ignore when the buffer runs over capcity
                    // write as much as you can and drop the rest
                    if e.kind() != io::ErrorKind::WriteZero {
                        eprintln!("{e}");
                    }
                }

                if let Err(e) = self.socket.send(&buf) {
                    eprintln!("{e}");
                }
            }
        }

        fn flush(&self) {}
    }

    struct StdErrLogger {
        formatter: v5424::Formatter,
        buf: Mutex<ArrayVec<u8, SYSLOG_MSG_BUFFER_LEN>>,
        log_level: log::LevelFilter,
    }

    impl log::Log for StdErrLogger {
        fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
            metadata.level() <= self.log_level
        }

        fn log(&self, record: &log::Record<'_>) {
            if self.enabled(record.metadata()) {
                let mut buf = self.buf.lock();

                let res = self.formatter.write_without_data(
                    &mut *buf,
                    Severity::Info,
                    Timestamp::CreateChronoLocal,
                    record.args(),
                    None,
                );

                if let Err(e) = res {
                    // ignore when the buffer runs over capcity
                    // write as much as you can and drop the rest
                    if e.kind() != io::ErrorKind::WriteZero {
                        eprintln!("{e}");
                    }
                }

                let bytes_written = io::stderr().write(&buf).unwrap();
                debug_assert!(bytes_written > 0);
            }
        }

        fn flush(&self) {}
    }

    pub fn run() -> io::Result<()> {
        setup_logger()?;

        log::info!("'su root' failed for lonvick on /dev/pts/8");
        log::debug!("'su root' failed for lonvick on /dev/pts/8");

        Ok(())
    }

    fn setup_logger() -> io::Result<()> {
        if io::stderr().is_terminal() {
            setup_stderr_logger()
        } else {
            setup_datagram_logger()
        }
    }

    fn setup_stderr_logger() -> io::Result<()> {
        let formatter = setup_syslog_formatter();
        let logger = StdErrLogger {
            formatter,
            buf: Mutex::new(ArrayVec::new()),
            log_level: log::LevelFilter::Info,
        };

        log::set_max_level(logger.log_level);
        log::set_boxed_logger(Box::new(logger)).map_err(io::Error::other)?;

        Ok(())
    }

    fn setup_datagram_logger() -> io::Result<()> {
        const UNIX_SOCK_PATHS: [&str; 3] = ["/dev/log", "/var/run/syslog", "/var/run/log"];

        let socket = any_datagram_socket(&UNIX_SOCK_PATHS)?;
        let formatter = setup_syslog_formatter();

        let logger = DatagramLogger {
            socket,
            formatter,
            buf: Mutex::new(ArrayVec::new()),
            log_level: log::LevelFilter::Info,
        };

        log::set_max_level(logger.log_level);
        log::set_boxed_logger(Box::new(logger)).map_err(io::Error::other)?;

        Ok(())
    }

    fn setup_syslog_formatter() -> v5424::Formatter {
        v5424::Config {
            facility: Facility::Local0,
            hostname: Some("localhost"),
            app_name: Some("simple_datagram_based_logger"),
            proc_id: std::process::id().to_string().as_str().into(),
        }
        .into_formatter()
    }

    /// Try to connect as a datagram socket to any of the given paths.
    ///
    /// It's quite likely that a datagram socket is being used for a syslog setup, as
    /// syslog messages should not be large enough to justify streaming.
    fn any_datagram_socket(paths: &[&str]) -> Result<UnixDatagram, io::Error> {
        for path in paths {
            if let Ok(socket) = UnixDatagram::unbound() {
                if socket.connect(path).is_ok() {
                    return Ok(socket);
                };
            }
        }

        Err(io::Error::other("No datagram socket could be found"))
    }
}
