use crate::{ClientError, KanidmClient};
use kanidm_proto::constants::*;
use kanidm_proto::internal::{CredentialStatus, IdentifyUserRequest, IdentifyUserResponse};
use kanidm_proto::v1::{AccountUnixExtend, Entry, SingleStringRequest, UatStatus};
use std::collections::BTreeMap;
use uuid::Uuid;

impl KanidmClient {
    pub async fn idm_person_account_list(&self) -> Result<Vec<Entry>, ClientError> {
        self.perform_get_request("/v1/person").await
    }

    pub async fn idm_person_account_get(&self, id: &str) -> Result<Option<Entry>, ClientError> {
        self.perform_get_request(format!("/v1/person/{id}").as_str())
            .await
    }

    pub async fn idm_person_search(&self, id: &str) -> Result<Vec<Entry>, ClientError> {
        self.perform_get_request(format!("/v1/person/_search/{id}").as_str())
            .await
    }

    pub async fn idm_person_account_create(
        &self,
        name: &str,
        displayname: &str,
    ) -> Result<(), ClientError> {
        let mut new_acct = Entry {
            attrs: BTreeMap::new(),
        };
        new_acct
            .attrs
            .insert(ATTR_NAME.to_string(), vec![name.to_string()]);
        new_acct
            .attrs
            .insert(ATTR_DISPLAYNAME.to_string(), vec![displayname.to_string()]);
        self.perform_post_request("/v1/person", new_acct).await
    }

    pub async fn idm_person_account_update(
        &self,
        id: &str,
        newname: Option<&str>,
        displayname: Option<&str>,
        legalname: Option<&str>,
        mail: Option<&[String]>,
    ) -> Result<(), ClientError> {
        let mut update_entry = Entry {
            attrs: BTreeMap::new(),
        };

        if let Some(newname) = newname {
            update_entry
                .attrs
                .insert(ATTR_NAME.to_string(), vec![newname.to_string()]);
        }
        if let Some(newdisplayname) = displayname {
            update_entry.attrs.insert(
                ATTR_DISPLAYNAME.to_string(),
                vec![newdisplayname.to_string()],
            );
        }
        if let Some(newlegalname) = legalname {
            // An empty string means we should purge the attribute, which
            // is done by inserting an empty vec as the entry value
            let val = if newlegalname.is_empty() {
                vec![]
            } else {
                vec![newlegalname.to_string()]
            };
            update_entry.attrs.insert(ATTR_LEGALNAME.to_string(), val);
        }
        if let Some(mail) = mail {
            update_entry
                .attrs
                .insert(ATTR_MAIL.to_string(), mail.to_vec());
        }

        self.perform_patch_request(format!("/v1/person/{id}").as_str(), update_entry)
            .await
    }

    pub async fn idm_person_account_delete(&self, id: &str) -> Result<(), ClientError> {
        self.perform_delete_request(format!("/v1/person/{id}").as_str())
            .await
    }

    pub async fn idm_person_account_add_attr(
        &self,
        id: &str,
        attr: &str,
        values: &[&str],
    ) -> Result<(), ClientError> {
        let msg: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
        self.perform_post_request(format!("/v1/person/{id}/_attr/{attr}").as_str(), msg)
            .await
    }

    pub async fn idm_person_account_set_attr(
        &self,
        id: &str,
        attr: &str,
        values: &[&str],
    ) -> Result<(), ClientError> {
        let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
        self.perform_put_request(format!("/v1/person/{id}/_attr/{attr}").as_str(), m)
            .await
    }

    pub async fn idm_person_account_get_attr(
        &self,
        id: &str,
        attr: &str,
    ) -> Result<Option<Vec<String>>, ClientError> {
        self.perform_get_request(format!("/v1/person/{id}/_attr/{attr}").as_str())
            .await
    }

    pub async fn idm_person_account_purge_attr(
        &self,
        id: &str,
        attr: &str,
    ) -> Result<(), ClientError> {
        self.perform_delete_request(format!("/v1/person/{id}/_attr/{attr}").as_str())
            .await
    }

    pub async fn idm_person_account_get_credential_status(
        &self,
        id: &str,
    ) -> Result<CredentialStatus, ClientError> {
        let res: Result<CredentialStatus, ClientError> = self
            .perform_get_request(format!("/v1/person/{id}/_credential/_status").as_str())
            .await;
        res.and_then(|cs| {
            if cs.creds.is_empty() {
                Err(ClientError::EmptyResponse)
            } else {
                Ok(cs)
            }
        })
    }

    // This helper calls through the credential update session wrappers to
    pub async fn idm_person_account_primary_credential_set_password(
        &self,
        id: &str,
        pw: &str,
    ) -> Result<(), ClientError> {
        let (session_tok, status) = self.idm_account_credential_update_begin(id).await?;
        trace!(?status);

        let status = self
            .idm_account_credential_update_set_password(&session_tok, pw)
            .await?;
        trace!(?status);

        self.idm_account_credential_update_commit(&session_tok)
            .await
    }

    pub async fn idm_person_account_post_ssh_pubkey(
        &self,
        id: &str,
        tag: &str,
        pubkey: &str,
    ) -> Result<(), ClientError> {
        let tag = urlencoding::encode(tag);
        let sk = (tag, pubkey.to_string());
        self.perform_post_request(format!("/v1/person/{id}/_ssh_pubkeys").as_str(), sk)
            .await
    }

    pub async fn idm_person_account_delete_ssh_pubkey(
        &self,
        id: &str,
        tag: &str,
    ) -> Result<(), ClientError> {
        let tag = urlencoding::encode(tag);
        self.perform_delete_request(format!("/v1/person/{id}/_ssh_pubkeys/{tag}").as_str())
            .await
    }

    pub async fn idm_person_account_unix_extend(
        &self,
        id: &str,
        gidnumber: Option<u32>,
        shell: Option<&str>,
    ) -> Result<(), ClientError> {
        let ux = AccountUnixExtend {
            shell: shell.map(str::to_string),
            gidnumber,
        };
        self.perform_post_request(format!("/v1/person/{id}/_unix").as_str(), ux)
            .await
    }

    pub async fn idm_person_account_unix_cred_put(
        &self,
        id: &str,
        cred: &str,
    ) -> Result<(), ClientError> {
        let req = SingleStringRequest {
            value: cred.to_string(),
        };
        self.perform_put_request(
            ["/v1/person/", id, "/_unix/_credential"].concat().as_str(),
            req,
        )
        .await
    }

    pub async fn idm_person_account_unix_cred_delete(&self, id: &str) -> Result<(), ClientError> {
        self.perform_delete_request(["/v1/person/", id, "/_unix/_credential"].concat().as_str())
            .await
    }

    pub async fn idm_person_identify_user(
        &self,
        id: &str,
        request: IdentifyUserRequest,
    ) -> Result<IdentifyUserResponse, ClientError> {
        self.perform_post_request(
            ["/v1/person/", id, "/_identify_user"].concat().as_str(),
            request,
        )
        .await
    }

    pub async fn idm_account_radius_credential_get(
        &self,
        id: &str,
    ) -> Result<Option<String>, ClientError> {
        self.perform_get_request(format!("/v1/person/{id}/_radius").as_str())
            .await
    }

    pub async fn idm_account_radius_credential_regenerate(
        &self,
        id: &str,
    ) -> Result<String, ClientError> {
        self.perform_post_request(format!("/v1/person/{id}/_radius").as_str(), ())
            .await
    }

    pub async fn idm_account_radius_credential_delete(&self, id: &str) -> Result<(), ClientError> {
        self.perform_delete_request(format!("/v1/person/{id}/_radius").as_str())
            .await
    }

    pub async fn idm_account_list_user_auth_token(
        &self,
        id: &str,
    ) -> Result<Vec<UatStatus>, ClientError> {
        self.perform_get_request(format!("/v1/account/{id}/_user_auth_token").as_str())
            .await
    }

    pub async fn idm_account_destroy_user_auth_token(
        &self,
        id: &str,
        token_id: Uuid,
    ) -> Result<(), ClientError> {
        self.perform_delete_request(
            format!(
                "/v1/account/{}/_user_auth_token/{}",
                id,
                &token_id.to_string()
            )
            .as_str(),
        )
        .await
    }

    pub async fn idm_person_certificate_list(&self, id: &str) -> Result<Vec<Entry>, ClientError> {
        self.perform_get_request(format!("/v1/person/{id}/_certificate").as_str())
            .await
    }

    pub async fn idm_person_certificate_create(
        &self,
        id: &str,
        pem_data: &str,
    ) -> Result<(), ClientError> {
        let mut new_cert = Entry {
            attrs: BTreeMap::new(),
        };
        new_cert
            .attrs
            .insert(ATTR_CERTIFICATE.to_string(), vec![pem_data.to_string()]);
        self.perform_post_request(format!("/v1/person/{id}/_certificate").as_str(), new_cert)
            .await
    }
}
