//! RFC 9421 HTTP Message Signature signing for Web Bot Auth (,
//! ).
//!
//! Builds the RFC 9421 signature base over the configured components,
//! signs it with Ed25519 or RSA-PSS (`rsa-pss-sha512`), and returns the
//! `Signature`, `Signature-Input`, and `Signature-Agent` headers (plus a
//! derived `Content-Digest` when that component is covered). The RSA-PSS
//! key loading and signing live in [`super::web_bot_auth_rsa`]; this module
//! owns the base assembly and the algorithm dispatch.
//!
//! The signature base follows RFC 9421 section 2.5: one line per covered
//! component in the form `"<name>": <value>`, then the
//! `"@signature-params"` line carrying the component list and the
//! `created`/`expires`/`keyid`/`alg`/`tag` parameters.

use std::time::{SystemTime, UNIX_EPOCH};

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::{Signer, SigningKey};
use sha2::{Digest, Sha256};

use super::web_bot_auth::{SignatureAlgorithm, WebBotAuthConfig};
use super::web_bot_auth_rsa;
use super::AuthError;

/// RFC 9421 `alg` identifier for the Ed25519 path.
const ED25519_ALG: &str = "ed25519";

/// Structured-field tag the Web Bot Auth profile uses on the signature.
const SIGNATURE_TAG: &str = "web-bot-auth";
/// The signature label. RFC 9421 allows any label; `sig1` is conventional.
const SIGNATURE_LABEL: &str = "sig1";

/// One signed header to attach to the outbound request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedHeader {
    /// Header name (e.g. `signature-input`).
    pub name: String,
    /// Header value.
    pub value: String,
}

/// Sign an outbound request with Web Bot Auth and return the headers to
/// attach. Dispatches to Ed25519 or RSA-PSS per the configured algorithm.
pub fn sign_request(
    method: &str,
    url: &str,
    body: &[u8],
    config: &WebBotAuthConfig,
) -> Result<Vec<SignedHeader>, AuthError> {
    config.validate()?;
    let pem = load_key_pem(config)?;
    let created = now_unix();
    let expires = created.saturating_add(config.expires_in_seconds);
    sign_with(method, url, body, config, &pem, created, expires)
}

/// Load and parse the signing key without signing anything, for the
/// `mcptest doctor` Web Bot Auth pre-flight.
///
/// Why a dedicated entry point: doctor wants to confirm the configured
/// key loads, parses, and uses a supported algorithm without issuing a
/// request or touching the network. The derived key id (base64 of the
/// public key for Ed25519, a SPKI fingerprint for RSA) is returned so
/// doctor can surface a non-secret identifier; the private key is never
/// returned or logged.
pub fn preflight_signing_key(config: &WebBotAuthConfig) -> Result<String, AuthError> {
    config.validate()?;
    let pem = load_key_pem(config)?;
    key_id_for(config.algorithm, &pem)
}

/// Signing core with an explicit PEM and clock, so a test can pin
/// `created`/`expires` and verify the signature round-trips.
fn sign_with(
    method: &str,
    url: &str,
    body: &[u8],
    config: &WebBotAuthConfig,
    pem: &str,
    created: u64,
    expires: u64,
) -> Result<Vec<SignedHeader>, AuthError> {
    let keyid = key_id_for(config.algorithm, pem)?;
    let alg = alg_identifier(config.algorithm);
    let prepared = prepare_components(method, url, body, &config.components)?;
    let params_line = signature_params(&config.components, created, expires, &keyid, alg);
    let base = signature_base(&prepared.lines, &params_line);

    let sig_b64 = BASE64.encode(sign_base(config.algorithm, base.as_bytes(), pem)?);

    let mut headers = prepared.derived_headers;
    headers.push(SignedHeader {
        name: "Signature-Input".into(),
        value: format!("{SIGNATURE_LABEL}={params_line}"),
    });
    headers.push(SignedHeader {
        name: "Signature".into(),
        value: format!("{SIGNATURE_LABEL}=:{sig_b64}:"),
    });
    headers.push(SignedHeader {
        name: "Signature-Agent".into(),
        value: quoted(&config.signature_agent),
    });
    Ok(headers)
}

/// Produce the raw signature bytes for the prepared base under the chosen
/// algorithm. Ed25519 signs directly; RSA-PSS defers to the sibling module.
fn sign_base(alg: SignatureAlgorithm, base: &[u8], pem: &str) -> Result<Vec<u8>, AuthError> {
    match alg {
        SignatureAlgorithm::Ed25519 => {
            let key = parse_ed25519(pem)?;
            Ok(key.sign(base).to_bytes().to_vec())
        }
        SignatureAlgorithm::RsaPss => web_bot_auth_rsa::sign_pss(base, pem),
    }
}

/// RFC 9421 `alg` identifier for the chosen algorithm.
fn alg_identifier(alg: SignatureAlgorithm) -> &'static str {
    match alg {
        SignatureAlgorithm::Ed25519 => ED25519_ALG,
        SignatureAlgorithm::RsaPss => web_bot_auth_rsa::RSA_PSS_ALG,
    }
}

/// Derive the non-secret key id for the chosen algorithm without signing.
fn key_id_for(alg: SignatureAlgorithm, pem: &str) -> Result<String, AuthError> {
    match alg {
        SignatureAlgorithm::Ed25519 => Ok(key_id(&parse_ed25519(pem)?)),
        SignatureAlgorithm::RsaPss => web_bot_auth_rsa::key_id_from_pem(pem),
    }
}

/// Components prepared for signing: the canonicalized signature-base
/// lines plus any headers the signer derived (e.g. `Content-Digest`)
/// that must also be attached to the request.
#[derive(Debug)]
struct Prepared {
    lines: Vec<String>,
    derived_headers: Vec<SignedHeader>,
}

/// Build one signature-base line per component and collect derived
/// headers. Supports `@authority`, `@target-uri`, `@method`, `@path`,
/// and `content-digest`; an unknown component is a typed error.
fn prepare_components(
    method: &str,
    url: &str,
    body: &[u8],
    components: &[String],
) -> Result<Prepared, AuthError> {
    let parsed =
        url::Url::parse(url).map_err(|e| crypto_error(&format!("invalid request url: {e}")))?;
    let mut lines = Vec::with_capacity(components.len());
    let mut derived_headers = Vec::new();
    for component in components {
        let value = match component.as_str() {
            "@authority" => authority(&parsed),
            "@target-uri" => parsed.as_str().to_string(),
            "@method" => method.to_ascii_uppercase(),
            "@path" => parsed.path().to_string(),
            "content-digest" => {
                let digest = content_digest(body);
                derived_headers.push(SignedHeader {
                    name: "Content-Digest".into(),
                    value: digest.clone(),
                });
                digest
            }
            other => {
                return Err(crypto_error(&format!(
                    "unsupported signed component `{other}`"
                )));
            }
        };
        lines.push(format!("{}: {value}", quoted(component)));
    }
    Ok(Prepared {
        lines,
        derived_headers,
    })
}

/// RFC 9421 `@authority`: host plus a non-default port.
fn authority(url: &url::Url) -> String {
    match (url.host_str(), url.port()) {
        (Some(host), Some(port)) => format!("{host}:{port}"),
        (Some(host), None) => host.to_string(),
        (None, _) => String::new(),
    }
}

/// RFC 9421 `Content-Digest` value: `sha-256=:base64(sha256(body)):`.
fn content_digest(body: &[u8]) -> String {
    let digest = Sha256::digest(body);
    format!("sha-256=:{}:", BASE64.encode(digest))
}

/// Build the `@signature-params` value: the inner component list plus the
/// structured-field parameters. This string is both the value of the
/// `Signature-Input` header and the final line of the signature base.
fn signature_params(
    components: &[String],
    created: u64,
    expires: u64,
    keyid: &str,
    alg: &str,
) -> String {
    let inner = components
        .iter()
        .map(|c| quoted(c))
        .collect::<Vec<_>>()
        .join(" ");
    format!(
        "({inner});created={created};expires={expires};keyid={};alg={};tag={}",
        quoted(keyid),
        quoted(alg),
        quoted(SIGNATURE_TAG)
    )
}

/// Assemble the full signature base: one line per component, then the
/// `"@signature-params"` line.
fn signature_base(lines: &[String], params_line: &str) -> String {
    let mut base = lines.join("\n");
    if !base.is_empty() {
        base.push('\n');
    }
    base.push_str(&format!("\"@signature-params\": {params_line}"));
    base
}

/// Derive a stable key id from the public key: base64 of the raw 32-byte
/// Ed25519 public key. The directory-published key id format is deferred;
/// this is deterministic and verifiable from the public key alone.
fn key_id(key: &SigningKey) -> String {
    BASE64.encode(key.verifying_key().to_bytes())
}

/// Read the PEM private key from the configured source (file or env)
/// without parsing it. The algorithm-specific parser runs downstream so a
/// single read path serves both Ed25519 and RSA. Never logs the key bytes.
fn load_key_pem(config: &WebBotAuthConfig) -> Result<String, AuthError> {
    if let Some(path) = &config.key_path {
        super::check_key_file_permissions(path)?;
        std::fs::read_to_string(path).map_err(|source| AuthError::Io {
            path: path.clone(),
            source,
        })
    } else if let Some(env_name) = &config.key_env {
        std::env::var(env_name).map_err(|_| AuthError::MissingEnvVar {
            name: env_name.clone(),
        })
    } else {
        Err(crypto_error("no key source (key_path or key_env)"))
    }
}

/// Parse the PEM as an Ed25519 PKCS#8 private key. A parse failure is a
/// typed crypto error whose message never includes the key bytes.
fn parse_ed25519(pem: &str) -> Result<SigningKey, AuthError> {
    SigningKey::from_pkcs8_pem(pem)
        .map_err(|e| crypto_error(&format!("failed to parse Ed25519 private key: {e}")))
}

/// Wrap a value in double quotes per the RFC 9421 / structured-field
/// string form. Component identifiers and string parameters use this.
fn quoted(value: &str) -> String {
    format!("\"{value}\"")
}

fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

fn crypto_error(reason: &str) -> AuthError {
    AuthError::Crypto {
        scheme: "web_bot_auth",
        reason: reason.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::pkcs8::EncodePrivateKey;
    use ed25519_dalek::Verifier;

    fn test_config() -> WebBotAuthConfig {
        WebBotAuthConfig {
            key_path: None,
            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: super::super::web_bot_auth::default_signed_components(),
            expires_in_seconds: 30,
        }
    }

    fn fixed_key() -> SigningKey {
        // Deterministic test key from fixed seed bytes. Not a real secret.
        let seed: [u8; 32] = [7u8; 32];
        SigningKey::from_bytes(&seed)
    }

    /// PKCS#8 PEM for the fixed Ed25519 test key, the shape `sign_with` now
    /// loads. Not a real secret.
    fn fixed_key_pem() -> String {
        fixed_key()
            .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
            .expect("encode pem")
            .to_string()
    }

    /// Generate a 2048-bit RSA key as PKCS#8 PEM for the RSA-PSS path tests.
    /// Generated in-process so no key fixture is committed.
    fn rsa_key_pem() -> String {
        use rsa::pkcs8::EncodePrivateKey;
        let mut rng = rand::rngs::OsRng;
        let key = rsa::RsaPrivateKey::new(&mut rng, 2048).expect("generate rsa key");
        key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
            .expect("encode rsa pem")
            .to_string()
    }

    #[test]
    fn signature_base_lists_components_then_params() {
        let cfg = test_config();
        let key = fixed_key();
        let keyid = key_id(&key);
        let prepared =
            prepare_components("POST", "https://mcptest.sh/mcp", b"{}", &cfg.components).unwrap();
        let params = signature_params(
            &cfg.components,
            1_700_000_000,
            1_700_000_030,
            &keyid,
            ED25519_ALG,
        );
        let base = signature_base(&prepared.lines, &params);

        // One line per component, in order, then @signature-params.
        let lines: Vec<&str> = base.lines().collect();
        assert_eq!(lines[0], "\"@authority\": mcptest.sh");
        assert_eq!(lines[1], "\"@target-uri\": https://mcptest.sh/mcp");
        assert!(lines[2].starts_with("\"content-digest\": sha-256=:"));
        assert!(lines[3].starts_with(
            "\"@signature-params\": (\"@authority\" \"@target-uri\" \"content-digest\")"
        ));
        assert!(lines[3].contains("created=1700000000"));
        assert!(lines[3].contains("expires=1700000030"));
        assert!(lines[3].contains("alg=\"ed25519\""));
        assert!(lines[3].contains("tag=\"web-bot-auth\""));
    }

    #[test]
    fn signature_input_lists_the_configured_components() {
        let cfg = test_config();
        let headers = sign_with(
            "POST",
            "https://mcptest.sh/mcp",
            b"{}",
            &cfg,
            &fixed_key_pem(),
            1_700_000_000,
            1_700_000_030,
        )
        .unwrap();
        let sig_input = headers
            .iter()
            .find(|h| h.name == "Signature-Input")
            .expect("Signature-Input present");
        assert!(sig_input
            .value
            .starts_with("sig1=(\"@authority\" \"@target-uri\" \"content-digest\")"));

        // The agent and a content-digest header are attached too.
        assert!(headers.iter().any(|h| h.name == "Signature-Agent"));
        assert!(headers.iter().any(|h| h.name == "Content-Digest"));
        assert!(headers.iter().any(|h| h.name == "Signature"));
    }

    #[test]
    fn signature_verifies_with_the_public_key() {
        let cfg = test_config();
        let key = fixed_key();
        let created = 1_700_000_000;
        let expires = 1_700_000_030;
        let headers = sign_with(
            "POST",
            "https://mcptest.sh/mcp",
            b"{}",
            &cfg,
            &fixed_key_pem(),
            created,
            expires,
        )
        .unwrap();

        // Reconstruct the signature base the verifier would build.
        let keyid = key_id(&key);
        let prepared =
            prepare_components("POST", "https://mcptest.sh/mcp", b"{}", &cfg.components).unwrap();
        let params = signature_params(&cfg.components, created, expires, &keyid, ED25519_ALG);
        let base = signature_base(&prepared.lines, &params);

        // Pull the base64 signature out of the `Signature` header value.
        let sig_header = headers
            .iter()
            .find(|h| h.name == "Signature")
            .expect("Signature present");
        let b64 = sig_header
            .value
            .trim_start_matches("sig1=:")
            .trim_end_matches(':');
        let raw = BASE64.decode(b64).expect("valid base64 signature");
        let sig_bytes: [u8; 64] = raw.as_slice().try_into().expect("64-byte signature");
        let signature = ed25519_dalek::Signature::from_bytes(&sig_bytes);

        key.verifying_key()
            .verify(base.as_bytes(), &signature)
            .expect("public key verifies the signature");
    }

    #[test]
    fn rsa_pss_signs_and_verifies_round_trip() {
        let pem = rsa_key_pem();
        let mut cfg = test_config();
        cfg.algorithm = SignatureAlgorithm::RsaPss;
        let created = 1_700_000_000;
        let expires = 1_700_000_030;
        let headers = sign_with(
            "POST",
            "https://mcptest.sh/mcp",
            b"{}",
            &cfg,
            &pem,
            created,
            expires,
        )
        .unwrap();

        // The params line advertises the RSA-PSS algorithm.
        let sig_input = headers
            .iter()
            .find(|h| h.name == "Signature-Input")
            .expect("Signature-Input present");
        assert!(sig_input.value.contains("alg=\"rsa-pss-sha512\""));

        // Reconstruct the base and verify with the public half of the key.
        use rsa::pss::{Signature as PssSignature, VerifyingKey};
        use rsa::sha2::Sha512;
        use rsa::signature::Verifier;
        let priv_key = rsa::RsaPrivateKey::from_pkcs8_pem(&pem).unwrap();
        let keyid = key_id_for(SignatureAlgorithm::RsaPss, &pem).unwrap();
        let prepared =
            prepare_components("POST", "https://mcptest.sh/mcp", b"{}", &cfg.components).unwrap();
        let params = signature_params(
            &cfg.components,
            created,
            expires,
            &keyid,
            web_bot_auth_rsa::RSA_PSS_ALG,
        );
        let base = signature_base(&prepared.lines, &params);
        let sig_header = headers.iter().find(|h| h.name == "Signature").unwrap();
        let b64 = sig_header
            .value
            .trim_start_matches("sig1=:")
            .trim_end_matches(':');
        let raw = BASE64.decode(b64).expect("valid base64 signature");
        let signature = PssSignature::try_from(raw.as_slice()).expect("decode rsa signature");
        let verifying_key = VerifyingKey::<Sha512>::new(priv_key.to_public_key());
        verifying_key
            .verify(base.as_bytes(), &signature)
            .expect("rsa-pss signature verifies");
    }

    #[test]
    fn loads_key_from_env_and_signs() {
        let pem = fixed_key_pem();
        let env_name = format!("MCPTEST_WBA_KEY_{}", std::process::id());
        std::env::set_var(&env_name, pem);
        let mut cfg = test_config();
        cfg.key_env = Some(env_name.clone());
        let result = sign_request("GET", "https://mcptest.sh/mcp", &[], &cfg);
        std::env::remove_var(&env_name);
        assert!(result.is_ok(), "should sign with env-sourced key");
    }

    #[test]
    fn preflight_returns_keyid_for_ed25519_env_key() {
        let key = fixed_key();
        let pem = fixed_key_pem();
        let env_name = format!("MCPTEST_WBA_PREFLIGHT_{}", std::process::id());
        std::env::set_var(&env_name, pem);
        let mut cfg = test_config();
        cfg.key_env = Some(env_name.clone());
        let result = preflight_signing_key(&cfg);
        std::env::remove_var(&env_name);
        let keyid = result.expect("preflight should load the key");
        assert_eq!(keyid, key_id(&key));
    }

    #[test]
    fn preflight_returns_keyid_for_rsa_pss_env_key() {
        let pem = rsa_key_pem();
        let env_name = format!("MCPTEST_WBA_RSA_PREFLIGHT_{}", std::process::id());
        std::env::set_var(&env_name, &pem);
        let mut cfg = test_config();
        cfg.algorithm = SignatureAlgorithm::RsaPss;
        cfg.key_env = Some(env_name.clone());
        let result = preflight_signing_key(&cfg);
        std::env::remove_var(&env_name);
        let keyid = result.expect("preflight should load the rsa key");
        assert_eq!(keyid, web_bot_auth_rsa::key_id_from_pem(&pem).unwrap());
    }

    #[test]
    fn unknown_component_is_rejected() {
        let err = prepare_components(
            "POST",
            "https://mcptest.sh/mcp",
            b"{}",
            &["@nonsense".to_string()],
        )
        .unwrap_err();
        assert!(matches!(err, AuthError::Crypto { .. }));
    }
}
