/*-
 * syslog-rs - a syslog client translated from libc to rust
 * 
 * Copyright 2025 Aleksandr Morozov
 * 
 * The syslog-rs crate can be redistributed and/or modified
 * under the terms of either of the following licenses:
 *
 *   1. the Mozilla Public License Version 2.0 (the “MPL”) OR
 *
 *   2. The MIT License (MIT)
 *                     
 *   3. EUROPEAN UNION PUBLIC LICENCE v. 1.2 EUPL © the European Union 2007, 2016
 */

use std::{fmt};

use crate::{error::SyRes, TapType, SyslogDestination};

/// A trait which implements the necessary functions for syslog internals.
/// It `taps` the connection and provides the functionality for the syslog client
/// to use the communication channel.
pub trait SyslogTap<D: SyslogDestination>: fmt::Debug + Send + Clone
{
    /// Proves the new instance of the syslog communication.
    /// 
    /// # Arguments
    /// 
    /// `req_tap` - a connection endpoint setup information i.e address, path...
    /// 
    /// # Returns
    /// 
    /// In case of any error a [SyRes] i.e [Result::Err] should be returned.
    fn new(req_tap: D) -> SyRes<Self> where Self: Sized;

    /// Opens a connection to target.
    fn connectlog(&mut self) -> SyRes<()>;

    /// Writes provided data to socket
    fn send(&self, msg: &[u8]) -> std::io::Result<usize>;

    /// Disconnects the connection.
    fn disconnectlog(&mut self) -> SyRes<()>;

    /// Tells if connection was opened.
    fn is_connected(&self) -> bool;

    /// Returns the exact type of the connection.
    fn get_type(&self) -> TapType;

    /// Returns the maximum message size (inc headers).
    fn get_max_msg_size() -> usize;

    /// Updates the current tap with new setup. Disconnects from the server!.
    fn update_tap_data(&mut self, tap_data: D);
}

/// A structure which holds a socket instance and all necessary information
/// which can be used to reestablish the connection. This is for sync only.
#[derive(Debug)]
pub struct Tap<T, D: SyslogDestination>
{
    /// Channel
    sock: Option<T>,

    /// Tap settings and state
    tap_data: D,

    cur_tap_type: TapType,
}

impl<T, D: SyslogDestination> Clone for Tap<T, D>
{
    /// Clones only `tap_data`. The rest are default.
    fn clone(&self) -> Self 
    {
        return 
            Self 
            { 
                sock: None,
                tap_data: self.tap_data.clone(), 
                cur_tap_type: TapType::None
            };
    }
}

impl<T, D: SyslogDestination> Drop for Tap<T, D>
{
    fn drop(&mut self) 
    {
        let _ = self.sock.take();
    }
}
 
#[cfg(feature = "build_ext_tls")]
mod with_tls
{
   
    use std::{io::Write, net::TcpStream};
    use std::io::ErrorKind;

    use rustls::{lock::Mutex, ClientConnection, StreamOwned};
    use socket2::{Domain, Protocol, Socket, Type};

    
    use crate::{SyslogDestMsg, map_error_os};

    use super::*;

    impl SyslogTap<crate::SyslogTls> for Tap<Mutex<StreamOwned<ClientConnection, TcpStream>>, crate::SyslogTls>
    {
        fn new(req_tap: crate::SyslogTls) -> SyRes<Self>
        {
            let ret = 
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::NetTcp,
                };

            return Ok(ret);
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {
            use crate::map_error;

            let tls_conn = 
                ClientConnection
                    ::new(self.tap_data.get_client_config(), self.tap_data.get_serv_name())
                        .map_err(|e|
                            map_error!("can not connect to tcp '{}', error: {}", self.tap_data.get_remote_addr(), e)
                        )?;

            // crating socket TCP
            let socket = 
                if self.tap_data.get_remote_addr().is_ipv4() == true
                {
                    Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))
                }
                else
                {
                    Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))
                }
                .map_err(|e|
                    map_error_os!(e, "cannot create socket TCP to '{}', error: {}", self.tap_data.get_remote_addr(), e)
                )?;

            // bind addr
            if self.tap_data.get_bind_addr().ip().is_unspecified() == false
            {
                socket
                    .bind(&(*self.tap_data.get_bind_addr()).into())
                    .map_err(|e|
                        map_error_os!(e, "cannot bind tpc to '{}', error: {}", self.tap_data.get_remote_addr(), e)
                    )?;
            }

            // connect
            if let Some(c_timeout) = self.tap_data.get_get_conn_timeout()
            {
                socket.connect_timeout(&(*self.tap_data.get_remote_addr()).into(), c_timeout)
            }
            else
            {
                socket.connect(&(*self.tap_data.get_remote_addr()).into())
            }
            .map_err(|e|
                map_error_os!(e, "can not connect to tcp '{}', error: {}", self.tap_data.get_remote_addr(), e)
            )?;

            /*let socket = 
                TcpStream::connect(self.tap_data.get_remote_addr())
                    .map_err(|e|
                        map_error!("can not connect to tcp '{}', error: {}", self.tap_data.get_remote_addr(), e)
                    )?;*/

            
            let tcp_steam: TcpStream = socket.into();

            let tls = StreamOwned::new(tls_conn, tcp_steam);

            // set socket
            self.sock = Some(Mutex::new(tls));

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            let mut sock = 
                self
                    .sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?
                    .lock()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::Deadlock, "failed to lock mutex")
                    )?;

            let res = sock.write(msg)?;
            sock.flush()?;

            return Ok(res);
        }

        fn disconnectlog(&mut self) -> SyRes<()>
        {
            match self.sock.take()
            {
                Some(s) => 
                {
                    drop(s);

                    Ok(())
                },
                None =>
                {
                    Ok(())
                }
            }
        }

        #[inline]
        fn is_connected(&self) -> bool 
        {
            return self.sock.is_some();
        }

        #[inline]
        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return crate::SyslogTls::get_max_msg_len();
        }

        #[inline]
        fn update_tap_data(&mut self, tap_data: crate::SyslogTls)
        {
            self.tap_data = tap_data;
        }
    }
}



#[cfg(feature = "build_ext_net")]
mod with_net
{
    use std::{io::Write, net::{TcpStream, UdpSocket}};
    use std::io::ErrorKind;

    use socket2::{Domain, Protocol, Socket, Type};

    use crate::{SyslogDestMsg, map_error_os};

    use super::*;
    
    impl SyslogTap<crate::SyslogNetTcp> for Tap<TcpStream, crate::SyslogNetTcp>
    {
        fn new(req_tap: crate::SyslogNetTcp) -> SyRes<Self>
        {
            let ret = 
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::NetTcp,
                };

            return Ok(ret);
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {  
            // crating socket TCP
            let socket = 
                if self.tap_data.get_remote_addr().is_ipv4() == true
                {
                    Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))
                }
                else
                {
                    Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))
                }
                .map_err(|e|
                    map_error_os!(e, "cannot create socket TCP to '{}', error: {}", self.tap_data.get_remote_addr(), e)
                )?;

            // bind addr
            if self.tap_data.get_bind_addr().ip().is_unspecified() == false
            {
                socket
                    .bind(&(*self.tap_data.get_bind_addr()).into())
                    .map_err(|e|
                        map_error_os!(e, "cannot bind tpc to '{}', error: {}", self.tap_data.get_remote_addr(), e)
                    )?;
            }

            // connect
            if let Some(c_timeout) = self.tap_data.get_conn_timeout()
            {
                socket.connect_timeout(&(*self.tap_data.get_remote_addr()).into(), c_timeout)
            }
            else
            {
                socket.connect(&(*self.tap_data.get_remote_addr()).into())
            }
            .map_err(|e|
                map_error_os!(e, "can not connect to tcp '{}', error: {}", self.tap_data.get_remote_addr(), e)
            )?;
            
            socket
                .shutdown(std::net::Shutdown::Read)
                .map_err(|e|
                    map_error_os!(e, "can not shutdown read portion, error: '{}'", e)
                )?;

            // set socket
            self.sock = Some(socket.into());

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            let mut sock = 
                self.sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?;

            let res = sock.write(msg)?;
            sock.flush()?;

            return Ok(res);
        }

        fn disconnectlog(&mut self) -> SyRes<()>
        {
            match self.sock.take()
            {
                Some(s) => 
                {
                    drop(s);

                    Ok(())
                },
                None =>
                {
                    Ok(())
                }
            }
        }

        #[inline]
        fn is_connected(&self) -> bool 
        {
            return self.sock.is_some();
        }

        #[inline]
        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return crate::SyslogNetTcp::get_max_msg_len();
        }

        #[inline]
        fn update_tap_data(&mut self, tap_data: crate::SyslogNetTcp)
        {
            self.tap_data = tap_data;
        }
    }


    impl SyslogTap<crate::SyslogNetUdp> for Tap<UdpSocket, crate::SyslogNetUdp>
    {
        fn new(req_tap: crate::SyslogNetUdp) -> SyRes<Self>
        {
            let ret = 
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::NetUdp,
                };

            return Ok(ret);
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {
            let socket = 
                UdpSocket
                    ::bind(self.tap_data.get_bind_addr())
                        .map_err(|e|
                            map_error_os!(e, "can not bind udp '{}', error: '{}'", self.tap_data.get_bind_addr(), e)
                        )?;

            socket
                .connect(self.tap_data.get_remote_addr())
                .map_err(|e|
                    map_error_os!(e, "can not connect to udp '{}', error: '{}'", self.tap_data.get_remote_addr(), e)
                )?;

            //todo shutdown read

            // set socket
            self.sock = Some(socket);

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            let sock = 
                self
                    .sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?;
                    
            return sock.send(msg);
        }

        fn disconnectlog(&mut self) -> SyRes<()> 
        {
            match self.sock.take()
            {
                Some(s) => 
                {
                    drop(s);

                    Ok(())
                },
                None =>
                {
                    Ok(())
                }
            }
        }

        fn is_connected(&self) -> bool 
        {
            return self.sock.is_some();
        }

        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return crate::SyslogNetUdp::get_max_msg_len();
        }

        fn update_tap_data(&mut self, tap_data: crate::SyslogNetUdp)
        {
            self.tap_data = tap_data;
        }
    }
}


#[cfg(target_family = "unix")]
mod with_unix_syslog
{
    
    use std::io::ErrorKind;
    use std::os::unix::net::UnixDatagram;

    use nix::errno::Errno;


    use crate::{error::SyRes, TapType};


    use crate::
    {
        SyslogDestMsg, SyslogLocal, SyslogTap, Tap, map_error_os, throw_error_errno, throw_error_os
    };

    use crate::common::*;

    impl SyslogTap<SyslogLocal> for Tap<UnixDatagram, SyslogLocal>
    {
        /// Creates the instance for local socket.
        /// 
        /// If `opt_path` does not exist or can not be opened, in case if 
        /// flag `use_altern` is set to true, other known localions will 
        /// be attempted to connect before giving up.
        /// 
        /// # Arguments
        /// 
        /// * `req_tap` - a type of the tap which is required. See [TapTypeData] 
        ///     The actual type of the tap selected will be stored in the field 
        ///     `cur_tap_type`.
        fn new(req_tap: SyslogLocal) -> SyRes<Self>
        {
            return Ok(
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::None,
                }
            );
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {
            let sock = 
                UnixDatagram
                    ::unbound()
                        .map_err(|e|
                            map_error_os!(e, "unbounded unix datagram initialization failure, error: '{}'", e)
                        )?;

            let tap_type = 
                if self.tap_data.get_use_alternative() == false && self.tap_data.get_custom_remote_path().is_some() == true 
                {
                    if let Err(e) = sock.connect(self.tap_data.get_custom_remote_path().as_ref().unwrap())
                    {
                        throw_error_os!(e, "failed to open connection to syslog server at '{}'", 
                            self.tap_data.get_custom_remote_path().as_ref().unwrap().display());
                    }
                    else 
                    {
                        TapType::CustomLog
                    }
                }
                else if self.tap_data.get_custom_remote_path().is_some() == true &&
                    sock.connect(self.tap_data.get_custom_remote_path().as_ref().unwrap()).is_ok() == true
                {
                    TapType::CustomLog
                }
                else if let Ok(_) = sock.connect(PATH_LOG_PRIV)
                {
                    TapType::Priv
                }
                else if let Ok(_) = sock.connect(PATH_LOG)
                {
                    TapType::UnPriv
                }
                else if let Ok(_) = sock.connect(PATH_OLDLOG)
                {
                    TapType::OldLog
                }
                else if let Ok(_) = sock.connect(PATH_OSX)
                {
                    TapType::Priv
                }
                else
                {
                    // failed to open socket
                    throw_error_errno!(Errno::last(), "failed to open connection to syslog server, no path to server");
                };

            #[cfg(any(
                target_os = "freebsd",
                target_os = "dragonfly",
                target_os = "openbsd",
                target_os = "netbsd",
                target_os = "macos"
            ))]
            {
                use std::os::fd::AsRawFd;
                
                // setting sendbuf
                let mut len: std::mem::MaybeUninit<nix::libc::socklen_t> = std::mem::MaybeUninit::uninit();

                // set the sndbuf len
                let res = 
                    unsafe
                    {
                        nix::libc::getsockopt(
                            sock.as_raw_fd(), 
                            nix::libc::SOL_SOCKET, 
                            nix::libc::SO_SNDBUF, 
                            len.as_mut_ptr() as *mut nix::libc::c_void, 
                            &mut { 
                                std::mem::size_of::<nix::libc::socklen_t>() as nix::libc::socklen_t
                            } 
                        )
                    };

                if res == 0
                {
                    let mut len = unsafe { len.assume_init() } as usize;

                    if len < MAXLINE
                    {
                        len = MAXLINE;

                        unsafe {
                            nix::libc::setsockopt(
                                sock.as_raw_fd(), 
                                nix::libc::SOL_SOCKET, 
                                nix::libc::SO_SNDBUF, 
                                &len as *const _ as *const nix::libc::c_void, 
                                std::mem::size_of::<nix::libc::socklen_t>() as nix::libc::socklen_t
                            )
                        };
                    }
                }
            }

            // shutdown read
            sock
                .shutdown(std::net::Shutdown::Read)
                .map_err(|e|
                    map_error_os!(e, "can not shutdown read portion, error: '{}'", e)
                )?;

            self.sock = Some(sock);
            self.cur_tap_type = tap_type;

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            let sock = 
                self
                    .sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?;

            return sock.send(msg);
        }

        fn disconnectlog(&mut self) -> SyRes<()>
        {
            match self.sock.take()
            {
                Some(s) => 
                {
                    self.cur_tap_type = TapType::None;

                    drop(s);

                    Ok(())
                },
                None =>
                {
                    self.cur_tap_type = TapType::None;

                    Ok(())
                }
            }
        }

        #[inline]
        fn is_connected(&self) -> bool
        {
            return self.sock.is_some();
        }

        #[inline]
        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return SyslogLocal::get_max_msg_len();
        }

        #[inline]
        fn update_tap_data(&mut self, tap_data: SyslogLocal)
        {
            self.tap_data = tap_data;
        }
    }
}

#[cfg(feature = "build_ext_file")]
mod with_file
{
    use std::fs::File;
    use std::io::ErrorKind;

    use crate::{SyslogDestMsg, SyslogFile, map_error_os};

    use super::*;
    
    impl SyslogTap<crate::SyslogFile> for Tap<File, SyslogFile>
    {
        /// Creates the instance for file.
        /// 
        /// # Arguments
        /// 
        /// * `path` - an reference to complete [Path] which point to the 
        ///     file. If does not exit, it will be created.
        fn new(req_tap: crate::SyslogFile) -> SyRes<Self>
        {
            let ret = 
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::LocalFile,
                };

            return Ok(ret);
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {
            let file = 
                File::options()
                    .append(true)
                    .read(false)
                    .write(true)
                    .create(true)
                    .open(self.tap_data.get_path())
                    .map_err(|e|
                        map_error_os!(e, "can not open file '{}' to write, error: '{}'", 
                            self.tap_data.get_path().display(), e)
                    )?;

            self.sock = Some(file);

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            use std::io::Write;
            
            let mut sock = 
                self
                    .sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?;
            
            sock.write_all(msg)?;

            return Ok(msg.len());
        }

        fn disconnectlog(&mut self) -> SyRes<()>
        {
            match self.sock.take()
            {
                Some(file) => 
                {
                    file
                        .sync_data()
                        .map_err(|e|
                            map_error_os!(e, "d")
                        )?;

                    drop(file);

                    return Ok(());
                },
                None =>
                {
                    return Ok(())
                }
            }
        }

        #[inline]
        fn is_connected(&self) -> bool 
        {
            return self.sock.is_some();
        }

        #[inline]
        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return SyslogFile::get_max_msg_len();
        }

        #[inline]
        fn update_tap_data(&mut self, tap_data: SyslogFile)
        {
            self.tap_data = tap_data;
        }
    }
}


// --- windows ---

#[cfg(target_family = "windows")]
mod with_windows_eventlog
{
    use std::io::ErrorKind;

    use crate::portable::{EventLogLocal};
    use crate::{error::SyRes, TapType};


    use crate::
    {
        SyslogDestMsg, WindowsEvent, SyslogTap, Tap, map_error_os
    };

    impl SyslogTap<WindowsEvent> for Tap<EventLogLocal, WindowsEvent>
    {
        /// Creates the instance for local socket.
        /// 
        /// If `opt_path` does not exist or can not be opened, in case if 
        /// flag `use_altern` is set to true, other known localions will 
        /// be attempted to connect before giving up.
        /// 
        /// # Arguments
        /// 
        /// * `req_tap` - a type of the tap which is required. See [TapTypeData] 
        ///     The actual type of the tap selected will be stored in the field 
        ///     `cur_tap_type`.
        fn new(req_tap: WindowsEvent) -> SyRes<Self>
        {
            return Ok(
                Self
                {
                    sock: None, 
                    tap_data: req_tap,
                    cur_tap_type: TapType::None,
                }
            );
        }

        fn connectlog(&mut self) -> SyRes<()> 
        {
            let sock = 
                EventLogLocal::new(&self.tap_data)
                    .map_err(|e|
                        map_error_os!(e, "EventRegister error: '{}'", e)
                    )?;

            self.sock = Some(sock);
            self.cur_tap_type = TapType::WindowsEventLog;

            return Ok(());
        }

        fn send(&self, msg: &[u8]) -> std::io::Result<usize> 
        {
            let sock = 
                self
                    .sock
                    .as_ref()
                    .ok_or_else(||
                        std::io::Error::new(ErrorKind::NotConnected, "no connection")
                    )?;

            return sock.send(msg);
        }

        fn disconnectlog(&mut self) -> SyRes<()>
        {
            match self.sock.take()
            {
                Some(s) => 
                {
                    self.cur_tap_type = TapType::None;

                    drop(s);

                    Ok(())
                },
                None =>
                {
                    self.cur_tap_type = TapType::None;

                    Ok(())
                }
            }
        }

        #[inline]
        fn is_connected(&self) -> bool
        {
            return self.sock.is_some();
        }

        #[inline]
        fn get_type(&self) -> TapType 
        {
            return self.cur_tap_type;
        }

        #[inline]
        fn get_max_msg_size() -> usize
        {
            return WindowsEvent::get_max_msg_len();
        }

        #[inline]
        fn update_tap_data(&mut self, tap_data: WindowsEvent)
        {
            self.tap_data = tap_data;
        }
    }
}
