#![warn(
    clippy::all,
    //clippy::restriction,
    clippy::pedantic,
    clippy::nursery,
    clippy::cargo
)]
#![allow(clippy::non_ascii_literal)]

use crate::api::GroupRead;
use crate::api::UserRead;
use log::{debug, error, info, trace, warn};
use std::fs::File;
use std::io::{BufReader, Read};
use std::ops::Deref;
use std::path::PathBuf;
use std::{collections::HashMap, io::Write};

pub struct UserDBLocal {
    source_files: Files,
    source_hashes: Hashes, // to detect changes
    pub users: HashMap<String, crate::User>,
    pub groups: Vec<crate::Group>,
}

pub struct Files {
    pub passwd: Option<PathBuf>,
    pub shadow: Option<PathBuf>,
    pub group: Option<PathBuf>,
}

impl Default for Files {
    /// use the default Linux `/etc/` paths
    fn default() -> Self {
        Self {
            passwd: Some(PathBuf::from("/etc/passwd")),
            shadow: Some(PathBuf::from("/etc/shadow")),
            group: Some(PathBuf::from("/etc/group")),
        }
    }
}

impl Files {
    /// Check if all the files are defined. Because some operations require the files to be present
    pub fn is_virtual(&self) -> bool {
        !(self.group.is_some() & self.passwd.is_some() & self.shadow.is_some())
    }
    pub fn lock_and_get_passwd(&self) -> Result<LockedFileGuard, crate::UserLibError> {
        let path = self.passwd.as_ref();
        match path {
            Some(p) => LockedFileGuard::new(p),
            None => Err(crate::UserLibError::FilesRequired),
        }
    }
    pub fn lock_and_get_shadow(&self) -> Result<LockedFileGuard, crate::UserLibError> {
        let path = self.shadow.as_ref();
        match path {
            Some(p) => LockedFileGuard::new(p),
            None => Err(crate::UserLibError::FilesRequired),
        }
    }
    pub fn lock_and_get_group(&self) -> Result<LockedFileGuard, crate::UserLibError> {
        let path = self.group.as_ref();
        match path {
            Some(p) => LockedFileGuard::new(p),
            None => Err(crate::UserLibError::FilesRequired),
        }
    }

    pub fn lock_all_get(
        &self,
    ) -> Result<(LockedFileGuard, LockedFileGuard, LockedFileGuard), crate::UserLibError> {
        let pwd = self.lock_and_get_passwd()?;
        let shd = self.lock_and_get_shadow()?;
        let grp = self.lock_and_get_group()?;
        Ok((pwd, shd, grp))
    }
}

pub struct LockedFileGuard {
    lockfile: PathBuf,
    path: PathBuf,
    pub(crate) file: File,
}

impl LockedFileGuard {
    pub fn new(path: &PathBuf) -> Result<Self, crate::UserLibError> {
        let locked = Self::try_to_lock_file(path);
        match locked {
            Ok((lockfile, file)) => Ok(Self {
                lockfile,
                path: path.to_owned(),
                file,
            }),
            Err(e) => Err(e),
        }
    }

    pub fn replace_contents(&mut self, new_content: String) -> Result<(), crate::UserLibError> {
        self.file = File::create(&self.path).expect("Failed to truncate file.");
        self.file
            .write_all(&new_content.into_bytes())
            .expect("Failed to write all users.");
        Ok(())
    }

    /// This function tries to lock a file in the way other passwd locking mechanisms work.
    ///
    /// * get the pid
    /// * create the temporary lockfilepath "/etc/passwd.12397"
    /// * create the lockfilepath "/etc/passwd.lock"
    /// * open the temporary file
    /// * write the pid to the tempfile
    /// * try to make a link from the temporary file created to the lockfile
    /// * ensure that the file has been linked successfully
    ///
    /// when the link could not be created:
    ///
    /// * Open the lockfile
    /// * read the contents of the lockfile
    /// * check if the lockfile contains a pid if not error out
    /// * check if the containing pid is in a valid format. If not create a matching error
    ///
    /// not implemented yet:
    ///
    /// * test if this process could be killed. If so disclose the pid in the error.
    /// * try to delete the lockfile as it is apparently not used by the process anmore. (cleanup)
    /// * try to lock again now that the old logfile has been safely removed.
    /// * remove the original file and only keep the lock hardlink
    fn try_to_lock_file(path: &PathBuf) -> Result<(PathBuf, File), crate::UserLibError> {
        struct TempLockFile {
            tlf: PathBuf,
        }
        impl Drop for TempLockFile {
            fn drop(&mut self) {
                info!("removing temporary lockfile {}", self.tlf.to_str().unwrap());
                std::fs::remove_file(&self.tlf).unwrap();
            }
        }
        impl Deref for TempLockFile {
            type Target = PathBuf;
            fn deref(&self) -> &PathBuf {
                &self.tlf
            }
        }

        info!("locking file {}", path.to_string_lossy());
        let mut tempfilepath_const = path.clone();
        // get the pid
        let pid = std::process::id();
        debug!("using pid {}", std::process::id());
        // get the filename
        let filename = tempfilepath_const.file_name().unwrap().to_owned();
        // and the base path which is the base for tempfile and lockfile.
        tempfilepath_const.pop();
        let mut lockfilepath = tempfilepath_const.clone();
        // push the filenames to the paths
        tempfilepath_const.push(format!("{}.{}", filename.to_str().unwrap(), pid));
        let tempfilepath = TempLockFile {
            tlf: tempfilepath_const,
        };
        lockfilepath.push(format!("{}.lock", filename.to_str().unwrap()));
        debug!(
            "Lockfile paths: {:?} (temporary) {:?} (final)",
            *tempfilepath, lockfilepath
        );
        // write the pid into the tempfile
        {
            let mut tempfile = File::create(&*tempfilepath)
                .expect(&format!("Failed to open {}", filename.to_str().unwrap()));
            match write!(tempfile, "{}", pid) {
                Ok(_) => {}
                Err(_) => error!("could not write to {}", filename.to_string_lossy()),
            };
        }

        // try to make a hardlink from the lockfile to the tempfile
        let linkresult = std::fs::hard_link(&*tempfilepath, &lockfilepath);
        match linkresult {
            Ok(()) => {
                debug!("successfully locked");

                // open the file
                let resfile = File::open(&path);
                return match resfile {
                    Ok(file) => Ok((lockfilepath, file)),
                    Err(e) => {
                        // failed to open the file undo the locks
                        let _ = std::fs::remove_file(&lockfilepath);
                        let ret: crate::UserLibError = format!(
                            "Failed to open the file: {}, error: {}",
                            path.to_str().unwrap(),
                            e
                        )
                        .into();
                        Err(ret)
                    }
                };
            }
            Err(e) => match e.kind() {
                // analyze the error further
                std::io::ErrorKind::AlreadyExists => {
                    warn!("The file is already locked by another process! – testing the validity of the lock");
                    {
                        let mut lf = match File::open(&lockfilepath) {
                            Ok(file) => file,
                            Err(e) => {
                                panic!("failed to open the lockfile: {}", e);
                            }
                        };
                        let mut content = String::new();
                        match lf.read_to_string(&mut content) {
                            Ok(_) => {}
                            Err(_) => {
                                panic!("failed to read the lockfile{}", e);
                            }
                        }
                        let content = content.trim().trim_matches(char::from(0));
                        let lock_pid = content.parse::<u32>();
                        match lock_pid {
                            Ok(pid) => {
                                warn!(
                                    "found a pid: {}, checking if this process is still running",
                                    pid
                                );
                                error!("The file could not be locked");
                                todo!("Validate the lock and delete the file if the process does not exist anymore");
                                /*let sent = nix::sys::signal::kill(
                                    nix::unistd::Pid::from_raw(pid as i32),
                                    nix::sys::signal::Signal::from(0),
                                );*/
                            }
                            Err(e) => error!(
                                "existing lock file {} with an invalid PID '{}' Error: {}",
                                lockfilepath.to_str().unwrap(),
                                content,
                                e
                            ),
                        }
                    }
                }

                _ => {
                    panic!("failed to lock the file: {}", e);
                }
            },
        }
        Err("was not able to lock!".into())
    }
}

impl Drop for LockedFileGuard {
    fn drop(&mut self) {
        info!("removing lock");
        std::fs::remove_file(&self.lockfile).unwrap();
    }
}

impl UserDBLocal {
    /// Import the database from strings
    #[must_use]
    pub fn import_from_strings(
        passwd_content: &str,
        shadow_content: &str,
        group_content: &str,
    ) -> Self {
        let shadow_entries: Vec<crate::Shadow> = string_to(&shadow_content);
        let mut users = user_vec_to_hashmap(string_to(&passwd_content));
        let groups = string_to(&group_content);
        shadow_to_users(&mut users, shadow_entries);
        let res = Self {
            source_files: Files {
                passwd: None,
                group: None,
                shadow: None,
            },
            users,
            groups,
            source_hashes: Hashes::new(&passwd_content, &shadow_content, &group_content),
        };
        res
    }

    /// Import the database from a [`Files`] struct
    #[must_use]
    pub fn load_files(files: Files) -> Result<Self, crate::UserLibError> {
        // Get the Strings for the files use an inner block to drop references after read.
        let (my_passwd_lines, my_shadow_lines, my_group_lines) = {
            let opened = files.lock_all_get();
            let (locked_p, locked_s, locked_g) = opened.expect("failed to lock files!");
            // read the files to strings
            let p = file_to_string(&locked_p.file)?;
            let s = file_to_string(&locked_s.file)?;
            let g = file_to_string(&locked_g.file)?;
            // return the strings to the outer scope and release the lock...
            (p, s, g)
        };

        let mut users = user_vec_to_hashmap(string_to(&my_passwd_lines));
        let passwds: Vec<crate::Shadow> = string_to(&my_shadow_lines);
        shadow_to_users(&mut users, passwds);
        Ok(Self {
            source_files: files,
            users,
            groups: string_to(&my_group_lines),
            source_hashes: Hashes::new(&my_passwd_lines, &my_shadow_lines, &my_group_lines),
        })
    }
}

use crate::api::UserDBWrite;
impl UserDBWrite for UserDBLocal {
    fn delete_user(&mut self, username: &str) -> Result<crate::User, crate::UserLibError> {
        // try to get the user from the database
        let user_opt = self.users.get(username);
        let user = match user_opt {
            Some(user) => user,
            None => {
                return Err(crate::UserLibError::NotFound);
            }
        };

        if self.source_files.is_virtual() {
            warn!("There are no associated files working in dummy mode!");
            let res = self.users.remove(username);
            match res {
                Some(u) => Ok(u),
                None => Err(crate::UserLibError::NotFound),
            }
        } else {
            let opened = self.source_files.lock_all_get();
            let (mut locked_p, locked_s, locked_g) = opened.expect("failed to lock files!");

            // read the files to strings
            let p = file_to_string(&locked_p.file)?;
            let _s = file_to_string(&locked_s.file)?;
            let _g = file_to_string(&locked_g.file)?;
            {
                if self.source_hashes.passwd.has_changed(&p) {
                    error!("The source files have changed. Deleting the user could corrupt the userdatabase. Aborting!");
                } else {
                    // create the new content of passwd
                    let modified = user.remove_in(&p);
                    // write the new content to the file.
                    let ncont = locked_p.replace_contents(modified);
                    match ncont {
                        Ok(_) => {
                            let res = self.users.remove(username);
                            return Ok(res.unwrap());
                        }
                        Err(_) => {
                            return Err("Error during write to the database. \
                        Please doublecheck as the userdatabase could be corrupted: {}"
                                .into());
                        }
                    }
                }
                Err(format!("The user has been changed {}", username).into())
            }
        }
    }

    fn new_user(
        &mut self, /*
                   username: String,
                   enc_password: String,
                   uid: u32,
                   gid: u32,
                   full_name: String,
                   room: String,
                   phone_work: String,
                   phone_home: String,
                   other: Option<Vec<String>>,
                   home_dir: String,
                   shell_path: String,*/
    ) -> Result<&crate::User, crate::UserLibError> {
        /*if self.users.contains_key(&username) {
            Err(format!(
                "The username {} already exists! Aborting!",
                username
            )
            .into())
        } else {
            let pwd = if self.source_files.shadow.is_none(){
                crate::Password::Encrypted(crate::EncryptedPassword{});
            }
            else{
                crate::Password::Shadow(crate::Shadow{})
            }
            self.users.insert(
                username,
                crate::User {
                    username: crate::Username { username },
                    password:,
                    uid: crate::Uid{uid},
                    gid:crate::Gid{gid},
                    gecos: crate::Gecos{},
                    home_dir:crate::HomeDir{dir: home_dir},
                    shell_path: crate::ShellPath{shell: shell_path},
                },
            )
        }*/
        todo!()
    }

    fn delete_group(&mut self, _group: &crate::Group) -> Result<(), crate::UserLibError> {
        todo!()
    }

    fn new_group(&mut self) -> Result<&crate::Group, crate::UserLibError> {
        todo!()
    }
}

use crate::api::UserDBRead;
impl UserDBRead for UserDBLocal {
    fn get_all_users(&self) -> Vec<&crate::User> {
        let mut res: Vec<&crate::User> = self.users.iter().map(|(_, x)| x).collect();
        res.sort();
        res
    }

    fn get_user_by_name(&self, name: &str) -> Option<&crate::User> {
        self.users.get(name)
    }

    fn get_user_by_id(&self, uid: u32) -> Option<&crate::User> {
        // could probably be more efficient - on the other hand its no problem to loop a thousand users.
        for (_, user) in self.users.iter() {
            if user.get_uid() == uid {
                return Some(&user);
            }
        }
        None
    }

    fn get_all_groups(&self) -> Vec<&crate::Group> {
        self.groups.iter().collect()
    }

    fn get_group_by_name(&self, name: &str) -> Option<&crate::Group> {
        for group in self.groups.iter() {
            if group.get_groupname()? == name {
                return Some(group);
            }
        }
        None
    }

    fn get_group_by_id(&self, id: u32) -> Option<&crate::Group> {
        for group in self.groups.iter() {
            if group.get_gid()? == id {
                return Some(group);
            }
        }
        None
    }
}

use crate::api::UserDBValidation;
impl UserDBValidation for UserDBLocal {
    fn is_uid_valid_and_free(&self, uid: u32) -> bool {
        warn!("No valid check, only free check");
        let free = self.users.iter().all(|(_, u)| u.get_uid() != uid);
        free
    }

    fn is_username_valid_and_free(&self, name: &str) -> bool {
        let valid = crate::user::passwd_fields::is_username_valid(name);
        let free = self.get_user_by_name(name).is_none();
        valid && free
    }

    fn is_gid_valid_and_free(&self, gid: u32) -> bool {
        warn!("No valid check, only free check");
        self.groups.iter().all(|x| x.get_gid().unwrap() != gid)
    }

    fn is_groupname_valid_and_free(&self, name: &str) -> bool {
        let valid = crate::group::is_groupname_valid(name);
        let free = self
            .groups
            .iter()
            .all(|x| x.get_groupname().unwrap() != name);
        valid && free
    }
}

pub struct SourceHash {
    hashvalue: String,
}

impl SourceHash {
    pub fn new(src: &str) -> Self {
        Self {
            hashvalue: src.to_owned(),
        }
    }
    pub fn has_changed(&self, new: &str) -> bool {
        trace!(
            "Old and new lengths: {}, {}",
            self.hashvalue.len(),
            new.len()
        );
        !self.hashvalue.eq(new)
    }
}

pub struct Hashes {
    pub passwd: SourceHash,
    pub shadow: SourceHash,
    pub group: SourceHash,
}

impl Hashes {
    pub fn new(passwd: &str, shadow: &str, group: &str) -> Self {
        Self {
            passwd: SourceHash::new(passwd),
            shadow: SourceHash::new(shadow),
            group: SourceHash::new(group),
        }
    }
}

/// Parse a file to a string
fn file_to_string(file: &File) -> Result<String, crate::UserLibError> {
    let mut reader = BufReader::new(file);
    let mut lines = String::new();
    let res = reader.read_to_string(&mut lines);
    match res {
        Ok(_) => Ok(lines),
        Err(e) => Err(format!("failed to read the file: {:?}", e).into()),
    }
}

/// Merge the Shadow passwords into the users
fn shadow_to_users(
    users: &mut HashMap<String, crate::User>,
    shadow: Vec<crate::Shadow>,
) -> &mut HashMap<String, crate::User> {
    for pass in shadow {
        let user = users
            .get_mut(pass.get_username())
            .expect(&format!("the user {} does not exist", pass.get_username()));
        user.password = crate::Password::Shadow(pass);
    }
    users
}

/// Convert a `Vec<crate::User>` to a `HashMap<String, crate::User>` where the username is used as key
fn user_vec_to_hashmap(users: Vec<crate::User>) -> HashMap<String, crate::User> {
    users
        .into_iter()
        .map(|x| {
            (
                x.get_username()
                    .expect("An empty username is not supported")
                    .to_owned(),
                x,
            )
        })
        .collect()
}

/// Try to parse a String into some Object
///
/// # Errors
/// if the parsing failed a [`UserLibError::Message`](crate::userlib_error::UserLibError::Message) is returned containing a more detailed error message.
pub trait NewFromString {
    fn new_from_string(line: String, position: u32) -> Result<Self, crate::UserLibError>
    where
        Self: Sized;
}

/// A generic function that parses a string line by line and creates the appropriate `Vec<T>` requested by the type system.
fn string_to<T>(source: &str) -> Vec<T>
where
    T: NewFromString,
{
    source
        .lines()
        .enumerate()
        .filter_map(|(n, line)| {
            if line.len() > 5 {
                Some(T::new_from_string(line.to_owned(), n as u32).expect("failed to read lines"))
            } else {
                None
            }
        })
        .collect()
}

#[test]
fn test_creator_user_db_local() {
    let data = UserDBLocal::import_from_strings("test:x:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test", "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::", "teste:x:1002:test,test");
    assert_eq!(
        data.users.get("test").unwrap().get_username().unwrap(),
        "test"
    )
}

#[test]
fn test_parsing_local_database() {
    // Parse the worldreadable user database ignore the shadow database as this would require root privileges.
    let pwdfile = File::open(PathBuf::from("/etc/passwd")).unwrap();
    let grpfile = File::open(PathBuf::from("/etc/group")).unwrap();
    let my_passwd_lines = file_to_string(&pwdfile).unwrap();
    let my_group_lines = file_to_string(&grpfile).unwrap();
    let data = UserDBLocal::import_from_strings(&my_passwd_lines, "", &my_group_lines);
    assert_eq!(data.groups.get(0).unwrap().get_groupname().unwrap(), "root");
}

#[test]
fn test_user_db_read_implementation() {
    let pwdfile = File::open(PathBuf::from("/etc/passwd")).unwrap();
    let grpfile = File::open(PathBuf::from("/etc/group")).unwrap();
    let pass = file_to_string(&pwdfile).unwrap();
    let group = file_to_string(&grpfile).unwrap();
    let data = UserDBLocal::import_from_strings(&pass, "", &group);
    // Usually there are more than 10 users
    assert!(data.get_all_users().len() > 10);
    assert!(data.get_user_by_name("root").is_some());
    assert_eq!(data.get_user_by_name("root").unwrap().get_uid(), 0);
    assert_eq!(
        data.get_user_by_id(0).unwrap().get_username().unwrap(),
        "root"
    );
    assert!(data.get_all_groups().len() > 10);
    assert!(data.get_group_by_name("root").is_some());
    assert_eq!(
        data.get_group_by_name("root").unwrap().get_gid().unwrap(),
        0
    );
    assert_eq!(
        data.get_group_by_id(0).unwrap().get_groupname().unwrap(),
        "root"
    );
    assert!(data.get_user_by_name("norealnameforsure").is_none());
    assert!(data.get_group_by_name("norealgroupforsure").is_none());
}

#[test]
fn test_user_db_write_implementation() {
    let mut data = UserDBLocal::import_from_strings("test:x:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test", "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::", "teste:x:1002:test,test");
    let user = "test";

    assert_eq!(data.get_all_users().len(), 1);
    assert!(data.delete_user(&user).is_ok());
    assert!(data.delete_user(&user).is_err());
    assert_eq!(data.get_all_users().len(), 0);
}