//! Web Bot Auth client (HTTP Message Signatures, RFC 9421 + draft-meunier)
//! for URL targets.
//!
//! The client signs outbound requests with Ed25519 or RSA-PSS per RFC
//! 9421: it builds the signature base over the configured components,
//! signs it, and attaches `Signature`, `Signature-Input`, and
//! `Signature-Agent`. The signing path lives in the sibling
//! [`super::web_bot_auth_sign`] module; this module owns the config and
//! validation.
//!
//! Spec versions targeted:
//! - RFC 9421 (HTTP Message Signatures)
//! - `draft-meunier-web-bot-auth-architecture` (umbrella draft)
//! - `draft-meunier-http-message-signatures-directory`
//!
//! Both Ed25519 and RSA-PSS (`rsa-pss-sha512`) are supported. See
//! `docs/web-bot-auth.md` and `docs/adr/0033-web-bot-auth-client.md` for
//! design rationale.

use std::path::PathBuf;

use serde::{Deserialize, Serialize};

use super::AuthError;

pub use super::web_bot_auth_sign::{preflight_signing_key, sign_request};

#[cfg(test)]
pub(crate) use defaults::default_signed_components;

#[cfg(test)]
mod defaults {
    /// Spec-default signed components, exposed to the signer's test
    /// module so it can build a config without re-listing them.
    pub(crate) fn default_signed_components() -> Vec<String> {
        super::default_components()
    }
}

/// Signature algorithm. Ed25519 is the default; RSA-PSS is supported
/// for environments that already manage RSA keys.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum SignatureAlgorithm {
    /// Ed25519 (recommended by the draft for new deployments).
    #[default]
    Ed25519,
    /// RSA-PSS over SHA-512 (`rsa-pss-sha512`), for sites already managing
    /// RSA keys.
    RsaPss,
}

/// Web Bot Auth configuration for a URL-target server.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WebBotAuthConfig {
    /// PEM file containing the private signing key. Mutually exclusive
    /// with `key_env`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub key_path: Option<PathBuf>,
    /// Env var holding the PEM-encoded private key.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub key_env: Option<String>,
    /// URL where the public-key directory is published, per
    /// draft-meunier-http-message-signatures-directory.
    pub directory_url: String,
    /// `Signature-Agent` header value: identifies the bot.
    pub signature_agent: String,
    /// Signature algorithm.
    #[serde(default)]
    pub algorithm: SignatureAlgorithm,
    /// Components covered by the signature input (per RFC 9421).
    /// Defaults to `["@authority", "@target-uri", "content-digest"]`.
    #[serde(default = "default_components")]
    pub components: Vec<String>,
    /// Signature validity window in seconds. Default 30s.
    #[serde(default = "default_expires_in_seconds")]
    pub expires_in_seconds: u64,
}

fn default_components() -> Vec<String> {
    vec![
        "@authority".into(),
        "@target-uri".into(),
        "content-digest".into(),
    ]
}

fn default_expires_in_seconds() -> u64 {
    30
}

impl WebBotAuthConfig {
    /// Validate the config. Both key sources cannot be set at once and
    /// at least one must be set when Web Bot Auth is enabled.
    pub fn validate(&self) -> Result<(), AuthError> {
        if self.key_path.is_some() && self.key_env.is_some() {
            return Err(AuthError::InvalidConfig {
                reason: "web_bot_auth.key_path and key_env are mutually exclusive".into(),
            });
        }
        if self.key_path.is_none() && self.key_env.is_none() {
            return Err(AuthError::InvalidConfig {
                reason: "web_bot_auth requires either key_path or key_env".into(),
            });
        }
        if self.directory_url.trim().is_empty() {
            return Err(AuthError::InvalidConfig {
                reason: "web_bot_auth.directory_url must not be empty".into(),
            });
        }
        if self.signature_agent.trim().is_empty() {
            return Err(AuthError::InvalidConfig {
                reason: "web_bot_auth.signature_agent must not be empty".into(),
            });
        }
        if self.components.is_empty() {
            return Err(AuthError::InvalidConfig {
                reason: "web_bot_auth.components must list at least one signed component".into(),
            });
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn well_formed_config() -> WebBotAuthConfig {
        WebBotAuthConfig {
            key_path: Some("bot.ed25519".into()),
            key_env: None,
            directory_url: "https://mcptest.sh/.well-known/http-message-signatures-directory"
                .into(),
            signature_agent: "https://mcptest.sh".into(),
            algorithm: SignatureAlgorithm::Ed25519,
            components: default_components(),
            expires_in_seconds: 30,
        }
    }

    #[test]
    fn well_formed_config_validates() {
        assert!(well_formed_config().validate().is_ok());
    }

    #[test]
    fn missing_key_rejected() {
        let mut cfg = well_formed_config();
        cfg.key_path = None;
        assert!(matches!(
            cfg.validate(),
            Err(AuthError::InvalidConfig { .. })
        ));
    }

    #[test]
    fn both_key_sources_rejected() {
        let mut cfg = well_formed_config();
        cfg.key_env = Some("MCPTEST_BOT_KEY".into());
        assert!(matches!(
            cfg.validate(),
            Err(AuthError::InvalidConfig { .. })
        ));
    }

    #[test]
    fn empty_directory_url_rejected() {
        let mut cfg = well_formed_config();
        cfg.directory_url = String::new();
        assert!(matches!(
            cfg.validate(),
            Err(AuthError::InvalidConfig { .. })
        ));
    }

    #[test]
    fn empty_signature_agent_rejected() {
        let mut cfg = well_formed_config();
        cfg.signature_agent = String::new();
        assert!(matches!(
            cfg.validate(),
            Err(AuthError::InvalidConfig { .. })
        ));
    }

    #[test]
    fn empty_components_rejected() {
        let mut cfg = well_formed_config();
        cfg.components = Vec::new();
        assert!(matches!(
            cfg.validate(),
            Err(AuthError::InvalidConfig { .. })
        ));
    }

    #[test]
    fn algorithm_round_trips_through_serde() {
        let cfg = WebBotAuthConfig {
            algorithm: SignatureAlgorithm::RsaPss,
            ..well_formed_config()
        };
        let yaml = serde_yaml::to_string(&cfg).unwrap();
        assert!(yaml.contains("rsa-pss"));
        let decoded: WebBotAuthConfig = serde_yaml::from_str(&yaml).unwrap();
        assert_eq!(cfg, decoded);
    }

    #[test]
    fn default_components_are_the_spec_default() {
        let comps = default_components();
        assert_eq!(comps, vec!["@authority", "@target-uri", "content-digest"]);
    }
}
