//! Support for password entries in the keychain.  Works on both iOS and macOS.
//!
//! If you want the extended keychain facilities only available on macOS, use the
//! version of these functions in the macOS extensions module.

#[doc(inline)]
pub use crate::passwords_options::{AccessControlOptions, PasswordOptions};

use crate::base::Result;
use crate::{cvt, Error};
use core_foundation::base::TCFType;
use core_foundation::boolean::CFBoolean;
use core_foundation::data::CFData;
use core_foundation::dictionary::CFDictionary;
use core_foundation_sys::base::{CFGetTypeID, CFRelease, CFTypeRef};
use core_foundation_sys::data::CFDataRef;
use security_framework_sys::base::{errSecDuplicateItem, errSecParam};
use security_framework_sys::item::{kSecReturnData, kSecValueData};
use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType};
use security_framework_sys::keychain_item::{
    SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate,
};

/// Set a generic password for the given service and account.
/// Creates or updates a keychain entry.
pub fn set_generic_password(service: &str, account: &str, password: &[u8]) -> Result<()> {
    let mut options = PasswordOptions::new_generic_password(service, account);
    set_password_internal(&mut options, password)
}

/// Set a generic password using the given password options.
/// Creates or updates a keychain entry.
pub fn set_generic_password_options(password: &[u8], mut options: PasswordOptions) -> Result<()> {
    set_password_internal(&mut options, password)
}

/// Get the generic password for the given service and account.  If no matching
/// keychain entry exists, fails with error code `errSecItemNotFound`.
#[doc(hidden)]
pub fn get_generic_password(service: &str, account: &str) -> Result<Vec<u8>> {
    generic_password(PasswordOptions::new_generic_password(service, account))
}

/// Get the generic password for the given service and account.  If no matching
/// keychain entry exists, fails with error code `errSecItemNotFound`.
///
/// See [`PasswordOptions`] and [`new_generic_password`](PasswordOptions::new_generic_password).
///
/// ```rust
/// use security_framework::passwords::{generic_password, PasswordOptions};
/// generic_password(PasswordOptions::new_generic_password("service", "account"));
/// ```
pub fn generic_password(mut options: PasswordOptions) -> Result<Vec<u8>> {
    unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
    let params = options.to_dictionary();
    let mut ret: CFTypeRef = std::ptr::null();
    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
    get_password_and_release(ret)
}

/// Delete the generic password keychain entry for the given service and account.
/// If none exists, fails with error code `errSecItemNotFound`.
pub fn delete_generic_password(service: &str, account: &str) -> Result<()> {
    let options = PasswordOptions::new_generic_password(service, account);
    delete_generic_password_options(options)
}

/// Delete the generic password keychain entry for the given service and account.
/// If none exists, fails with error code `errSecItemNotFound`.
///
/// See [`PasswordOptions`] and [`new_generic_password`](PasswordOptions::new_generic_password).
///
/// ```rust
/// use security_framework::passwords::{delete_generic_password_options, PasswordOptions};
/// delete_generic_password_options(PasswordOptions::new_generic_password("service", "account"));
/// ```
pub fn delete_generic_password_options(options: PasswordOptions) -> Result<()> {
    let params = options.to_dictionary();
    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
}

/// Set an internet password for the given endpoint parameters.
/// Creates or updates a keychain entry.
#[allow(clippy::too_many_arguments)]
pub fn set_internet_password(
    server: &str,
    security_domain: Option<&str>,
    account: &str,
    path: &str,
    port: Option<u16>,
    protocol: SecProtocolType,
    authentication_type: SecAuthenticationType,
    password: &[u8],
) -> Result<()> {
    let mut options = PasswordOptions::new_internet_password(
        server,
        security_domain,
        account,
        path,
        port,
        protocol,
        authentication_type,
    );
    set_password_internal(&mut options, password)
}

/// Get the internet password for the given endpoint parameters.  If no matching
/// keychain entry exists, fails with error code `errSecItemNotFound`.
pub fn get_internet_password(
    server: &str,
    security_domain: Option<&str>,
    account: &str,
    path: &str,
    port: Option<u16>,
    protocol: SecProtocolType,
    authentication_type: SecAuthenticationType,
) -> Result<Vec<u8>> {
    let mut options = PasswordOptions::new_internet_password(
        server,
        security_domain,
        account,
        path,
        port,
        protocol,
        authentication_type,
    );
    unsafe { options.push_query(kSecReturnData, CFBoolean::from(true)); }
    let params = options.to_dictionary();
    let mut ret: CFTypeRef = std::ptr::null();
    cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?;
    get_password_and_release(ret)
}

/// Delete the internet password for the given endpoint parameters.
/// If none exists, fails with error code `errSecItemNotFound`.
pub fn delete_internet_password(
    server: &str,
    security_domain: Option<&str>,
    account: &str,
    path: &str,
    port: Option<u16>,
    protocol: SecProtocolType,
    authentication_type: SecAuthenticationType,
) -> Result<()> {
    let options = PasswordOptions::new_internet_password(
        server,
        security_domain,
        account,
        path,
        port,
        protocol,
        authentication_type,
    );
    let params = options.to_dictionary();
    cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) })
}

// This starts by trying to create the password with the given query params.
// If the creation attempt reveals that one exists, its password is updated.
fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> {
    #[allow(deprecated)]
    let query_without_password = options.query.len();
    unsafe { options.push_query(kSecValueData, CFData::from_buffer(password)); }

    let params = options.to_dictionary();
    let mut ret = std::ptr::null();
    let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) };
    if status == errSecDuplicateItem {
        #[allow(deprecated)]
        let (query, pass) = options.query.split_at(query_without_password);
        let params = CFDictionary::from_CFType_pairs(query);
        let update = CFDictionary::from_CFType_pairs(pass);
        cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) })
    } else {
        cvt(status)
    }
}

// Having retrieved a password entry, this copies and returns the password.
//
// # Safety
// The data element passed in is assumed to have been returned from a Copy
// call, so it's released after we are done with it.
fn get_password_and_release(data: CFTypeRef) -> Result<Vec<u8>> {
    if !data.is_null() {
        let type_id = unsafe { CFGetTypeID(data) };
        if type_id == CFData::type_id() {
            let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) };
            let mut vec = Vec::new();
            if !val.is_empty() {
                vec.extend_from_slice(val.bytes());
            }
            return Ok(vec);
        }
        // unexpected: we got a reference to some other type.
        // Release it to make sure there's no leak, but
        // we can't return the password in this case.
        unsafe { CFRelease(data) };
    }
    Err(Error::from_code(errSecParam))
}

#[cfg(test)]
mod test {
    use super::*;
    use security_framework_sys::base::errSecItemNotFound;

    #[test]
    fn missing_generic() {
        let name = "a string not likely to already be in the keychain as service or account";
        let result = delete_generic_password(name, name);
        match result {
            Ok(()) => (), // this is ok because the name _might_ be in the keychain
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
        }
        let result = get_generic_password(name, name);
        match result {
            Ok(bytes) => panic!("missing_generic: get returned {bytes:?}"),
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_generic: get failed with status: {}", err.code()),
        }
        let result = delete_generic_password(name, name);
        match result {
            Ok(()) => panic!("missing_generic: second delete found a password"),
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()),
        }
    }

    #[test]
    fn roundtrip_generic() {
        let name = "roundtrip_generic";
        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
        let pass = get_generic_password(name, name).expect("get_generic_password");
        assert_eq!(name.as_bytes(), pass);
        delete_generic_password(name, name).expect("delete_generic_password");
    }

    #[test]
    fn update_generic() {
        let name = "update_generic";
        set_generic_password(name, name, name.as_bytes()).expect("set_generic_password");
        let alternate = "update_generic_alternate";
        set_generic_password(name, name, alternate.as_bytes()).expect("set_generic_password");
        let pass = get_generic_password(name, name).expect("get_generic_password");
        assert_eq!(pass, alternate.as_bytes());
        delete_generic_password(name, name).expect("delete_generic_password");
    }

    #[test]
    fn missing_internet() {
        let name = "a string not likely to already be in the keychain as service or account";
        let (server, domain, account, path, port, protocol, auth) =
            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);
        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
        match result {
            Ok(()) => (), // this is ok because the name _might_ be in the keychain
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_internet: delete failed with status: {}", err.code()),
        }
        let result = get_internet_password(server, domain, account, path, port, protocol, auth);
        match result {
            Ok(bytes) => panic!("missing_internet: get returned {bytes:?}"),
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_internet: get failed with status: {}", err.code()),
        }
        let result = delete_internet_password(server, domain, account, path, port, protocol, auth);
        match result {
            Ok(()) => panic!("missing_internet: second delete found a password"),
            Err(err) if err.code() == errSecItemNotFound => (),
            Err(err) => panic!("missing_internet: delete failed with status: {}", err.code()),
        }
    }

    #[test]
    fn roundtrip_internet() {
        let name = "roundtrip_internet";
        let (server, domain, account, path, port, protocol, auth) =
            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);
        set_internet_password(server, domain, account, path, port, protocol, auth, name.as_bytes())
            .expect("set_internet_password");
        let pass =
            get_internet_password(server, domain, account, path, port, protocol, auth).expect("get_internet_password");
        assert_eq!(name.as_bytes(), pass);
        delete_internet_password(server, domain, account, path, port, protocol, auth)
            .expect("delete_internet_password");
    }

    #[test]
    fn update_internet() {
        let name = "update_internet";
        let (server, domain, account, path, port, protocol, auth) =
            (name, None, name, "/", Some(8080u16), SecProtocolType::HTTP, SecAuthenticationType::Any);

        // cleanup after failed test
        let _ = delete_internet_password(server, domain, account, path, port, protocol, auth);

        set_internet_password(server, domain, account, path, port, protocol, auth, name.as_bytes())
            .expect("set_internet_password");
        let alternate = "alternate_internet_password";
        set_internet_password(server, domain, account, path, port, protocol, auth, alternate.as_bytes())
            .expect("set_internet_password");
        let pass =
            get_internet_password(server, domain, account, path, port, protocol, auth).expect("get_internet_password");
        assert_eq!(pass, alternate.as_bytes());
        delete_internet_password(server, domain, account, path, port, protocol, auth)
            .expect("delete_internet_password");
    }
}
