//! [`KeySpecifier`] implementations for hidden service keys.
//!
//! Some of these `KeySpecifier`s represent time-bound keys (that are only valid
//! as long as their time period is relevant). Time-bound keys are expired (removed)
//! by [`expire_publisher_keys`].
//!
//! If you add a new key that is not a per-service singleton, you also need to
//! make arrangements to delete old ones.
//! For TP-based keys, that involves deriving [`HsTimePeriodKeySpecifier`]
//! and adding a call to `remove_if_expired!` in [`expire_publisher_keys`].

use crate::{internal_prelude::*, list_expired_keys_for_service};

/// Keys that are used by publisher, which relate to our HS and a TP
///
/// Derived using
/// the derive-deftly macro of the same name.
// We'd like to link to crate::derive_deftly_template_HsTimePeriodKeySpecifier
// but linking to a module-local macro doesn't work with rustdoc.
pub(crate) trait HsTimePeriodKeySpecifier: Debug {
    /// Inspect the nickname
    fn nickname(&self) -> &HsNickname;
    /// Inspect the period
    fn period(&self) -> &TimePeriod;
}

define_derive_deftly! {
    /// Implement `HsTimePeriodKeySpecifier` for a struct with `nickname` and `period`
    HsTimePeriodKeySpecifier:

    impl HsTimePeriodKeySpecifier for $ttype {
      $(
        ${when any(approx_equal($fname, nickname), approx_equal($fname, period))}
        fn $fname(&self) -> &$ftype {
            &self.$fname
        }
      )
    }
}

#[derive(Deftly, PartialEq, Debug, Constructor)]
#[derive_deftly(KeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(role = "KP_hs_id")]
#[deftly(summary = "Public part of the identity key")]
#[deftly(keypair_specifier = HsIdKeypairSpecifier)]
#[deftly(ctor_path = HsIdPublicKey)]
/// The public part of the identity key of the service.
pub struct HsIdPublicKeySpecifier {
    /// The nickname of the  hidden service.
    nickname: HsNickname,
}

#[derive(Deftly, PartialEq, Debug, Constructor)]
#[derive_deftly(KeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(role = "KS_hs_id")]
#[deftly(summary = "Long-term identity keypair")]
#[deftly(ctor_path = HsIdKeypair)]
/// The long-term identity keypair of the service.
pub struct HsIdKeypairSpecifier {
    /// The nickname of the  hidden service.
    pub(crate) nickname: HsNickname,
}

impl From<&HsIdPublicKeySpecifier> for HsIdKeypairSpecifier {
    fn from(hs_id_public_key_specifier: &HsIdPublicKeySpecifier) -> HsIdKeypairSpecifier {
        HsIdKeypairSpecifier::new(hs_id_public_key_specifier.nickname.clone())
    }
}

#[derive(Deftly, PartialEq, Debug, Constructor)]
#[derive_deftly(KeySpecifier, HsTimePeriodKeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(role = "KS_hs_blind_id")]
#[deftly(summary = "Blinded signing keypair")]
/// The blinded signing keypair.
pub struct BlindIdKeypairSpecifier {
    /// The nickname of the  hidden service.
    pub(crate) nickname: HsNickname,
    #[deftly(denotator)]
    /// The time period associated with this key.
    pub(crate) period: TimePeriod,
}

#[derive(Deftly, PartialEq, Debug, Constructor)]
#[derive_deftly(KeySpecifier, HsTimePeriodKeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(role = "KP_hs_blind_id")]
#[deftly(keypair_specifier = BlindIdKeypairSpecifier)]
#[deftly(summary = "Blinded public key")]
/// The blinded public key.
pub struct BlindIdPublicKeySpecifier {
    /// The nickname of the  hidden service.
    pub(crate) nickname: HsNickname,
    #[deftly(denotator)]
    /// The time period associated with this key.
    pub(crate) period: TimePeriod,
}

impl From<&BlindIdPublicKeySpecifier> for BlindIdKeypairSpecifier {
    fn from(
        hs_blind_id_public_key_specifier: &BlindIdPublicKeySpecifier,
    ) -> BlindIdKeypairSpecifier {
        BlindIdKeypairSpecifier::new(
            hs_blind_id_public_key_specifier.nickname.clone(),
            hs_blind_id_public_key_specifier.period,
        )
    }
}

#[derive(Deftly, PartialEq, Debug, Constructor)]
#[derive_deftly(KeySpecifier, HsTimePeriodKeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(role = "KS_hs_desc_sign")]
#[deftly(summary = "Descriptor signing key")]
/// The descriptor signing key.
pub struct DescSigningKeypairSpecifier {
    /// The nickname of the  hidden service.
    pub(crate) nickname: HsNickname,
    #[deftly(denotator)]
    /// The time period associated with this key.
    pub(crate) period: TimePeriod,
}

/// Denotates one of the keys, in the context of a particular HS and intro point
#[derive(Debug, Deftly, Eq, PartialEq, strum::Display, strum::EnumString)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum IptKeyRole {
    /// `k_hss_ntor`
    KHssNtor,
    /// `k_hss_ntor`
    KSid,
}

impl KeySpecifierComponentViaDisplayFromStr for IptKeyRole {}

/// Specifies an intro point key
#[derive(Debug, Deftly, Eq, PartialEq)]
#[derive_deftly(KeySpecifier)]
#[deftly(prefix = "hss")]
#[deftly(summary = "introduction point key")]
pub(crate) struct IptKeySpecifier {
    /// nick
    pub(crate) nick: HsNickname,
    /// which key
    #[deftly(fixed_path_component = "ipts")]
    #[deftly(role)]
    pub(crate) role: IptKeyRole,
    /// lid
    #[deftly(denotator)]
    pub(crate) lid: IptLocalId,
}

/// Expire publisher keys for no-longer relevant TPs
pub(crate) fn expire_publisher_keys(
    keymgr: &KeyMgr,
    nickname: &HsNickname,
    relevant_periods: &[HsDirParams],
) -> tor_keymgr::Result<()> {
    // TODO: any invalid/malformed keys are ignored (rather than
    // removed).
    let keys_to_remove = list_expired_keys_for_service(relevant_periods, nickname, keymgr)?;
    for entry in keys_to_remove {
        keymgr.remove_entry(&entry)?;
    }

    Ok(())
}

#[cfg(test)]
mod test {
    // @@ 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)]
    #![allow(clippy::string_slice)] // See arti#2571
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use tor_keymgr::KeySpecifier;
    use tor_keymgr::test_utils::check_key_specifier;

    #[test]
    fn hsid_key_specifiers() {
        let nickname = HsNickname::try_from("shallot".to_string()).unwrap();
        let key_spec = HsIdPublicKeySpecifier::new(nickname.clone());
        assert_eq!(
            key_spec.arti_path().unwrap().as_str(),
            "hss/shallot/kp_hs_id"
        );

        let key_spec = HsIdKeypairSpecifier::new(nickname);
        check_key_specifier(&key_spec, "hss/shallot/ks_hs_id");
    }

    #[test]
    fn blind_id_key_specifiers() {
        let nickname = HsNickname::try_from("shallot".to_string()).unwrap();
        let period = TimePeriod::from_parts(1, 2, 3);
        let key_spec = BlindIdPublicKeySpecifier::new(nickname.clone(), period);
        assert_eq!(
            key_spec.arti_path().unwrap().as_str(),
            "hss/shallot/kp_hs_blind_id+2_1_3"
        );

        let key_spec = BlindIdKeypairSpecifier::new(nickname, period);
        check_key_specifier(&key_spec, "hss/shallot/ks_hs_blind_id+2_1_3");
    }

    #[test]
    fn desc_signing_key_specifiers() {
        let nickname = HsNickname::try_from("shallot".to_string()).unwrap();
        let period = TimePeriod::from_parts(1, 2, 3);
        let key_spec = DescSigningKeypairSpecifier::new(nickname, period);
        check_key_specifier(&key_spec, "hss/shallot/ks_hs_desc_sign+2_1_3");
    }

    #[test]
    fn ipt_key_specifiers() {
        let nick = HsNickname::try_from("shallot".to_string()).unwrap();
        let lid = IptLocalId::dummy(1);
        let spec = |role| IptKeySpecifier {
            nick: nick.clone(),
            lid,
            role,
        };
        let lid_s = "0101010101010101010101010101010101010101010101010101010101010101";
        check_key_specifier(
            &spec(IptKeyRole::KHssNtor),
            &format!("hss/shallot/ipts/k_hss_ntor+{lid_s}"),
        );
        check_key_specifier(
            &spec(IptKeyRole::KSid),
            &format!("hss/shallot/ipts/k_sid+{lid_s}"),
        );
    }
}
