use crate::{Group, InstallError, InstallMessage, User};
use std::{
    fs::OpenOptions,
    io::{Read, Seek, SeekFrom, Write},
    path::PathBuf,
    process::{Command, Output},
    sync::mpsc::Sender,
    thread,
};

#[derive(Debug, Clone)]
pub struct Hooks {
    root: PathBuf,
    man: bool,
    glib_schema: bool,
    info: Vec<PathBuf>,
    pinstall: Vec<PathBuf>,
    users: Vec<User>,
    groups: Vec<Group>,
    shells: Vec<PathBuf>,
}

impl Default for Hooks {
    fn default() -> Self {
        Self {
            root: PathBuf::from("/"),
            man: false,
            glib_schema: false,
            info: vec![],
            pinstall: vec![],
            users: vec![],
            groups: vec![],
            shells: vec![],
        }
    }
}

impl Hooks {
    pub fn new(root: &str) -> Self {
        let mut h = Self::default();
        h.root = PathBuf::from(PathBuf::from(root));
        h
    }

    pub fn push_man(&mut self) {
        self.man = true;
    }

    pub fn push_glib_schema(&mut self) {
        self.glib_schema = true;
    }

    pub fn push_info(&mut self, file: &str) {
        self.info.push(PathBuf::from(file));
    }

    pub fn push_pinstall(&mut self, pinstall: PathBuf) {
        self.pinstall.push(pinstall);
    }

    pub fn push_user(&mut self, user: User) {
        self.users.push(user);
    }

    pub fn push_group(&mut self, group: Group) {
        self.groups.push(group);
    }

    pub fn push_shell(&mut self, shell: PathBuf) {
        self.shells.push(shell);
    }

    pub fn run(self, sender: Sender<InstallMessage>) -> Result<(), InstallError> {
        let Some(root) = self.root.to_str().map(|x| x.to_string()) else {
            return Err(InstallError::BadPath);
        };
        let mut threads = vec![];
        if !self.users.is_empty() {
            let sender = sender.clone();
            let root = root.clone();
            let users = self.users;
            threads.push(thread::spawn(move || {
                create_users(users, &root, sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if !self.groups.is_empty() {
            let sender = sender.clone();
            let root = root.clone();
            let groups = self.groups;
            threads.push(thread::spawn(move || {
                create_groups(groups, &root, sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if !self.shells.is_empty() {
            let sender = sender.clone();
            let root = root.clone();
            let shells = self.shells;
            threads.push(thread::spawn(move || {
                append_shells(shells, &root, sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if self.man && root.as_str() == "/" {
            let sender = sender.clone();
            threads.push(thread::spawn(move || {
                install_man(sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if self.glib_schema {
            let sender = sender.clone();
            let root = root.clone();
            threads.push(thread::spawn(move || {
                compile_schemas(&root, sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if !self.info.is_empty() {
            let sender = sender.clone();
            let root = PathBuf::from(&root);
            let pages = self.info;
            threads.push(thread::spawn(move || {
                install_info(pages, root, sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        if !self.pinstall.is_empty() {
            let scripts = self.pinstall;
            threads.push(thread::spawn(move || {
                run_pinstall(scripts, root.into(), sender)?;
                Ok::<(), InstallError>(())
            }));
        }
        for th in threads {
            if let Err(e) = th.join() {
                eprintln!("{e:?}");
            }
        }
        Ok(())
    }
}

pub fn append_shells(
    shells: Vec<PathBuf>,
    root: &str,
    sender: Sender<InstallMessage>,
) -> Result<(), InstallError> {
    shells.iter().try_for_each(|shell| {
        let s = PathBuf::from("/");
        let shell = s.join(shell);
        let Some(shell) = shell.to_str().map(|x| x.to_string()) else {
            return Err(InstallError::BadPath);
        };
        let mut f = PathBuf::from(root);
        f.push("etc");
        f.push("shells");
        let mut fd = OpenOptions::new().read(true).write(true).open(&f)?;
        let meta = fd.metadata()?;
        let len = meta.len();
        let pos = fd.seek(SeekFrom::Start(len - 1))?;
        if !pos == len - 1 {
            eprintln!("Bad seek operation in /etc/shells");
        }
        let mut buf = [0; 1];
        fd.read_exact(&mut buf)?;
        if !buf[0] == b'\n' {
            write!(fd, "\n")?;
        }
        write!(fd, "{shell}")?;
        sender.send(InstallMessage::ShellAdded(shell))?;
        Ok::<(), InstallError>(())
    })?;
    Ok(())
}

pub fn create_users(
    users: Vec<User>,
    root: &str,
    sender: Sender<InstallMessage>,
) -> Result<(), InstallError> {
    users.iter().try_for_each(|user| {
        let mut cmd = Command::new("useradd");
        cmd.args(["-rs", "/sbin/nologin", "-R", root]);
        if let Some(uid) = user.uid {
            cmd.args(["-u", &format!("{uid}")]);
        }
        cmd.arg(&user.name);
        let _output = cmd.output().map_err(Into::<InstallError>::into)?;
        sender.send(InstallMessage::UserCreated(user.clone()))?;
        Ok::<(), InstallError>(())
    })?;
    Ok(())
}

pub fn create_groups(
    groups: Vec<Group>,
    root: &str,
    sender: Sender<InstallMessage>,
) -> Result<(), InstallError> {
    groups.iter().try_for_each(|group| {
        let mut cmd = Command::new("groupadd");
        cmd.args(["-r", "-R", root]);
        if let Some(gid) = group.gid {
            cmd.args(["-u", &format!("{gid}")]);
        }
        cmd.arg(&group.name);
        let _output = cmd.output().map_err(Into::<InstallError>::into)?;
        sender.send(InstallMessage::GroupCreated(group.clone()))?;
        Ok::<(), InstallError>(())
    })?;
    Ok(())
}

pub fn install_man(sender: Sender<InstallMessage>) -> Result<Output, InstallError> {
    let output = Command::new("makewhatis")
        .output()
        .map_err(Into::<InstallError>::into)?;
    sender.send(InstallMessage::Man)?;
    Ok(output)
}

pub fn compile_schemas(root: &str, sender: Sender<InstallMessage>) -> Result<Output, InstallError> {
    let mut dir = PathBuf::from(root);
    dir.push("usr");
    dir.push("share");
    dir.push("glib-2.0");
    dir.push("schemas");
    let output = Command::new("glib-compile-schemas")
        .arg(dir.to_str().unwrap())
        .output()
        .map_err(Into::<InstallError>::into)?;
    sender.send(InstallMessage::GlibSchemas)?;
    Ok(output)
}

pub fn install_info(
    pages: Vec<PathBuf>,
    root: PathBuf,
    sender: Sender<InstallMessage>,
) -> Result<(), InstallError> {
    pages.iter().try_for_each(|page| {
        let page = root.join(page);
        let Some(dir) = page.parent() else {
                return Err(InstallError::BadPath);
            };
        let mut dir = dir.to_path_buf();
        dir.push("dir");
        let Some(page) = page.to_str() else {
                return Err(InstallError::BadPath);
            };
        let Some(dir) = dir.to_str() else {
                return Err(InstallError::BadPath);
            };
        Command::new("install-info")
            .args([page, dir])
            .output()
            .map_err(Into::<InstallError>::into)?;
        sender.send(InstallMessage::Info(page.to_string()))?;
        Ok(())
    })?;
    Ok(())
}

pub fn run_pinstall(
    scripts: Vec<PathBuf>,
    root: PathBuf,
    sender: Sender<InstallMessage>,
) -> Result<(), InstallError> {
    scripts.iter().try_for_each(|pinstall| {
        let p = root.clone();
        let pinstall = p.join(pinstall);
        let Some(root) = root.to_str() else {
            return Err(InstallError::BadPath);
        };
        let Some(pinstall) = pinstall.to_str() else {
            return Err(InstallError::BadPath);
        };
        let output = Command::new("/bin/sh")
            .arg(pinstall)
            .env("HPOK_ROOT", root)
            .output()
            .map_err(Into::<InstallError>::into)?;
        sender.send(InstallMessage::PostInstall(output))?;
        Ok(())
    })?;
    Ok(())
}
