use crate::error::{KeyringError, Result};
use crate::Keyring;
use security_framework::base::Error as SfError;
use security_framework::os::macos::keychain::SecKeychain;
use security_framework::os::macos::passwords::find_generic_password;
#[cfg(feature = "macos-specify-keychain")]
use std::path::Path;

#[allow(non_upper_case_globals)]
const errSecItemNotFound: i32 = -25300;

pub struct MacosKeyringManager {
    application: String,
    keychain: SecKeychain,
}

impl MacosKeyringManager {
    pub fn new(application: &str) -> Result<Self> {
        Ok(MacosKeyringManager {
            application: application.to_owned(),
            keychain: SecKeychain::default()?,
        })
    }

    #[cfg(feature = "macos-specify-keychain")]
    pub fn with_path(application: &str, path: &Path) -> Result<Self> {
        Ok(MacosKeyringManager {
            application: application.to_owned(),
            keychain: SecKeychain::open(path)?,
        })
    }

    pub fn with_keyring<F, T>(&self, service: &str, key: &str, func: F) -> Result<T>
    where
        F: FnOnce(&mut dyn Keyring) -> Result<T>,
    {
        let mut kr = MacosKeyring::new(self, service, key)?;
        func(&mut kr)
    }
}

pub struct MacosKeyring<'a> {
    manager: &'a MacosKeyringManager,
    service: &'a str,
    key: &'a str,
}

impl<'a> MacosKeyring<'a> {
    fn new(
        manager: &'a MacosKeyringManager,
        service: &'a str,
        key: &'a str,
    ) -> Result<MacosKeyring<'a>> {
        Ok(MacosKeyring {
            manager,
            service,
            key,
        })
    }

    fn make_service_name(&self) -> String {
        [
            crate::escape(&self.manager.application),
            crate::escape(self.service),
        ]
        .join("\t")
    }
}

impl Keyring for MacosKeyring<'_> {
    fn set_value(&mut self, value: &str) -> Result<()> {
        let service_name = self.make_service_name();

        self.manager
            .keychain
            .set_generic_password(&service_name, self.key, value.as_bytes())?;

        Ok(())
    }

    fn get_value(&self) -> Result<String> {
        let service_name = self.make_service_name();

        let (password_bytes, _) = match find_generic_password(
            Some(std::slice::from_ref(&self.manager.keychain)),
            &service_name,
            self.key,
        ) {
            Ok(v) => v,
            Err(e) => {
                if e.code() == errSecItemNotFound {
                    return Err(KeyringError::NoPasswordFound);
                } else {
                    return Err(KeyringError::MacOsKeychainError(SfError::from_code(
                        e.code(),
                    )));
                }
            }
        };

        // Mac keychain allows non-UTF8 values, but this library only supports adding UTF8 items
        // to the keychain, so this should only fail if we are trying to retrieve a non-UTF8
        // password that was added to the keychain by another library

        let password = String::from_utf8(password_bytes.to_vec())?;

        Ok(password)
    }

    fn delete_value(&mut self) -> Result<()> {
        let service_name = self.make_service_name();

        let (_, item) = match find_generic_password(
            Some(std::slice::from_ref(&self.manager.keychain)),
            &service_name,
            self.key,
        ) {
            Ok(v) => v,
            Err(e) => {
                if e.code() == errSecItemNotFound {
                    return Err(KeyringError::NoPasswordFound);
                } else {
                    return Err(KeyringError::MacOsKeychainError(SfError::from_code(
                        e.code(),
                    )));
                }
            }
        };
        item.delete();

        Ok(())
    }
}
