//! Read-only C Tor client key store implementation
//!
//! See [`CTorClientKeystore`] for more details.

use std::fs;
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
use std::str::FromStr as _;
use std::sync::Arc;

use crate::keystore::ctor::CTorKeystore;
use crate::keystore::ctor::err::{CTorKeystoreError, MalformedClientKeyError};
use crate::keystore::fs_utils::{FilesystemAction, FilesystemError, RelKeyPath, checked_op};
use crate::keystore::{EncodableItem, ErasedKey, KeySpecifier, Keystore};
use crate::raw::{RawEntryId, RawKeystoreEntry};
use crate::{
    CTorPath, KeyPath, KeystoreEntry, KeystoreEntryResult, KeystoreId, Result,
    UnrecognizedEntryError,
};

use fs_mistrust::Mistrust;
use itertools::Itertools as _;
use tor_basic_utils::PathExt;
use tor_error::debug_report;
use tor_hscrypto::pk::{HsClientDescEncKeypair, HsId};
use tor_key_forge::{KeyType, KeystoreItemType};
use tor_llcrypto::pk::curve25519;
use tracing::debug;

/// A read-only C Tor client keystore.
///
/// This keystore provides read-only access to the client restricted discovery keys
/// rooted at a given `ClientOnionAuthDir` directory (see `ClientOnionAuthDir` in `tor(1)`).
///
/// The key files must be in the
/// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>` format
/// and have the `.auth_private` extension.
/// Invalid keys, and keys that don't have the expected extension, will be ignored.
///
/// The only supported [`Keystore`] operations are [`contains`](Keystore::contains),
/// [`get`](Keystore::get), and [`list`](Keystore::list). All other keystore operations
/// will return an error.
///
/// This keystore implementation uses the [`CTorPath`] of the requested [`KeySpecifier`]
/// and the [`KeystoreItemType`] to identify the appropriate restricted discovery keypair.
/// If the requested `CTorPath` is not [`HsClientDescEncKeypair`](CTorPath::HsClientDescEncKeypair),
/// the keystore will declare the key not found.
/// If the requested `CTorPath` is [`HsClientDescEncKeypair`](CTorPath::HsClientDescEncKeypair),
/// but the `KeystoreItemType` is not [`X25519StaticKeypair`](KeyType::X25519StaticKeypair),
/// an error is returned.
pub struct CTorClientKeystore(CTorKeystore);

impl CTorClientKeystore {
    /// Create a new `CTorKeystore` rooted at the specified `keystore_dir` directory.
    ///
    /// This function returns an error if `keystore_dir` is not a directory,
    /// or if it does not conform to the requirements of the specified `Mistrust`.
    pub fn from_path_and_mistrust(
        keystore_dir: impl AsRef<Path>,
        mistrust: &Mistrust,
        id: KeystoreId,
    ) -> Result<Self> {
        CTorKeystore::from_path_and_mistrust(keystore_dir, mistrust, id).map(Self)
    }
}

/// Extract the HsId from `spec, or return `res`.
macro_rules! hsid_if_supported {
    ($spec:expr, $ret:expr, $key_type:expr) => {{
        // If the key specifier doesn't have a CTorPath,
        // we can't possibly handle this key.
        let Some(ctor_path) = $spec.ctor_path() else {
            return $ret;
        };

        // This keystore only deals with service keys...
        let CTorPath::HsClientDescEncKeypair { hs_id } = ctor_path else {
            return $ret;
        };

        if *$key_type != KeyType::X25519StaticKeypair.into() {
            return Err(CTorKeystoreError::InvalidKeystoreItemType {
                item_type: $key_type.clone(),
                item: "client restricted discovery key".into(),
            }
            .into());
        }

        hs_id
    }};
}

impl CTorClientKeystore {
    /// List all the key entries in the keystore_dir.
    fn list_entries(&self, dir: &RelKeyPath) -> Result<fs::ReadDir> {
        let entries = checked_op!(read_directory, dir)
            .map_err(|e| FilesystemError::FsMistrust {
                action: FilesystemAction::Read,
                path: dir.rel_path_unchecked().into(),
                err: e.into(),
            })
            .map_err(CTorKeystoreError::Filesystem)?;

        Ok(entries)
    }
}

/// The extension of the client keys stored in this store.
const KEY_EXTENSION: &str = "auth_private";

impl CTorClientKeystore {
    /// Read the contents of the specified key.
    ///
    /// Returns `Ok(None)` if the file doesn't exist.
    fn read_key(&self, key_path: &Path) -> StdResult<Option<String>, CTorKeystoreError> {
        let key_path = self.0.rel_path(key_path.into());

        // TODO: read and parse the key, see if it matches the specified hsid
        let content = match checked_op!(read_to_string, key_path) {
            Err(fs_mistrust::Error::NotFound(_)) => {
                // Someone removed the file between the time we read the directory and now.
                return Ok(None);
            }
            res => res
                .map_err(|err| FilesystemError::FsMistrust {
                    action: FilesystemAction::Read,
                    path: key_path.rel_path_unchecked().into(),
                    err: err.into(),
                })
                .map_err(CTorKeystoreError::Filesystem)?,
        };

        Ok(Some(content))
    }

    /// List all entries in this store
    ///
    /// Returns a list of results, where `Ok` signifies a recognized entry,
    /// and [`Err(CTorKeystoreError)`](crate::keystore::ctor::CTorKeystoreError)
    /// an unrecognized one.
    /// A key is said to be recognized if its file name ends with `.auth_private`,
    /// and it presents this format:
    /// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>`
    fn list_keys(
        &self,
    ) -> Result<
        impl Iterator<Item = StdResult<(HsId, HsClientDescEncKeypair), CTorKeystoreError>> + '_,
    > {
        use CTorKeystoreError::*;

        let dir = self.0.rel_path(PathBuf::from("."));
        Ok(self.list_entries(&dir)?.filter_map(|entry| {
            let entry = entry
                .map_err(|e| {
                    // NOTE: can't use debug_report here, because debug_report
                    // expects the ErrorKind (returned by e.kind()) to be
                    // tor_error::ErrorKind (which has a is_always_a_warning() function
                    // used by the macro).
                    //
                    // We have an io::Error here, which has an io::ErrorKind,
                    // and thus can't be used with debug_report.
                    debug!("cannot access key entry: {e}");
                })
                .ok()?;

            let file_name = entry.file_name();
            let path: &Path = file_name.as_ref();
            let Some(KEY_EXTENSION) = path.extension().and_then(|e| e.to_str()) else {
                return Some(Err(MalformedKey {
                    path: entry.path(),
                    err: MalformedClientKeyError::InvalidFormat.into(),
                }));
            };

            let content = match self.read_key(path) {
                Ok(c) => c,
                Err(e) => {
                    debug_report!(&e, "failed to read {}", path.display_lossy());
                    return Some(Err(e));
                }
            }?;
            Some(
                parse_client_keypair(content.trim()).map_err(|e| MalformedKey {
                    path: path.into(),
                    err: e.into(),
                }),
            )
        }))
    }
}

/// Parse a client restricted discovery keypair,
/// returning the [`HsId`] of the service the key is meant for,
/// and the corresponding [`HsClientDescEncKeypair`].
///
/// `key` is expected to be in the
/// `<hsid>:descriptor:x25519:<base32-encoded-x25519-public-key>`
/// format.
///
/// TODO: we might want to move this to tor-hscrypto at some point,
/// but for now, we don't actually *need* to expose this publicly.
fn parse_client_keypair(
    key: impl AsRef<str>,
) -> StdResult<(HsId, HsClientDescEncKeypair), MalformedClientKeyError> {
    let key = key.as_ref();
    let (hsid, auth_type, key_type, encoded_key) = key
        .split(':')
        .collect_tuple()
        .ok_or(MalformedClientKeyError::InvalidFormat)?;

    if auth_type != "descriptor" {
        return Err(MalformedClientKeyError::InvalidAuthType(auth_type.into()));
    }

    if key_type != "x25519" {
        return Err(MalformedClientKeyError::InvalidKeyType(key_type.into()));
    }

    // Note: Tor's base32 decoder is case-insensitive, so we can't assume the input
    // is all uppercase.
    //
    // TODO: consider using `data_encoding_macro::new_encoding` to create a new Encoding
    // with an alphabet that includes lowercase letters instead of to_uppercase()ing the string.
    let encoded_key = encoded_key.to_uppercase();
    let x25519_sk = data_encoding::BASE32_NOPAD.decode(encoded_key.as_bytes())?;
    let x25519_sk: [u8; 32] = x25519_sk
        .try_into()
        .map_err(|_| MalformedClientKeyError::InvalidKeyMaterial)?;

    let secret = curve25519::StaticSecret::from(x25519_sk);
    let public = (&secret).into();
    let x25519_keypair = curve25519::StaticKeypair { secret, public };
    let hsid = HsId::from_str(&format!("{hsid}.onion"))?;

    Ok((hsid, x25519_keypair.into()))
}

impl Keystore for CTorClientKeystore {
    fn id(&self) -> &KeystoreId {
        &self.0.id
    }

    fn contains(&self, key_spec: &dyn KeySpecifier, item_type: &KeystoreItemType) -> Result<bool> {
        self.get(key_spec, item_type).map(|k| k.is_some())
    }

    fn get(
        &self,
        key_spec: &dyn KeySpecifier,
        item_type: &KeystoreItemType,
    ) -> Result<Option<ErasedKey>> {
        let want_hsid = hsid_if_supported!(key_spec, Ok(None), item_type);
        Ok(self
            .list_keys()?
            .find_map(|entry| {
                if let Ok((hsid, key)) = entry {
                    (hsid == want_hsid).then(|| key.into())
                } else {
                    None
                }
            })
            .map(|k: curve25519::StaticKeypair| Box::new(k) as ErasedKey))
    }

    #[cfg(feature = "onion-service-cli-extra")]
    fn raw_entry_id(&self, raw_id: &str) -> Result<RawEntryId> {
        Ok(RawEntryId::Path(PathBuf::from(raw_id.to_string())))
    }

    fn insert(&self, _key: &dyn EncodableItem, _key_spec: &dyn KeySpecifier) -> Result<()> {
        Err(CTorKeystoreError::NotSupported { action: "insert" }.into())
    }

    fn remove(
        &self,
        _key_spec: &dyn KeySpecifier,
        _item_type: &KeystoreItemType,
    ) -> Result<Option<()>> {
        Err(CTorKeystoreError::NotSupported { action: "remove" }.into())
    }

    #[cfg(feature = "onion-service-cli-extra")]
    fn remove_unchecked(&self, _entry_id: &RawEntryId) -> Result<()> {
        Err(CTorKeystoreError::NotSupported {
            action: "remove_unchecked",
        }
        .into())
    }

    fn list(&self) -> Result<Vec<KeystoreEntryResult<KeystoreEntry>>> {
        use CTorKeystoreError::*;

        let keys = self
            .list_keys()?
            .filter_map(|entry| match entry {
                Ok((hs_id, _)) => {
                    let key_path: KeyPath = CTorPath::HsClientDescEncKeypair { hs_id }.into();
                    let key_type: KeystoreItemType = KeyType::X25519StaticKeypair.into();
                    let raw_id = RawEntryId::Path(key_path.ctor()?.to_string().into());
                    Some(Ok(KeystoreEntry::new(
                        key_path,
                        key_type,
                        self.id(),
                        raw_id,
                    )))
                }
                Err(e) => match e {
                    MalformedKey { ref path, err: _ } => {
                        let raw_id = RawEntryId::Path(path.clone());
                        let entry = RawKeystoreEntry::new(raw_id, self.id().clone()).into();
                        Some(Err(UnrecognizedEntryError::new(entry, Arc::new(e))))
                    }
                    // `InvalidKeystoreItemType` variant is filtered out because it can't
                    // be returned by [`CTorClientKeystore::list_keys`].
                    InvalidKeystoreItemType { .. } => None,
                    // The following variants are irrelevant at this level because they
                    // cannot represent an unrecognized key.
                    Filesystem(_) => None,
                    NotSupported { .. } => None,
                    Bug(_) => None,
                },
            })
            .collect();

        Ok(keys)
    }
}

#[cfg(test)]
mod tests {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_time_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use std::fs;
    use tempfile::{TempDir, tempdir};

    use crate::test_utils::{DummyKey, TestCTorSpecifier, assert_found};

    #[cfg(unix)]
    use std::os::unix::fs::PermissionsExt;

    /// A valid client restricted discovery key.
    const ALICE_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/alice.auth_private");

    /// An invalid client restricted discovery key.
    const BOB_AUTH_PRIVATE_INVALID: &str = include_str!("../../../testdata/bob.auth_private");

    /// A valid client restricted discovery key.
    const CAROL_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/carol.auth_private");

    /// A valid client restricted discovery key.
    const DAN_AUTH_PRIVATE_VALID: &str = include_str!("../../../testdata/dan.auth_private");

    // An .onion addr we don't have a client key for.
    const HSID: &str = "mnyizjj7m3hpcr7i5afph3zt7maa65johyu2ruis6z7cmnjmaj3h6tad.onion";

    fn init_keystore(id: &str) -> (CTorClientKeystore, TempDir) {
        let keystore_dir = tempdir().unwrap();

        #[cfg(unix)]
        fs::set_permissions(&keystore_dir, fs::Permissions::from_mode(0o700)).unwrap();

        let id = KeystoreId::from_str(id).unwrap();
        let keystore =
            CTorClientKeystore::from_path_and_mistrust(&keystore_dir, &Mistrust::default(), id)
                .unwrap();

        let keys: &[(&str, &str)] = &[
            ("alice.auth_private", ALICE_AUTH_PRIVATE_VALID),
            // A couple of malformed key, added to check that our impl doesn't trip over them
            ("bob.auth_private", BOB_AUTH_PRIVATE_INVALID),
            (
                "alice-truncated.auth_private",
                &ALICE_AUTH_PRIVATE_VALID[..100],
            ),
            // A valid key, but with the wrong extension (so it should be ignored)
            ("carol.auth", CAROL_AUTH_PRIVATE_VALID),
            ("dan.auth_private", DAN_AUTH_PRIVATE_VALID),
        ];

        for (name, key) in keys {
            fs::write(keystore_dir.path().join(name), key).unwrap();
        }

        (keystore, keystore_dir)
    }

    #[test]
    fn get() {
        let (keystore, _keystore_dir) = init_keystore("foo");
        let path = CTorPath::HsClientDescEncKeypair {
            hs_id: HsId::from_str(HSID).unwrap(),
        };

        // Not found!
        assert_found!(
            keystore,
            &TestCTorSpecifier(path.clone()),
            &KeyType::X25519StaticKeypair,
            false
        );

        for hsid in &[ALICE_AUTH_PRIVATE_VALID, DAN_AUTH_PRIVATE_VALID] {
            // Extract the HsId associated with this key.
            let onion = hsid.split(":").next().unwrap();
            let hsid = HsId::from_str(&format!("{onion}.onion")).unwrap();
            let path = CTorPath::HsClientDescEncKeypair {
                hs_id: hsid.clone(),
            };

            // Found!
            assert_found!(
                keystore,
                &TestCTorSpecifier(path.clone()),
                &KeyType::X25519StaticKeypair,
                true
            );
        }

        let keys: Vec<_> = keystore
            .list()
            .unwrap()
            .into_iter()
            .filter(|e| e.is_ok())
            .collect();

        assert_eq!(keys.len(), 2);
        assert!(keys.iter().all(|entry| {
            entry.as_ref().unwrap().key_type() == &KeyType::X25519StaticKeypair.into()
        }));
    }

    #[test]
    fn unsupported_operation() {
        let (keystore, _keystore_dir) = init_keystore("foo");
        let path = CTorPath::HsClientDescEncKeypair {
            hs_id: HsId::from_str(HSID).unwrap(),
        };

        let err = keystore
            .remove(
                &TestCTorSpecifier(path.clone()),
                &KeyType::X25519StaticKeypair.into(),
            )
            .unwrap_err();

        assert_eq!(err.to_string(), "Operation not supported: remove");

        let err = keystore
            .insert(&DummyKey, &TestCTorSpecifier(path))
            .unwrap_err();

        assert_eq!(err.to_string(), "Operation not supported: insert");
    }

    #[test]
    fn wrong_keytype() {
        let (keystore, _keystore_dir) = init_keystore("foo");
        let path = CTorPath::HsClientDescEncKeypair {
            hs_id: HsId::from_str(HSID).unwrap(),
        };

        let err = keystore
            .get(
                &TestCTorSpecifier(path.clone()),
                &KeyType::Ed25519PublicKey.into(),
            )
            .map(|_| ())
            .unwrap_err();

        assert_eq!(
            err.to_string(),
            "Invalid item type Ed25519PublicKey for client restricted discovery key"
        );
    }

    #[test]
    fn list() {
        let (keystore, _keystore_dir) = init_keystore("foo");
        // The keystore contains two recognized entries and three
        // unrecognized entries.
        let mut recognized = 0;
        let mut unrecognized = 0;
        for e in keystore.list().unwrap() {
            if e.is_ok() {
                recognized += 1;
            } else {
                unrecognized += 1;
            }
        }
        assert_eq!(recognized, 2);
        assert_eq!(unrecognized, 3);
    }
}
