//! RFC 9421 HTTP Message Signatures verification.
//!
//! Verifies incoming requests signed according to RFC 9421, as used by
//! Mastodon 5.x and other modern ActivityPub implementations.

mod sf;

use crate::activitypub::keys;
use crate::activitypub::signatures::SignatureError;
use actix_web::http::header::HeaderMap;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use chrono::Utc;
use sf::{SfParam, SfValue};
use sha2::{Digest, Sha256};

/// Verify an inbound request's signature using RFC 9421 HTTP Message Signatures.
///
/// Returns the verified actor URL on success.
///
/// # Arguments
/// * `scheme` - Request scheme (http or https)
/// * `authority` - Request authority (host:port)
/// * `path` - Request path without query
/// * `query` - Optional query string (without leading ?)
/// * `method` - HTTP method (POST)
/// * `headers` - Request headers
/// * `body` - Request body
/// * `fetch_key` - Callback to fetch the public key for a keyId
///
/// # Verification rules
/// 1. Signature-Input header present and parseable
/// 2. Signature header present and parseable
/// 3. Exactly one signature label present in both headers
/// 4. Parameters extracted: keyid (required), created (required), alg (optional)
/// 5. Algorithm must be rsa-v1_5-sha256 (only RSA keys supported)
/// 6. Created timestamp within 300 seconds of now
/// 7. Expires timestamp (if present) must not be in the past
/// 8. Covered components must include @method and content integrity
/// 9. Signature base reconstructed and verified
/// 10. Public key fetched and signature verified
/// 11. Return verified actor URL
#[allow(clippy::too_many_arguments)]
pub async fn verify_request<F, Fut>(
    scheme: &str,
    authority: &str,
    path: &str,
    query: Option<&str>,
    method: &str,
    headers: &HeaderMap,
    body: &[u8],
    fetch_key: F,
) -> Result<String, SignatureError>
where
    F: FnOnce(String) -> Fut,
    Fut: std::future::Future<Output = Result<Option<(String, String)>, SignatureError>>,
{
    // 1. Parse Signature-Input header (RFC 8941 dictionary)
    let signature_input = headers
        .get("signature-input")
        .and_then(|v| v.to_str().ok())
        .ok_or(SignatureError::MissingSignature)?;

    tracing::debug!(
        signature_input = %signature_input,
        "parsing RFC 9421 signature-input"
    );

    let input_dict = sf::parse_dictionary(signature_input)
        .map_err(|e| SignatureError::InvalidKeyId(format!("invalid signature-input: {}", e)))?;

    // 2. Parse Signature header
    let signature_header = headers
        .get("signature")
        .and_then(|v| v.to_str().ok())
        .ok_or(SignatureError::MissingSignature)?;

    let sig_dict =
        sf::parse_dictionary(signature_header).map_err(|_e| SignatureError::MissingSignature)?;

    // 3. Mastodon only sends one signature at a time; find matching label
    if input_dict.len() != 1 {
        return Err(SignatureError::InvalidKeyId(
            "expected exactly one signature label".to_string(),
        ));
    }

    let (label, input_member) = &input_dict[0];
    let sig_member =
        sig_dict
            .iter()
            .find(|(k, _)| k == label)
            .ok_or(SignatureError::InvalidKeyId(format!(
                "signature label '{}' not found",
                label
            )))?;

    // 4. Extract parameters from signature-input
    let mut key_id: Option<String> = None;
    let mut created: Option<i64> = None;
    let mut expires: Option<i64> = None;
    let mut alg: Option<String> = None;

    for (param_name, param_value) in &input_member.params {
        match param_name.as_str() {
            "keyid" => {
                if let SfParam::String(k) = param_value {
                    key_id = Some(k.clone());
                }
            }
            "created" => {
                if let SfParam::Integer(c) = param_value {
                    created = Some(*c);
                }
            }
            "expires" => {
                if let SfParam::Integer(e) = param_value {
                    expires = Some(*e);
                }
            }
            "alg" => match param_value {
                SfParam::Token(a) | SfParam::String(a) => {
                    alg = Some(a.clone());
                }
                _ => {}
            },
            _ => {} // Ignore other parameters (nonce, tag, etc.)
        }
    }

    let key_id = key_id
        .ok_or_else(|| SignatureError::InvalidKeyId("missing keyid parameter".to_string()))?;
    let created = created.ok_or(SignatureError::DateOutOfRange)?;

    // 5. Check algorithm - only RSA-PKCS1-v1_5 with SHA-256 supported
    if let Some(a) = &alg
        && a != "rsa-v1_5-sha256"
    {
        return Err(SignatureError::InvalidAlgorithm(a.clone()));
    }

    // 6. Check freshness - created must be within 300 seconds of now
    let now = Utc::now().timestamp();
    let diff = (now - created).abs();
    if diff > 300 {
        return Err(SignatureError::DateOutOfRange);
    }

    // 7. Check expires if present
    if let Some(exp) = expires
        && now > exp
    {
        return Err(SignatureError::DateOutOfRange);
    }

    // 8. Extract covered components from inner list
    let covered_components = match &input_member.value {
        SfValue::InnerList(items) => items,
        _ => {
            return Err(SignatureError::InvalidKeyId(
                "signature-input value must be an inner list".to_string(),
            ));
        }
    };

    // Verify policy: must cover @method and content integrity
    let has_method = covered_components
        .iter()
        .any(|item| matches!(&item.value, SfValue::String(s) if s == "@method"));
    let has_content = covered_components.iter().any(
        |item| matches!(&item.value, SfValue::String(s) if s == "content-digest" || s == "digest"),
    );

    if !has_method {
        return Err(SignatureError::InvalidKeyId(
            "covered components must include @method".to_string(),
        ));
    }
    if !has_content {
        return Err(SignatureError::InvalidKeyId(
            "covered components must include content-digest or digest".to_string(),
        ));
    }

    // 9. Build signature base
    let mut signature_base_lines = Vec::new();

    for item in covered_components {
        let component_name = match &item.value {
            SfValue::String(s) => s.as_str(),
            _ => {
                return Err(SignatureError::InvalidKeyId(
                    "covered components must be strings".to_string(),
                ));
            }
        };

        let line = build_component_line(
            component_name,
            scheme,
            authority,
            path,
            query,
            method,
            headers,
            body,
        )?;
        signature_base_lines.push(line);
    }

    // 10. Add @signature-params line (the raw inner list + parameters)
    signature_base_lines.push(format!("\"@signature-params\": {}", input_member.raw_text));

    let signature_base = signature_base_lines.join("\n");

    tracing::debug!(
        signature_base = %signature_base,
        "RFC 9421 signature base constructed"
    );

    // 11. Fetch the public key
    tracing::debug!(key_id = %key_id, "fetching public key for RFC 9421 verification");
    let (actor_url, public_key_pem) = fetch_key(key_id.clone())
        .await?
        .ok_or_else(|| SignatureError::KeyNotFound(key_id.clone()))?;

    tracing::debug!(actor_url = %actor_url, "public key fetched successfully");

    // 12. Decode signature and verify
    let signature_bytes = match &sig_member.1.value {
        SfValue::ByteSeq(bytes) => bytes.clone(),
        _ => {
            return Err(SignatureError::VerificationFailed);
        }
    };

    if !keys::verify(&public_key_pem, signature_base.as_bytes(), &signature_bytes) {
        return Err(SignatureError::VerificationFailed);
    }

    tracing::debug!(actor_url = %actor_url, "RFC 9421 signature verified successfully");
    Ok(actor_url)
}

/// Build a signature base line for a covered component.
#[allow(clippy::too_many_arguments)]
fn build_component_line(
    component_name: &str,
    scheme: &str,
    authority: &str,
    path: &str,
    query: Option<&str>,
    method: &str,
    headers: &HeaderMap,
    body: &[u8],
) -> Result<String, SignatureError> {
    match component_name {
        "@method" => Ok(format!("\"@method\": {}", method)),
        "@authority" => Ok(format!("\"@authority\": {}", authority)),
        "@scheme" => Ok(format!("\"@scheme\": {}", scheme)),
        "@path" => Ok(format!("\"@path\": {}", path)),
        "@query" => {
            let q = query.unwrap_or("");
            Ok(format!("\"@query\": {}", q))
        }
        "@target-uri" => {
            let target_uri = if let Some(q) = query {
                format!("{}://{}{}?{}", scheme, authority, path, q)
            } else {
                format!("{}://{}{}", scheme, authority, path)
            };
            Ok(format!("\"@target-uri\": {}", target_uri))
        }
        "content-digest" => {
            let digest = headers
                .get("content-digest")
                .and_then(|v| v.to_str().ok())
                .ok_or(SignatureError::MissingHeader("content-digest".to_string()))?;

            // Verify the digest matches the body
            let expected = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));
            if digest != expected {
                return Err(SignatureError::DigestMismatch);
            }

            Ok(format!("\"content-digest\": {}", digest))
        }
        "digest" => {
            // Legacy digest format
            let digest = headers
                .get("digest")
                .and_then(|v| v.to_str().ok())
                .ok_or(SignatureError::MissingHeader("digest".to_string()))?;

            // Verify the digest matches the body
            let expected = format!("SHA-256={}", BASE64.encode(Sha256::digest(body)));
            if digest != expected {
                return Err(SignatureError::DigestMismatch);
            }

            Ok(format!("\"digest\": {}", digest))
        }
        // Regular header field
        name if !name.starts_with('@') => {
            let value = headers
                .get(name)
                .and_then(|v| v.to_str().ok())
                .ok_or(SignatureError::MissingHeader(name.to_string()))?;

            Ok(format!("\"{}\": {}", name, value))
        }
        _ => Err(SignatureError::InvalidKeyId(format!(
            "unsupported component: {}",
            component_name
        ))),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::activitypub::keys;
    use actix_web::http::header::{HeaderMap, HeaderName, HeaderValue};

    #[tokio::test]
    async fn test_rfc9421_round_trip() {
        let (private_pem, public_pem) = keys::generate().unwrap();
        let key_id = "https://example.com/actor#main-key";
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let body = b"test body";

        // Create headers
        let mut headers = HeaderMap::new();
        let now = Utc::now().timestamp();
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        // Build signature base
        let signature_input = format!(
            "sig1=(\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            now, key_id
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        // Build signature
        let signature_base = format!(
            "\"@method\": POST\n\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            content_digest, now, key_id
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        // Verify
        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            body,
            |kid| async move {
                if kid == key_id {
                    Ok(Some((key_id.to_string(), public_pem)))
                } else {
                    Ok(None)
                }
            },
        )
        .await;

        assert!(result.is_ok(), "verification should succeed: {:?}", result);
        assert_eq!(result.unwrap(), key_id);
    }

    #[tokio::test]
    async fn test_rfc9421_tampered_body_rejected() {
        let (private_pem, public_pem) = keys::generate().unwrap();
        let key_id = "https://example.com/actor#main-key";
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let original_body = b"original body";
        let tampered_body = b"tampered body";

        let mut headers = HeaderMap::new();
        let now = Utc::now().timestamp();
        // Digest of original body
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(original_body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        let signature_input = format!(
            "sig1=(\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            now, key_id
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        let signature_base = format!(
            "\"@method\": POST\n\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            content_digest, now, key_id
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        // Verify with tampered body - should fail with DigestMismatch
        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            tampered_body,
            |kid| async move {
                if kid == key_id {
                    Ok(Some((key_id.to_string(), public_pem)))
                } else {
                    Ok(None)
                }
            },
        )
        .await;

        assert!(result.is_err(), "tampered body should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            SignatureError::DigestMismatch
        ));
    }

    #[tokio::test]
    async fn test_rfc9421_missing_keyid_rejected() {
        let (private_pem, _public_pem) = keys::generate().unwrap();
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let body = b"test body";

        let mut headers = HeaderMap::new();
        let now = Utc::now().timestamp();
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        // Missing keyid parameter
        let signature_input = format!(
            "sig1=(\"@method\" \"@target-uri\" \"content-digest\");created={};alg=\"rsa-v1_5-sha256\"",
            now
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        let signature_base = format!(
            "\"@method\": POST\n\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@method\" \"@target-uri\" \"content-digest\");created={};alg=\"rsa-v1_5-sha256\"",
            content_digest, now
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            body,
            |_kid| async move { Ok(None) },
        )
        .await;

        assert!(result.is_err(), "missing keyid should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            SignatureError::InvalidKeyId(_)
        ));
    }

    #[tokio::test]
    async fn test_rfc9421_bad_algorithm_rejected() {
        let (private_pem, public_pem) = keys::generate().unwrap();
        let key_id = "https://example.com/actor#main-key";
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let body = b"test body";

        let mut headers = HeaderMap::new();
        let now = Utc::now().timestamp();
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        // Wrong algorithm
        let signature_input = format!(
            "sig1=(\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"ecdsa-p256-sha256\"",
            now, key_id
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        let signature_base = format!(
            "\"@method\": POST\n\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"ecdsa-p256-sha256\"",
            content_digest, now, key_id
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            body,
            |kid| async move {
                if kid == key_id {
                    Ok(Some((key_id.to_string(), public_pem)))
                } else {
                    Ok(None)
                }
            },
        )
        .await;

        assert!(result.is_err(), "bad algorithm should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            SignatureError::InvalidAlgorithm(_)
        ));
    }

    #[tokio::test]
    async fn test_rfc9421_expired_created_rejected() {
        let (private_pem, public_pem) = keys::generate().unwrap();
        let key_id = "https://example.com/actor#main-key";
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let body = b"test body";

        let mut headers = HeaderMap::new();
        // Created 600 seconds ago (beyond 300s window)
        let now = Utc::now().timestamp() - 600;
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        let signature_input = format!(
            "sig1=(\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            now, key_id
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        let signature_base = format!(
            "\"@method\": POST\n\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@method\" \"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            content_digest, now, key_id
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            body,
            |kid| async move {
                if kid == key_id {
                    Ok(Some((key_id.to_string(), public_pem)))
                } else {
                    Ok(None)
                }
            },
        )
        .await;

        assert!(result.is_err(), "expired created should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            SignatureError::DateOutOfRange
        ));
    }

    #[tokio::test]
    async fn test_rfc9421_missing_method_component_rejected() {
        let (private_pem, public_pem) = keys::generate().unwrap();
        let key_id = "https://example.com/actor#main-key";
        let method = "POST";
        let path = "/inbox";
        let authority = "example.com";
        let scheme = "https";
        let body = b"test body";

        let mut headers = HeaderMap::new();
        let now = Utc::now().timestamp();
        let content_digest = format!("sha-256=:{}:", BASE64.encode(Sha256::digest(body)));

        headers.insert(
            HeaderName::from_static("content-digest"),
            HeaderValue::from_str(&content_digest).unwrap(),
        );

        // Missing @method in covered components
        let signature_input = format!(
            "sig1=(\"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            now, key_id
        );
        headers.insert(
            HeaderName::from_static("signature-input"),
            HeaderValue::from_str(&signature_input).unwrap(),
        );

        let signature_base = format!(
            "\"@target-uri\": https://example.com/inbox\n\"content-digest\": {}\n\"@signature-params\": (\"@target-uri\" \"content-digest\");created={};keyid=\"{}\";alg=\"rsa-v1_5-sha256\"",
            content_digest, now, key_id
        );

        let signature_bytes = keys::sign(&private_pem, signature_base.as_bytes()).unwrap();
        let signature = format!("sig1=:{}:", BASE64.encode(signature_bytes));
        headers.insert(
            HeaderName::from_static("signature"),
            HeaderValue::from_str(&signature).unwrap(),
        );

        let result = verify_request(
            scheme,
            authority,
            path,
            None,
            method,
            &headers,
            body,
            |kid| async move {
                if kid == key_id {
                    Ok(Some((key_id.to_string(), public_pem)))
                } else {
                    Ok(None)
                }
            },
        )
        .await;

        assert!(result.is_err(), "missing @method should be rejected");
        assert!(matches!(
            result.unwrap_err(),
            SignatureError::InvalidKeyId(_)
        ));
    }
}
