use mail_auth::dmarc::verify::DmarcParameters;
use mail_auth::dmarc::Policy;
use mail_auth::{
    AuthenticatedMessage, DkimOutput, DmarcResult as MailAuthDmarcResult, MessageAuthenticator,
    SpfOutput,
};
use serde::{Deserialize, Serialize};

use crate::error::{Result, SecurityError};

/// DMARC policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DmarcPolicy {
    None,
    Quarantine,
    Reject,
}

impl From<Policy> for DmarcPolicy {
    fn from(p: Policy) -> Self {
        match p {
            Policy::None | Policy::Unspecified => DmarcPolicy::None,
            Policy::Quarantine => DmarcPolicy::Quarantine,
            Policy::Reject => DmarcPolicy::Reject,
        }
    }
}

/// DMARC verification result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DmarcResult {
    /// Whether DMARC verification passed overall.
    pub passed: bool,
    /// The evaluated policy.
    pub policy: DmarcPolicy,
    /// The domain that was checked.
    pub domain: String,
    /// DKIM alignment result: "pass", "fail", etc.
    pub dkim_result: String,
    /// SPF alignment result: "pass", "fail", etc.
    pub spf_result: String,
    /// Recommended action: "pass", "quarantine", "reject".
    pub action: String,
    /// Human-readable details.
    pub details: Option<String>,
}

/// Check DMARC for an email, given prior DKIM and SPF results.
///
/// * `raw_message` - The raw RFC 5322 message bytes
/// * `dkim_output` - DKIM verification results from `verify_dkim`
/// * `spf_output` - SPF verification output from `check_spf`
/// * `mail_from_domain` - The MAIL FROM domain (RFC 5321)
/// * `authenticator` - The MessageAuthenticator for DNS lookups
pub async fn check_dmarc<'x>(
    raw_message: &'x [u8],
    dkim_output: &'x [DkimOutput<'x>],
    spf_output: &'x SpfOutput,
    mail_from_domain: &'x str,
    authenticator: &MessageAuthenticator,
) -> Result<DmarcResult> {
    let message = AuthenticatedMessage::parse(raw_message)
        .ok_or_else(|| SecurityError::Parse("Failed to parse email for DMARC check".into()))?;

    let dmarc_output = authenticator
        .verify_dmarc(
            DmarcParameters::new(&message, dkim_output, mail_from_domain, spf_output)
                .with_domain_suffix_fn(|domain| psl::domain_str(domain).unwrap_or(domain)),
        )
        .await;

    let policy = DmarcPolicy::from(dmarc_output.policy());
    let domain = dmarc_output.domain().to_string();

    let dkim_result_str = dmarc_result_to_string(dmarc_output.dkim_result());
    let spf_result_str = dmarc_result_to_string(dmarc_output.spf_result());

    let dkim_passed = matches!(dmarc_output.dkim_result(), MailAuthDmarcResult::Pass);
    let spf_passed = matches!(dmarc_output.spf_result(), MailAuthDmarcResult::Pass);
    let passed = dkim_passed || spf_passed;

    let action = if passed {
        "pass".to_string()
    } else {
        match policy {
            DmarcPolicy::None => "pass".to_string(), // p=none means monitor only
            DmarcPolicy::Quarantine => "quarantine".to_string(),
            DmarcPolicy::Reject => "reject".to_string(),
        }
    };

    Ok(DmarcResult {
        passed,
        policy,
        domain,
        dkim_result: dkim_result_str,
        spf_result: spf_result_str,
        action,
        details: None,
    })
}

fn dmarc_result_to_string(result: &MailAuthDmarcResult) -> String {
    match result {
        MailAuthDmarcResult::Pass => "pass".to_string(),
        MailAuthDmarcResult::Fail(err) => format!("fail: {}", err),
        MailAuthDmarcResult::TempError(err) => format!("temperror: {}", err),
        MailAuthDmarcResult::PermError(err) => format!("permerror: {}", err),
        MailAuthDmarcResult::None => "none".to_string(),
    }
}

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

    #[test]
    fn test_dmarc_policy_from() {
        assert_eq!(DmarcPolicy::from(Policy::None), DmarcPolicy::None);
        assert_eq!(
            DmarcPolicy::from(Policy::Quarantine),
            DmarcPolicy::Quarantine
        );
        assert_eq!(DmarcPolicy::from(Policy::Reject), DmarcPolicy::Reject);
    }
}
