use crate::authorization::permissions::Permission;
use crate::session::SessionHandle;
use crate::users::{db, UserRef};
use crate::CONFIG;
use api::general_capnp::optional;
use api::user_capnp::user::card_d_e_s_fire_e_v2::{
    BindParams, BindResults, GenCardTokenParams, GenCardTokenResults, GetMetaInfoParams,
    GetMetaInfoResults, GetSpaceInfoParams, GetSpaceInfoResults, GetTokenListParams,
    GetTokenListResults, UnbindParams, UnbindResults,
};
use api::user_capnp::user::{self, admin, card_d_e_s_fire_e_v2, info, manage};
use capnp::capability::Promise;
use capnp::Error;
use capnp_rpc::pry;
use std::borrow::Cow;
use std::io::Write;
use uuid::Uuid;

const TARGET: &str = "bffh::api::user";

#[derive(Clone)]
pub struct User {
    span: tracing::Span,
    session: SessionHandle,
    user: UserRef,
}

impl User {
    pub fn new(session: SessionHandle, user: UserRef) -> Self {
        let span = tracing::info_span!(target: TARGET, "User");
        Self {
            span,
            session,
            user,
        }
    }

    pub fn new_self(session: SessionHandle) -> Self {
        let user = session.get_user_ref();
        Self::new(session, user)
    }

    pub fn build_optional(
        session: &SessionHandle,
        user: Option<UserRef>,
        builder: optional::Builder<user::Owned>,
    ) {
        if let Some(user) = user.and_then(|u| session.users.get_user(u.get_username())) {
            let builder = builder.init_just();
            Self::fill(&session, user, builder);
        }
    }

    pub fn build(session: SessionHandle, builder: user::Builder) {
        let this = Self::new_self(session);
        let user = this.session.get_user();
        Self::fill(&this.session, user, builder);
    }

    pub fn fill(session: &SessionHandle, user: db::User, mut builder: user::Builder) {
        builder.set_username(user.id.as_str());

        // We have permissions on ourself
        let is_me = &session.get_user_ref().id == &user.id;

        let client = Self::new(session.clone(), UserRef::new(user.id));

        if is_me || session.has_perm(Permission::new("bffh.users.info")) {
            builder.set_info(capnp_rpc::new_client(client.clone()));
        }
        if is_me {
            builder.set_manage(capnp_rpc::new_client(client.clone()));
        }
        if session.has_perm(Permission::new("bffh.users.admin")) {
            builder.set_admin(capnp_rpc::new_client(client.clone()));
            builder.set_card_d_e_s_fire_e_v2(capnp_rpc::new_client(client));
        }
    }
}

impl info::Server for User {
    fn list_roles(
        &mut self,
        _: info::ListRolesParams,
        mut result: info::ListRolesResults,
    ) -> Promise<(), ::capnp::Error> {
        if let Some(user) = self.session.users.get_user(self.user.get_username()) {
            let mut builder = result.get().init_roles(user.userdata.roles.len() as u32);
            for (i, role) in user.userdata.roles.into_iter().enumerate() {
                let mut b = builder.reborrow().get(i as u32);
                b.set_name(role.as_str());
            }
        }
        Promise::ok(())
    }
}

impl manage::Server for User {
    fn pwd(
        &mut self,
        params: manage::PwdParams,
        _results: manage::PwdResults,
    ) -> Promise<(), ::capnp::Error> {
        let params = pry!(params.get());
        let old_pw = pry!(params.get_old_pwd());
        let new_pw = pry!(params.get_new_pwd());

        let uid = self.user.get_username();
        if let Some(mut user) = self.session.users.get_user(uid) {
            if let Ok(true) = user.check_password(old_pw.as_bytes()) {
                user.set_pw(new_pw.as_bytes());
                pry!(self.session.users.put_user(uid, &user));
            }
        }
        Promise::ok(())
    }
}

impl admin::Server for User {
    fn get_user_info_extended(
        &mut self,
        _: admin::GetUserInfoExtendedParams,
        _: admin::GetUserInfoExtendedResults,
    ) -> Promise<(), ::capnp::Error> {
        Promise::err(::capnp::Error::unimplemented(
            "method not implemented".to_string(),
        ))
    }
    fn add_role(
        &mut self,
        param: admin::AddRoleParams,
        _: admin::AddRoleResults,
    ) -> Promise<(), ::capnp::Error> {
        let rolename = pry!(pry!(pry!(param.get()).get_role()).get_name());

        if let Some(_role) = self.session.roles.get(rolename) {
            let mut target = self
                .session
                .users
                .get_user(self.user.get_username())
                .unwrap();

            // Only update if needed
            if !target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
                target.userdata.roles.push(rolename.to_string());
                pry!(self
                    .session
                    .users
                    .put_user(self.user.get_username(), &target));
            }
        }

        Promise::ok(())
    }
    fn remove_role(
        &mut self,
        param: admin::RemoveRoleParams,
        _: admin::RemoveRoleResults,
    ) -> Promise<(), ::capnp::Error> {
        let rolename = pry!(pry!(pry!(param.get()).get_role()).get_name());

        if let Some(_role) = self.session.roles.get(rolename) {
            let mut target = self
                .session
                .users
                .get_user(self.user.get_username())
                .unwrap();

            // Only update if needed
            if target.userdata.roles.iter().any(|r| r.as_str() == rolename) {
                target.userdata.roles.retain(|r| r.as_str() != rolename);
                pry!(self
                    .session
                    .users
                    .put_user(self.user.get_username(), &target));
            }
        }

        Promise::ok(())
    }
    fn pwd(
        &mut self,
        param: admin::PwdParams,
        _: admin::PwdResults,
    ) -> Promise<(), ::capnp::Error> {
        let new_pw = pry!(pry!(param.get()).get_new_pwd());
        let uid = self.user.get_username();
        if let Some(mut user) = self.session.users.get_user(uid) {
            user.set_pw(new_pw.as_bytes());
            pry!(self.session.users.put_user(uid, &user));
        }
        Promise::ok(())
    }
}

impl card_d_e_s_fire_e_v2::Server for User {
    fn get_token_list(
        &mut self,
        _: GetTokenListParams,
        mut results: GetTokenListResults,
    ) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "get_token_list").entered();
        tracing::trace!("method call");

        // TODO: This only supports a single token per user
        let user = pry!(self
            .session
            .users
            .get_user(self.user.get_username())
            .ok_or_else(|| Error::failed(format!(
                "User API object with nonexisting user \"{}\"",
                self.user.get_username()
            ))));
        if let Some(tk) = user.userdata.kv.get("cardtoken") {
            let b = results.get();
            let mut lb = b.init_token_list(1);
            lb.set(0, tk.as_ref());
        }
        Promise::ok(())
    }

    fn bind(&mut self, params: BindParams, _: BindResults) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "bind").entered();
        let params = pry!(params.get());
        let card_key = pry!(params.get_auth_key());
        let token = pry!(params.get_token());

        let token: Cow<'_, str> = if let Ok(uuid) = Uuid::from_slice(token) {
            Cow::Owned(
                uuid.to_hyphenated()
                    .encode_lower(&mut Uuid::encode_buffer())
                    .to_string(),
            )
        } else {
            Cow::Owned(hex::encode(token))
        };

        tracing::trace!(
            params.token = token.as_ref(),
            params.auth_key = "<censored>",
            "method call"
        );

        let card_key = hex::encode(card_key);

        let mut user = pry!(self
            .session
            .users
            .get_user(self.user.get_username())
            .ok_or_else(|| Error::failed(format!(
                "User API object with nonexisting user \"{}\"",
                self.user.get_username()
            ))));

        let prev_token = user.userdata.kv.get("cardtoken");
        let prev_cardk = user.userdata.kv.get("cardkey");

        match (prev_token, prev_cardk) {
            (Some(prev_token), Some(prev_cardk))
                if prev_token.as_str() == &token && prev_cardk.as_str() == card_key.as_str() =>
            {
                tracing::info!(
                    user.id, token = token.as_ref(),
                    "new token and card key are identical, skipping no-op"
                );
                return Promise::ok(());
            },
            (Some(prev_token), Some(_))
                if prev_token.as_str() == token /* above guard means prev_cardk != card_key */ =>
            {
                tracing::warn!(
                    token = token.as_ref(),
                    "trying to overwrite card key for existing token, ignoring!"
                );
                return Promise::ok(());
            },
            (Some(prev_token), None) => tracing::warn!(
                user.id, prev_token,
                "token already set for user but no card key, setting new pair unconditionally!"
            ),
            (None, Some(_)) => tracing::warn!(
                user.id,
                "card key already set for user but no token, setting new pair unconditionally!"
            ),
            (Some(_), Some(_)) | (None, None) => tracing::debug!(
                user.id, token = token.as_ref(),
                "Adding new card key/token pair"
            ),
        }

        user.userdata
            .kv
            .insert("cardtoken".to_string(), token.to_string());
        user.userdata.kv.insert("cardkey".to_string(), card_key);

        pry!(self.session.users.put_user(self.user.get_username(), &user));

        Promise::ok(())
    }

    fn unbind(&mut self, params: UnbindParams, _: UnbindResults) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "unbind").entered();

        let params = pry!(params.get());
        let token = pry!(params.get_token());

        let token: Cow<'_, str> = if let Ok(uuid) = Uuid::from_slice(token) {
            Cow::Owned(
                uuid.to_hyphenated()
                    .encode_lower(&mut Uuid::encode_buffer())
                    .to_string(),
            )
        } else {
            Cow::Owned(hex::encode(token))
        };

        tracing::trace!(params.token = token.as_ref(), "method call");

        let mut user = pry!(self
            .session
            .users
            .get_user(self.user.get_username())
            .ok_or_else(|| Error::failed(format!(
                "User API object with nonexisting user \"{}\"",
                self.user.get_username()
            ))));
        if let Some(prev_token) = user.userdata.kv.get("cardtoken") {
            if token.as_ref() == prev_token.as_str() {
                tracing::debug!(
                    user.id,
                    token = token.as_ref(),
                    "removing card key/token pair"
                );
                user.userdata.kv.remove("cardtoken");
                user.userdata.kv.remove("cardkey");
            }
        }

        pry!(self.session.users.put_user(self.user.get_username(), &user));

        Promise::ok(())
    }

    fn gen_card_token(
        &mut self,
        _: GenCardTokenParams,
        mut results: GenCardTokenResults,
    ) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "gen_card_token").entered();
        tracing::trace!("method call");

        results.get().set_token(Uuid::new_v4().as_bytes());

        Promise::ok(())
    }

    fn get_meta_info(
        &mut self,
        _: GetMetaInfoParams,
        mut results: GetMetaInfoResults,
    ) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "get_meta_info").entered();
        tracing::trace!("method call");

        results.get().set_bytes(b"FABACCESS\x00DESFIRE\x001.0\x00");

        Promise::ok(())
    }

    fn get_space_info(
        &mut self,
        _: GetSpaceInfoParams,
        mut results: GetSpaceInfoResults,
    ) -> Promise<(), Error> {
        let _guard = self.span.enter();
        let _span = tracing::trace_span!(target: TARGET, "get_space_info").entered();
        tracing::trace!("method call");

        let space = if let Some(space) = CONFIG.get().map(|c| c.spacename.as_str()) {
            space
        } else {
            return Promise::err(Error::failed("No space name configured".to_string()));
        };

        let url = if let Some(url) = CONFIG.get().map(|c| c.instanceurl.as_str()) {
            url
        } else {
            return Promise::err(Error::failed("No instance url configured".to_string()));
        };

        let mut data = Vec::new();
        write!(&mut data, "urn:fabaccess:lab:{space}\x00{url}").unwrap();
        results.get().set_bytes(&data);

        Promise::ok(())
    }
}