use kanidm_unix_common::client_sync::DaemonClientBlocking;
use kanidm_unix_common::unix_config::PamNssConfig;
use kanidm_unix_common::unix_passwd::{
    read_etc_group_file, read_etc_passwd_file, EtcGroup, EtcUser,
};
use kanidm_unix_common::unix_proto::{ClientRequest, ClientResponse, NssGroup, NssUser};

use libnss::group::Group;
use libnss::interop::Response;
use libnss::passwd::Passwd;

#[cfg(test)]
use kanidm_unix_common::client_sync::UnixStream;

pub enum RequestOptions {
    Main {
        config_path: &'static str,
    },
    #[cfg(test)]
    Test {
        socket: Option<UnixStream>,
        users: Vec<EtcUser>,
        groups: Vec<EtcGroup>,
    },
}

enum Source {
    Daemon(DaemonClientBlocking),
    Fallback {
        users: Vec<EtcUser>,
        groups: Vec<EtcGroup>,
    },
}

impl RequestOptions {
    fn connect_to_daemon(self) -> Source {
        match self {
            RequestOptions::Main { config_path } => {
                let maybe_client = PamNssConfig::new()
                    .read_options_from_optional_config(config_path)
                    .ok()
                    .and_then(|cfg| {
                        DaemonClientBlocking::new(cfg.sock_path.as_str(), cfg.unix_sock_timeout)
                            .ok()
                    });

                if let Some(client) = maybe_client {
                    Source::Daemon(client)
                } else {
                    let users = read_etc_passwd_file("/etc/passwd").unwrap_or_default();

                    let groups = read_etc_group_file("/etc/group").unwrap_or_default();

                    Source::Fallback { users, groups }
                }
            }
            #[cfg(test)]
            RequestOptions::Test {
                socket,
                users,
                groups,
            } => {
                if let Some(socket) = socket {
                    Source::Daemon(DaemonClientBlocking::from(socket))
                } else {
                    Source::Fallback { users, groups }
                }
            }
        }
    }
}

pub fn get_all_user_entries(req_options: RequestOptions) -> Response<Vec<Passwd>> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssAccounts;

            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssAccounts(l) => {
                        l.into_iter().map(passwd_from_nssuser).collect()
                    }
                    _ => Vec::new(),
                })
                .map(Response::Success)
                .unwrap_or_else(|_| Response::Success(vec![]))
        }
        Source::Fallback { users, groups: _ } => {
            if users.is_empty() {
                return Response::Unavail;
            }

            let users = users.into_iter().map(passwd_from_etcuser).collect();

            Response::Success(users)
        }
    }
}

pub fn get_user_entry_by_uid(uid: libc::uid_t, req_options: RequestOptions) -> Response<Passwd> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssAccountByUid(uid);
            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssAccount(opt) => opt
                        .map(passwd_from_nssuser)
                        .map(Response::Success)
                        .unwrap_or_else(|| Response::NotFound),
                    _ => Response::NotFound,
                })
                .unwrap_or_else(|_| Response::NotFound)
        }
        Source::Fallback { users, groups: _ } => {
            if users.is_empty() {
                return Response::Unavail;
            }

            let user = users
                .into_iter()
                .filter_map(|etcuser| {
                    if etcuser.uid == uid {
                        Some(passwd_from_etcuser(etcuser))
                    } else {
                        None
                    }
                })
                .next();

            if let Some(user) = user {
                Response::Success(user)
            } else {
                Response::NotFound
            }
        }
    }
}

pub fn get_user_entry_by_name(name: String, req_options: RequestOptions) -> Response<Passwd> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssAccountByName(name);
            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssAccount(opt) => opt
                        .map(passwd_from_nssuser)
                        .map(Response::Success)
                        .unwrap_or_else(|| Response::NotFound),
                    _ => Response::NotFound,
                })
                .unwrap_or_else(|_| Response::NotFound)
        }
        Source::Fallback { users, groups: _ } => {
            if users.is_empty() {
                return Response::Unavail;
            }

            let user = users
                .into_iter()
                .filter_map(|etcuser| {
                    if etcuser.name == name {
                        Some(passwd_from_etcuser(etcuser))
                    } else {
                        None
                    }
                })
                .next();

            if let Some(user) = user {
                Response::Success(user)
            } else {
                Response::NotFound
            }
        }
    }
}

pub fn get_all_group_entries(req_options: RequestOptions) -> Response<Vec<Group>> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssGroups;
            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssGroups(l) => {
                        l.into_iter().map(group_from_nssgroup).collect()
                    }
                    _ => Vec::new(),
                })
                .map(Response::Success)
                .unwrap_or_else(|_| Response::Success(vec![]))
        }
        Source::Fallback { users: _, groups } => {
            if groups.is_empty() {
                return Response::Unavail;
            }

            let groups = groups.into_iter().map(group_from_etcgroup).collect();

            Response::Success(groups)
        }
    }
}

pub fn get_group_entry_by_gid(gid: libc::gid_t, req_options: RequestOptions) -> Response<Group> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssGroupByGid(gid);
            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssGroup(opt) => opt
                        .map(group_from_nssgroup)
                        .map(Response::Success)
                        .unwrap_or_else(|| Response::NotFound),
                    _ => Response::NotFound,
                })
                .unwrap_or_else(|_| Response::NotFound)
        }
        Source::Fallback { users: _, groups } => {
            if groups.is_empty() {
                return Response::Unavail;
            }

            let group = groups
                .into_iter()
                .filter_map(|etcgroup| {
                    if etcgroup.gid == gid {
                        Some(group_from_etcgroup(etcgroup))
                    } else {
                        None
                    }
                })
                .next();

            if let Some(group) = group {
                Response::Success(group)
            } else {
                Response::NotFound
            }
        }
    }
}

pub fn get_group_entry_by_name(name: String, req_options: RequestOptions) -> Response<Group> {
    match req_options.connect_to_daemon() {
        Source::Daemon(mut daemon_client) => {
            let req = ClientRequest::NssGroupByName(name);
            daemon_client
                .call_and_wait(&req, None)
                .map(|r| match r {
                    ClientResponse::NssGroup(opt) => opt
                        .map(group_from_nssgroup)
                        .map(Response::Success)
                        .unwrap_or_else(|| Response::NotFound),
                    _ => Response::NotFound,
                })
                .unwrap_or_else(|_| Response::NotFound)
        }
        Source::Fallback { users: _, groups } => {
            if groups.is_empty() {
                return Response::Unavail;
            }

            let group = groups
                .into_iter()
                .filter_map(|etcgroup| {
                    if etcgroup.name == name {
                        Some(group_from_etcgroup(etcgroup))
                    } else {
                        None
                    }
                })
                .next();

            if let Some(group) = group {
                Response::Success(group)
            } else {
                Response::NotFound
            }
        }
    }
}

fn passwd_from_etcuser(etc: EtcUser) -> Passwd {
    Passwd {
        name: etc.name,
        gecos: etc.gecos,
        passwd: "x".to_string(),
        uid: etc.uid,
        gid: etc.gid,
        dir: etc.homedir,
        shell: etc.shell,
    }
}

fn passwd_from_nssuser(nu: NssUser) -> Passwd {
    Passwd {
        name: nu.name,
        gecos: nu.gecos,
        passwd: "x".to_string(),
        uid: nu.uid,
        gid: nu.gid,
        dir: nu.homedir,
        shell: nu.shell,
    }
}

fn group_from_etcgroup(etc: EtcGroup) -> Group {
    Group {
        name: etc.name,
        passwd: "x".to_string(),
        gid: etc.gid,
        members: etc.members,
    }
}

fn group_from_nssgroup(ng: NssGroup) -> Group {
    Group {
        name: ng.name,
        passwd: "x".to_string(),
        gid: ng.gid,
        members: ng.members,
    }
}