//! HMAC signature verification for Meta and Twilio WhatsApp webhooks.
//!
//! - **Meta** signs every inbound request with the app secret and sends the
//!   result as `X-Hub-Signature-256: sha256=<hex_digest>` (HMAC-SHA256).
//! - **Twilio** signs every inbound request with the auth token and sends the
//!   result as `X-Twilio-Signature: <base64_digest>` (HMAC-SHA1).
//!
//! Both paths use a constant-time comparison from `subtle` to prevent timing
//! attacks.

use std::fmt::Write as FmtWrite;

use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use hmac::{Hmac, KeyInit, Mac};
use sha1::Sha1;
use sha2::Sha256;
use subtle::ConstantTimeEq;

type HmacSha256 = Hmac<Sha256>;
type HmacSha1 = Hmac<Sha1>;

/// Verify the `X-Hub-Signature-256` header sent by Meta.
///
/// Meta computes `HMAC-SHA256(app_secret, raw_body)` and sends the result
/// as `sha256=<hex_digest>` in the `X-Hub-Signature-256` header.
///
/// Returns `true` if the signature is valid, `false` otherwise (including
/// when the header is missing or malformed).
pub(super) fn verify_meta_signature(app_secret: &str, body: &[u8], signature: &str) -> bool {
    // The header value must start with "sha256=".
    let Some(hex_sig) = signature.strip_prefix("sha256=") else {
        return false;
    };

    // Compute HMAC-SHA256 over the raw request body.
    // `new_from_slice` accepts any key length, so `expect` is unreachable.
    let mut mac =
        HmacSha256::new_from_slice(app_secret.as_bytes()).expect("HMAC accepts any key size");
    mac.update(body);
    let computed = mac.finalize().into_bytes();

    // Hex-encode the computed digest (32 bytes → 64 hex chars).
    let mut expected_hex = String::with_capacity(64);
    for byte in computed.iter() {
        write!(expected_hex, "{byte:02x}").unwrap();
    }

    // Constant-time comparison to prevent timing attacks.
    expected_hex.as_bytes().ct_eq(hex_sig.as_bytes()).into()
}

/// Verify the `X-Twilio-Signature` header sent by Twilio.
///
/// Twilio's algorithm (see Twilio docs — "Validating Requests from Twilio"):
/// 1. Start with the full URL of the request.
/// 2. Sort POST parameters alphabetically and append each `key + value`
///    (no delimiters) to the URL string.
/// 3. Compute `HMAC-SHA1(auth_token, signed_string)`.
/// 4. Base64-encode the result.
/// 5. Compare with the `X-Twilio-Signature` header value using constant-time
///    comparison.
///
/// Returns `true` if the signature is valid, `false` otherwise (including
/// when the header value is empty or the body cannot be decoded).
pub(super) fn verify_twilio_signature(
    auth_token: &str,
    url: &str,
    form_body: &[u8],
    signature: &str,
) -> bool {
    let signed = build_twilio_signed_string(url, form_body);
    let computed = hmac_sha1(auth_token.as_bytes(), signed.as_bytes());
    let computed_b64 = BASE64_STANDARD.encode(computed.as_slice());

    // Constant-time comparison to prevent timing attacks.
    computed_b64.as_bytes().ct_eq(signature.as_bytes()).into()
}

fn hmac_sha1(key: &[u8], message: &[u8]) -> [u8; 20] {
    let mut mac = HmacSha1::new_from_slice(key).expect("HMAC accepts any key size");
    mac.update(message);
    mac.finalize().into_bytes().into()
}

/// Build the string that Twilio signs for a POST webhook.
///
/// Format: `url` followed by each sorted POST parameter's `key + value`
/// (decoded, no delimiters between pairs).
fn build_twilio_signed_string(url: &str, form_body: &[u8]) -> String {
    // Parse form-encoded body into key-value pairs (already percent-decoded).
    let mut params: Vec<(String, String)> =
        serde_urlencoded::from_bytes(form_body).unwrap_or_default();
    // Sort alphabetically by key (Unix case-sensitive order).
    params.sort_by(|(a, _), (b, _)| a.cmp(b));

    let mut s = url.to_string();
    for (k, v) in &params {
        s.push_str(k);
        s.push_str(v);
    }
    s
}

// ── Tests ───────────────────────────────────────────────────────────────

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

    /// Compute a reference signature using the same algorithm.
    fn make_signature(secret: &str, body: &[u8]) -> String {
        let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
        mac.update(body);
        let result = mac.finalize().into_bytes();
        let mut hex = String::from("sha256=");
        for b in result.iter() {
            write!(hex, "{b:02x}").unwrap();
        }
        hex
    }

    #[test]
    fn valid_signature_passes() {
        let secret = "my-app-secret";
        let body = b"test webhook body";
        let sig = make_signature(secret, body);
        assert!(verify_meta_signature(secret, body, &sig));
    }

    #[test]
    fn invalid_signature_is_rejected() {
        let secret = "my-app-secret";
        let body = b"test webhook body";
        assert!(!verify_meta_signature(
            secret,
            body,
            "sha256=0000000000000000000000000000000000000000000000000000000000000000"
        ));
    }

    #[test]
    fn missing_header_prefix_is_rejected() {
        let secret = "my-app-secret";
        let body = b"test webhook body";
        // No "sha256=" prefix.
        assert!(!verify_meta_signature(secret, body, "badhash"));
    }

    #[test]
    fn empty_signature_is_rejected() {
        assert!(!verify_meta_signature("secret", b"body", ""));
    }

    #[test]
    fn wrong_secret_is_rejected() {
        let body = b"test webhook body";
        let sig = make_signature("correct-secret", body);
        assert!(!verify_meta_signature("wrong-secret", body, &sig));
    }

    // ── Twilio HMAC-SHA1 tests ─────────────────────────────────────────

    /// Compute a Twilio reference signature for tests.
    fn make_twilio_signature(auth_token: &str, url: &str, form_body: &[u8]) -> String {
        let signed = build_twilio_signed_string(url, form_body);
        let result = hmac_sha1(auth_token.as_bytes(), signed.as_bytes());
        BASE64_STANDARD.encode(result.as_slice())
    }

    const TEST_URL: &str = "https://example.com/webhook/whatsapp";
    const TEST_TOKEN: &str = "twilio-auth-token";

    #[test]
    fn twilio_valid_signature_passes() {
        let body = b"Body=hello+world&From=whatsapp%3A%2B15551234567";
        let sig = make_twilio_signature(TEST_TOKEN, TEST_URL, body);
        assert!(verify_twilio_signature(TEST_TOKEN, TEST_URL, body, &sig));
    }

    #[test]
    fn twilio_tampered_body_is_rejected() {
        let body = b"Body=hello+world&From=whatsapp%3A%2B15551234567";
        let sig = make_twilio_signature(TEST_TOKEN, TEST_URL, body);
        let tampered = b"Body=injected&From=whatsapp%3A%2B15551234567";
        assert!(!verify_twilio_signature(
            TEST_TOKEN, TEST_URL, tampered, &sig
        ));
    }

    #[test]
    fn twilio_missing_signature_is_rejected() {
        let body = b"Body=hello&From=whatsapp%3A%2B15551234567";
        // Empty string simulates a missing header.
        assert!(!verify_twilio_signature(TEST_TOKEN, TEST_URL, body, ""));
    }

    #[test]
    fn twilio_wrong_token_is_rejected() {
        let body = b"Body=hello&From=whatsapp%3A%2B15551234567";
        let sig = make_twilio_signature("correct-token", TEST_URL, body);
        assert!(!verify_twilio_signature(
            "wrong-token",
            TEST_URL,
            body,
            &sig
        ));
    }

    #[test]
    fn twilio_params_sorted_alphabetically() {
        // Body < From — order in the raw body should not affect the signed string.
        let body_bf = b"Body=hi&From=whatsapp%3A%2B1";
        let body_fb = b"From=whatsapp%3A%2B1&Body=hi";
        let sig = make_twilio_signature(TEST_TOKEN, TEST_URL, body_bf);
        // Both orderings of the same parameters must produce identical signatures.
        assert!(verify_twilio_signature(TEST_TOKEN, TEST_URL, body_fb, &sig));
    }
}
