//! The top-level settings object for OpenPGP related settings.

use std::{fmt::Display, num::NonZeroUsize};

use garde::Validate;
use serde::{Deserialize, Serialize};

use crate::{
    Error,
    config::technology::openpgp::{PlainMode, TrustAnchorMode, WebOfTrustMode},
    file::{ConfigOpenpgpSettings, ConfigVerificationMethod},
};

/// Default required amount of signatures for an artifact.
const DEFAULT_REQUIRED_SIGNATURES: NonZeroUsize =
    NonZeroUsize::new(1).expect("1 is greater than 0");

/// The required number of data signatures for an artifact.
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct NumDataSignatures(NonZeroUsize);

impl NumDataSignatures {
    /// Creates a new [`NumDataSignatures`] from a [`NonZeroUsize`].
    pub fn new(num: NonZeroUsize) -> Self {
        Self(num)
    }

    /// Returns the inner [`NonZeroUsize`].
    pub fn get(&self) -> NonZeroUsize {
        self.0
    }
}

impl Default for NumDataSignatures {
    fn default() -> Self {
        NumDataSignatures(DEFAULT_REQUIRED_SIGNATURES)
    }
}

impl Display for NumDataSignatures {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

/// The OpenPGP verification method to use for OpenPGP verifiers.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Validate)]
#[serde(rename_all = "snake_case")]
pub enum VerificationMethod {
    /// Only use artifact verifiers, ignore trust-anchors.
    Plain(#[garde(skip)] PlainMode),

    /// Always use trust-anchors for artifact verifiers.
    TrustAnchor(#[garde(dive)] TrustAnchorMode),

    /// Use the "Web of Trust" model.
    WebOfTrust(#[garde(skip)] WebOfTrustMode),
}

impl Default for VerificationMethod {
    fn default() -> Self {
        Self::TrustAnchor(TrustAnchorMode::default())
    }
}

impl Display for VerificationMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Plain(mode) => write!(f, "{mode}"),
            Self::TrustAnchor(mode) => write!(f, "{mode}"),
            Self::WebOfTrust(mode) => write!(f, "{mode}"),
        }
    }
}

/// Validates that the required number of data signatures matches the verification method.
///
/// # Errors
///
/// Returns an error, if the `verification_method` is [`PlainMode`] and its number of
/// `fingerprint_matches` is fewer than that of `num_data_signatures`.
fn validate_num_data_signatures(
    verification_method: &VerificationMethod,
) -> impl FnOnce(&NumDataSignatures, &()) -> garde::Result + '_ {
    move |num_data_signatures, _| {
        if let VerificationMethod::Plain(mode) = verification_method {
            let num_fingerprints = mode.fingerprint_matches().len();
            if num_fingerprints > 0 && num_data_signatures.get().get() > num_fingerprints {
                return Err(garde::Error::new(format!(
                    "must be <= {num_fingerprints}, as the plain verification mode pins {num_fingerprints} artifact verifier fingerprint(s)"
                )));
            }
        }
        Ok(())
    }
}

/// OpenPGP settings.
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize, Validate)]
pub struct OpenpgpSettings {
    /// The number of signatures required for an artifact to be considered valid.
    #[garde(custom(validate_num_data_signatures(&self.verification_method)))]
    num_data_signatures: NumDataSignatures,

    /// The verification method to use.
    #[garde(dive)]
    pub(crate) verification_method: VerificationMethod,
}

impl OpenpgpSettings {
    /// Creates a new [`OpenpgpSettings`].
    ///
    /// # Errors
    ///
    /// Returns an error if validation for the created [`OpenpgpSettings`] fails:
    /// The `verification_method` uses [`PlainMode`] and `num_data_signatures` is larger than the
    /// length of the verification mode's `fingerprint_matches`,
    pub fn new(
        num_data_signatures: NumDataSignatures,
        verification_method: VerificationMethod,
    ) -> Result<Self, Error> {
        let settings = Self {
            num_data_signatures,
            verification_method,
        };

        settings.validate().map_err(|source| Error::Validation {
            context: "creating an OpenPGP settings object".to_string(),
            source,
        })?;

        Ok(settings)
    }

    /// Creates a new [`OpenpgpSettings`] from a [`ConfigOpenpgpSettings`] and explicit defaults.
    ///
    /// # Errors
    ///
    /// Returns an error if [`TrustAnchorMode::from_config_with_defaults`] fails.
    pub(crate) fn from_config_with_defaults(
        config_openpgp_settings: &ConfigOpenpgpSettings,
        defaults: &OpenpgpSettings,
    ) -> Result<Self, Error> {
        let num_data_signatures = config_openpgp_settings
            .num_data_signatures()
            .unwrap_or(defaults.num_data_signatures());
        let verification_method = match (
            config_openpgp_settings.config_verification_method(),
            defaults.verification_method(),
        ) {
            // Replace unset fields on the `ConfigPlainMode` with the specific fields of a
            // `TrustAnchorMode`.
            (
                ConfigVerificationMethod::Plain(config_mode),
                VerificationMethod::Plain(default_mode),
            ) => VerificationMethod::Plain(PlainMode::from_config_with_defaults(
                config_mode,
                default_mode,
            )),
            // Replace unset fields on the `ConfigPlainMode` with inherent defaults.
            (ConfigVerificationMethod::Plain(config_mode), _) => VerificationMethod::Plain(
                PlainMode::from_config_with_defaults(config_mode, &PlainMode::default()),
            ),
            // Replace unset fields on the `ConfigTrustAnchorMode` with the specific fields of a
            // `TrustAnchorMode`.
            (
                ConfigVerificationMethod::TrustAnchor(config_mode),
                VerificationMethod::TrustAnchor(defaults_mode),
            ) => VerificationMethod::TrustAnchor(TrustAnchorMode::from_config_with_defaults(
                config_mode,
                defaults_mode,
            )?),
            // Replace unset fields on the `ConfigTrustAnchorMode` with inherent defaults.
            (ConfigVerificationMethod::TrustAnchor(config_mode), _) => {
                VerificationMethod::TrustAnchor(TrustAnchorMode::from_config_with_defaults(
                    config_mode,
                    &TrustAnchorMode::default(),
                )?)
            }
            // Replace unset fields on the `ConfigWebOfTrustMode` with the specific fields of a
            // `WebOfTrustMode`.
            (
                ConfigVerificationMethod::WebOfTrust(config_mode),
                VerificationMethod::WebOfTrust(defaults_mode),
            ) => VerificationMethod::WebOfTrust(WebOfTrustMode::from_config_with_defaults(
                config_mode,
                defaults_mode,
            )),
            // Replace unset fields on the `ConfigWebOfTrustMode` with inherent defaults.
            (ConfigVerificationMethod::WebOfTrust(config_mode), _) => {
                VerificationMethod::WebOfTrust(WebOfTrustMode::from_config_with_defaults(
                    config_mode,
                    &WebOfTrustMode::default(),
                ))
            }
        };

        Self::new(num_data_signatures, verification_method)
    }

    /// Returns the required number of data signatures required for an artifact to be considered
    /// valid.
    pub fn num_data_signatures(&self) -> NumDataSignatures {
        self.num_data_signatures
    }

    /// Returns a reference to the OpenPGP verification method.
    pub fn verification_method(&self) -> &VerificationMethod {
        &self.verification_method
    }
}

impl Display for OpenpgpSettings {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "OpenPGP settings\n")?;
        writeln!(
            f,
            "🔏 Each artifact requires {} valid data signature(s) from artifact verifiers to be successfully verified.\n",
            self.num_data_signatures
        )?;

        write!(f, "{}", self.verification_method())
    }
}

#[cfg(test)]
mod tests {
    use std::{collections::HashSet, num::NonZeroU8};

    use rstest::rstest;
    use testresult::TestResult;

    use super::*;
    use crate::{
        config::technology::openpgp::{
            NumCertifications,
            TrustAmountFlow,
            TrustAmountPartial,
            TrustAmountRoot,
            WebOfTrustRoot,
        },
        file::{ConfigWebOfTrustMode, ConfigWebOfTrustRoot},
    };

    /// Ensures that [`OpenpgpSettings::from_config_with_defaults`] behaves consistently.
    #[rstest]
    #[case::all_defaults(
        ConfigOpenpgpSettings::default(),
        OpenpgpSettings::default(),
        OpenpgpSettings::default()
    )]
    #[case::default_config_and_custom_defaults(
        ConfigOpenpgpSettings::default(),
        OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
            VerificationMethod::Plain(PlainMode::new(
                HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
                HashSet::from_iter([
                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?
                ])
            ))
        )?,
        OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
            VerificationMethod::TrustAnchor(TrustAnchorMode::default())
        )?
    )]
    #[case::custom_config_and_default_defaults(
        ConfigOpenpgpSettings::new(
            Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
            ConfigVerificationMethod::WebOfTrust(
                ConfigWebOfTrustMode::new(
                    Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
                    Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                    Some(HashSet::from_iter([
                        ConfigWebOfTrustRoot::new(
                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                        ),
                        ConfigWebOfTrustRoot::new(
                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                        ),
                    ])),
                    Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]))
                )
            )
        )?,
        OpenpgpSettings::default(),
        OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0")),
            VerificationMethod::WebOfTrust(
                WebOfTrustMode::new(
                    TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
                    TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                    HashSet::from_iter([
                        WebOfTrustRoot::new(
                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                        ),
                        WebOfTrustRoot::new(
                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                        ),
                    ]),
                    HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])
                )
            )
        )?
    )]
    #[case::custom_config_and_custom_defaults(
        ConfigOpenpgpSettings::new(
            Some(NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0"))),
            ConfigVerificationMethod::WebOfTrust(
                ConfigWebOfTrustMode::new(
                    Some(TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0"))),
                    Some(TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                    Some(HashSet::from_iter([
                        ConfigWebOfTrustRoot::new(
                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                        ),
                        ConfigWebOfTrustRoot::new(
                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                            Some(TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?),
                        ),
                    ])),
                    Some(HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]))
                )
            )
        )?,
        OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
            VerificationMethod::Plain(PlainMode::new(
                HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?]),
                HashSet::from_iter([
                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?
                ])
            ))
        )?,
        OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(1).expect("1 is larger than 0")),
            VerificationMethod::WebOfTrust(
                WebOfTrustMode::new(
                    TrustAmountFlow::new(NonZeroUsize::new(140).expect("140 is larger than 0")),
                    TrustAmountPartial::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                    HashSet::from_iter([
                        WebOfTrustRoot::new(
                            "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                        ),
                        WebOfTrustRoot::new(
                            "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                            TrustAmountRoot::new(NonZeroU8::new(50).expect("50 is larger than 0"))?,
                        ),
                    ]),
                    HashSet::from_iter(["example.org".parse()?, "sub.example.org".parse()?])
                )
            )
        )?
    )]
    fn openpgp_settings_from_config_with_defaults(
        #[case] config: ConfigOpenpgpSettings,
        #[case] defaults: OpenpgpSettings,
        #[case] expected_output: OpenpgpSettings,
    ) -> TestResult {
        let config = OpenpgpSettings::from_config_with_defaults(&config, &defaults)?;
        assert_eq!(config, expected_output);
        Ok(())
    }

    /// Ensures that [`OpenpgpSettings::validate`] succeeds if the `num_data_signatures` is equal to
    /// or smaller than the number of `fingerprint_matches` in a [`PlainMode`] verification method.
    #[test]
    fn openpgp_settings_validate_succeeds_with_plain_mode() -> TestResult {
        let result = OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(1).expect("2 is larger than 0")),
            VerificationMethod::Plain(PlainMode::new(
                HashSet::new(),
                HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
            )),
        );

        match result {
            Ok(settings) => {
                eprintln!("{}", settings.num_data_signatures());
            }
            Err(error) => {
                panic!("Should have succeeded but failed instead: {error}")
            }
        }

        Ok(())
    }

    /// Ensures that [`OpenpgpSettings::validate`] fails if the `num_data_signatures` is larger than
    /// the number of `fingerprint_matches` in a [`PlainMode`].
    #[test]
    fn openpgp_settings_validate_fails_on_num_data_signature_mismatch() -> TestResult {
        let result = OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(2).expect("2 is larger than 0")),
            VerificationMethod::Plain(PlainMode::new(
                HashSet::new(),
                HashSet::from_iter(["f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?]),
            )),
        );

        match result {
            Err(Error::Validation { .. }) => {}
            Err(error) => panic!(
                "Should have failed with an Error::Validation but failed differently: {error}"
            ),
            Ok(settings) => {
                panic!(
                    "Should have failed with a garde::error::Error but succeeded instead: {settings:?}"
                )
            }
        }

        Ok(())
    }

    /// Ensures that [`OpenpgpSettings::validate`] succeeds if the `required_certifications` is
    /// equal to or smaller than the number of `trust_anchor_fingerprint_matches` in a
    /// [`TrustAnchorMode`] verification method.
    #[test]
    fn openpgp_settings_validate_succeeds_with_trust_anchor_mode() -> TestResult {
        let result = OpenpgpSettings::new(
            NumDataSignatures::new(NonZeroUsize::new(1).expect("2 is larger than 0")),
            VerificationMethod::TrustAnchor(TrustAnchorMode::new(
                NumCertifications::new(NonZeroUsize::new(3).expect("3 is larger than 0")),
                HashSet::new(),
                HashSet::from_iter([
                    "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15".parse()?,
                    "e242ed3bffccdf271b7fbaf34ed72d089537b42f".parse()?,
                    "6eadeac2dade6347e87c0d24fd455feffa7069f0".parse()?,
                ]),
            )?),
        );

        match result {
            Ok(_settings) => {}
            Err(error) => {
                panic!("Should have succeeded but failed instead: {error}")
            }
        }

        Ok(())
    }
}
