use std::ffi::CString;

use crate::correlation::CorrelationId;
use crate::errors::{BlpError, Result};
use crate::ffi;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthConfig {
    User,
    App {
        app_name: String,
    },
    UserApp {
        app_name: String,
    },
    Directory {
        property_name: String,
    },
    Manual {
        app_name: String,
        user_id: String,
        ip_address: String,
    },
    Token {
        token: String,
    },
}

impl AuthConfig {
    pub fn method_name(&self) -> &'static str {
        match self {
            Self::User => "user",
            Self::App { .. } => "app",
            Self::UserApp { .. } => "userapp",
            Self::Directory { .. } => "dir",
            Self::Manual { .. } => "manual",
            Self::Token { .. } => "token",
        }
    }

    pub fn build_auth_options(&self) -> Result<AuthOptions> {
        match self {
            Self::User => {
                let user = AuthUser::with_logon_name()?;
                AuthOptions::for_user(&user)
            }
            Self::App { app_name } => {
                let app = AuthApplication::new(app_name)?;
                AuthOptions::for_app(&app)
            }
            Self::UserApp { app_name } => {
                let user = AuthUser::with_logon_name()?;
                let app = AuthApplication::new(app_name)?;
                AuthOptions::for_user_and_app(&user, &app)
            }
            Self::Directory { property_name } => {
                let user = AuthUser::with_active_directory_property(property_name)?;
                AuthOptions::for_user(&user)
            }
            Self::Manual {
                app_name,
                user_id,
                ip_address,
            } => {
                let user = AuthUser::with_manual_options(user_id, ip_address)?;
                let app = AuthApplication::new(app_name)?;
                AuthOptions::for_user_and_app(&user, &app)
            }
            Self::Token { token } => {
                let token = AuthToken::new(token)?;
                AuthOptions::for_token(&token)
            }
        }
    }
}

pub struct AuthUser {
    ptr: *mut ffi::blpapi_AuthUser_t,
}

unsafe impl Send for AuthUser {}
unsafe impl Sync for AuthUser {}

impl AuthUser {
    pub fn with_logon_name() -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthUser_createWithLogonName(&mut ptr) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::InvalidArgument {
                detail: format!("failed to create AuthUser(OS_LOGON): rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn with_active_directory_property(property_name: &str) -> Result<Self> {
        let property_name = cstring(property_name, "Active Directory property")?;
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe {
            ffi::blpapi_AuthUser_createWithActiveDirectoryProperty(&mut ptr, property_name.as_ptr())
        };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::InvalidArgument {
                detail: format!("failed to create AuthUser(DIRECTORY_SERVICE): rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn with_manual_options(user_id: &str, ip_address: &str) -> Result<Self> {
        let user_id = cstring(user_id, "manual user_id")?;
        let ip_address = cstring(ip_address, "manual ip_address")?;
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe {
            ffi::blpapi_AuthUser_createWithManualOptions(
                &mut ptr,
                user_id.as_ptr(),
                ip_address.as_ptr(),
            )
        };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::InvalidArgument {
                detail: format!("failed to create AuthUser(manual): rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub(crate) fn as_ptr(&self) -> *const ffi::blpapi_AuthUser_t {
        self.ptr
    }
}

impl Drop for AuthUser {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::blpapi_AuthUser_destroy(self.ptr) };
            self.ptr = std::ptr::null_mut();
        }
    }
}

pub struct AuthApplication {
    ptr: *mut ffi::blpapi_AuthApplication_t,
}

unsafe impl Send for AuthApplication {}
unsafe impl Sync for AuthApplication {}

impl AuthApplication {
    pub fn new(app_name: &str) -> Result<Self> {
        let app_name = cstring(app_name, "application name")?;
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthApplication_create(&mut ptr, app_name.as_ptr()) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::InvalidArgument {
                detail: format!("failed to create AuthApplication: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub(crate) fn as_ptr(&self) -> *const ffi::blpapi_AuthApplication_t {
        self.ptr
    }
}

impl Drop for AuthApplication {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::blpapi_AuthApplication_destroy(self.ptr) };
            self.ptr = std::ptr::null_mut();
        }
    }
}

pub struct AuthToken {
    ptr: *mut ffi::blpapi_AuthToken_t,
}

unsafe impl Send for AuthToken {}
unsafe impl Sync for AuthToken {}

impl AuthToken {
    pub fn new(token: &str) -> Result<Self> {
        let token = cstring(token, "token")?;
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthToken_create(&mut ptr, token.as_ptr()) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::InvalidArgument {
                detail: format!("failed to create AuthToken: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub(crate) fn as_ptr(&self) -> *const ffi::blpapi_AuthToken_t {
        self.ptr
    }
}

impl Drop for AuthToken {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::blpapi_AuthToken_destroy(self.ptr) };
            self.ptr = std::ptr::null_mut();
        }
    }
}

pub struct AuthOptions {
    ptr: *mut ffi::blpapi_AuthOptions_t,
}

unsafe impl Send for AuthOptions {}
unsafe impl Sync for AuthOptions {}

impl AuthOptions {
    pub fn none() -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthOptions_create_default(&mut ptr) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::Internal {
                detail: format!("failed to create default AuthOptions: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn for_user(user: &AuthUser) -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthOptions_create_forUserMode(&mut ptr, user.as_ptr()) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::Internal {
                detail: format!("failed to create user AuthOptions: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn for_app(app: &AuthApplication) -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthOptions_create_forAppMode(&mut ptr, app.as_ptr()) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::Internal {
                detail: format!("failed to create app AuthOptions: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn for_user_and_app(user: &AuthUser, app: &AuthApplication) -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe {
            ffi::blpapi_AuthOptions_create_forUserAndAppMode(&mut ptr, user.as_ptr(), app.as_ptr())
        };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::Internal {
                detail: format!("failed to create user+app AuthOptions: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub fn for_token(token: &AuthToken) -> Result<Self> {
        let mut ptr = std::ptr::null_mut();
        let rc = unsafe { ffi::blpapi_AuthOptions_create_forToken(&mut ptr, token.as_ptr()) };
        if rc != 0 || ptr.is_null() {
            return Err(BlpError::Internal {
                detail: format!("failed to create token AuthOptions: rc={rc}"),
            });
        }
        Ok(Self { ptr })
    }

    pub(crate) fn as_ptr(&self) -> *const ffi::blpapi_AuthOptions_t {
        self.ptr
    }
}

impl Drop for AuthOptions {
    fn drop(&mut self) {
        if !self.ptr.is_null() {
            unsafe { ffi::blpapi_AuthOptions_destroy(self.ptr) };
            self.ptr = std::ptr::null_mut();
        }
    }
}

fn cstring(value: &str, field: &str) -> Result<CString> {
    CString::new(value).map_err(|e| BlpError::InvalidArgument {
        detail: format!("invalid {field}: {e}"),
    })
}

pub fn apply_session_identity_options(
    options: &mut crate::options::SessionOptions,
    auth_config: &AuthConfig,
) -> Result<CorrelationId> {
    let auth_options = auth_config.build_auth_options()?;
    options.set_session_identity_options(&auth_options)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn auth_config_method_names_match_public_api() {
        assert_eq!(AuthConfig::User.method_name(), "user");
        assert_eq!(
            AuthConfig::App {
                app_name: "app".to_string(),
            }
            .method_name(),
            "app"
        );
        assert_eq!(
            AuthConfig::UserApp {
                app_name: "app".to_string(),
            }
            .method_name(),
            "userapp"
        );
        assert_eq!(
            AuthConfig::Directory {
                property_name: "mail=user@example.com".to_string(),
            }
            .method_name(),
            "dir"
        );
        assert_eq!(
            AuthConfig::Manual {
                app_name: "app".to_string(),
                user_id: "1234".to_string(),
                ip_address: "10.0.0.1".to_string(),
            }
            .method_name(),
            "manual"
        );
        assert_eq!(
            AuthConfig::Token {
                token: "token".to_string(),
            }
            .method_name(),
            "token"
        );
    }
}
