//! SMTP 多层校验流水线（防滥用核心）。
//!
//! 覆盖 onConnect、onMailFrom 与 onRcptTo，包含连接限流、PTR、黑名单、
//! 域名/地址校验、蜜罐、配额、积分余额和灰名单。
//!
//! 限流默认 fail-open（Redis 不可用时放行频率限制，其他 DB 校验仍生效）。

use crate::domain::entities::mailbox;
use crate::repositories::mailbox_repository::MailboxRepository;
use crate::AppState;
use hickory_resolver::TokioAsyncResolver;
use std::net::IpAddr;

/// onConnect / onMailFrom 的校验结论。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
    Accept,
    /// 450 临时退避（灰名单/频率限制）
    Defer,
    /// 421 临时失败断连（IP 限流/熔断）
    TempFail,
    /// 550 永久拒绝（黑名单/PTR/不存在）
    Reject,
    /// 452 配额/额度超限
    OverQuota,
}

impl Verdict {
    pub fn smtp_code(&self) -> u16 {
        match self {
            Verdict::Accept => 250,
            Verdict::Defer => 450,
            Verdict::TempFail => 421,
            Verdict::Reject => 550,
            Verdict::OverQuota => 452,
        }
    }

    pub fn message(&self) -> &'static str {
        match self {
            Verdict::Accept => "OK",
            Verdict::Defer => "4.7.1 greylisted / rate limited, try again later",
            Verdict::TempFail => "4.7.0 too many connections, try again later",
            Verdict::Reject => "5.7.1 relay denied / policy violation",
            Verdict::OverQuota => "4.2.2 mailbox quota exceeded",
        }
    }
}

/// onRcptTo 的校验结论（Accept 时携带解析出的邮箱模型，供 DATA 入库用）。
pub enum RcptVerdict {
    Accept(Box<mailbox::Model>),
    /// 命中蜜罐：假装接受，DATA 时丢弃并封禁来源
    Honeypot,
    Defer,
    Reject,
    OverQuota,
}

impl RcptVerdict {
    pub fn as_simple(&self) -> Verdict {
        match self {
            RcptVerdict::Accept(_) | RcptVerdict::Honeypot => Verdict::Accept,
            RcptVerdict::Defer => Verdict::Defer,
            RcptVerdict::Reject => Verdict::Reject,
            RcptVerdict::OverQuota => Verdict::OverQuota,
        }
    }
}

/// 从 `<a@b>` 或 `a@b` 中提取域名（小写）。
fn extract_domain(addr: &str) -> Option<String> {
    let a = addr.trim_matches(|c| c == '<' || c == '>' || c == ' ');
    if a.is_empty() {
        return None;
    }
    a.rsplit_once('@')
        .map(|(_, d)| d.to_lowercase())
        .filter(|d| !d.is_empty())
}

fn is_private(ip: IpAddr) -> bool {
    match ip {
        IpAddr::V4(v4) => v4.is_private() || v4.is_link_local(),
        IpAddr::V6(v6) => v6.is_loopback(),
    }
}

static RESOLVER: std::sync::OnceLock<TokioAsyncResolver> = std::sync::OnceLock::new();

/// PTR 反查：本地/私有地址直接放行；无 PTR 记录返回 false。
async fn has_valid_ptr(ip: IpAddr) -> bool {
    if ip.is_loopback() || is_private(ip) {
        return true;
    }
    let resolver = match RESOLVER.get() {
        Some(r) => r,
        None => match TokioAsyncResolver::tokio_from_system_conf() {
            Ok(r) => {
                let _ = RESOLVER.set(r);
                RESOLVER.get().expect("resolver just initialized")
            }
            Err(error) => {
                tracing::warn!(%error, "DNS resolver 初始化失败，PTR 校验放行");
                return true; // DNS 不可用时放行，避免误伤
            }
        },
    };
    match resolver.reverse_lookup(ip).await {
        Ok(lookup) => lookup.into_iter().next().is_some(),
        Err(_) => false,
    }
}

/// onConnect：IP 连接限流 + PTR 校验。
pub async fn check_connect(state: &AppState, peer_ip: IpAddr) -> Verdict {
    let cfg = &state.config.smtp;
    // IP 黑名单
    let blacklist =
        crate::repositories::blacklist_repository::BlacklistRepository::new(state.pool.clone());
    if let Ok(Some(_)) = blacklist.find_active_ip(&peer_ip.to_string()).await {
        return Verdict::Reject;
    }
    // IP 连接频率（Redis 滑动窗口）
    if let Some(redis) = &state.redis_client {
        let key = format!("smtp:rl:ip:{}", peer_ip);
        match redis
            .sliding_window_allow(&key, cfg.ip_connect_per_min, 60_000, 120)
            .await
        {
            Ok(false) => return Verdict::TempFail,
            Err(e) => tracing::warn!(%e, "IP 限流查询失败，fail-open 放行"),
            Ok(true) => {}
        }
    }
    // PTR 反查
    if cfg.require_ptr && !has_valid_ptr(peer_ip).await {
        return Verdict::Reject;
    }
    Verdict::Accept
}

/// onMailFrom：发件域黑名单 + 频率限制。
pub async fn check_mail_from(state: &AppState, mail_from: &str) -> Verdict {
    let cfg = &state.config.smtp;
    let domain = extract_domain(mail_from);
    // 发件域黑名单
    if let Some(d) = &domain {
        let blacklist =
            crate::repositories::blacklist_repository::BlacklistRepository::new(state.pool.clone());
        if let Ok(Some(_)) = blacklist.find_active_sender("domain", d).await {
            return Verdict::Reject;
        }
    }
    // 发件域频率
    if let (Some(redis), Some(d)) = (&state.redis_client, domain) {
        let key = format!("smtp:rl:domain:{}", d);
        match redis
            .sliding_window_allow(&key, cfg.domain_per_min, 60_000, 120)
            .await
        {
            Ok(false) => return Verdict::Defer,
            Err(e) => tracing::warn!(%e, "发件域限流查询失败，fail-open 放行"),
            Ok(true) => {}
        }
    }
    Verdict::Accept
}

/// onRcptTo：域名白名单 + 地址存在性 + 收件地址限流 + 配额。
/// 命中即返回，避免无谓的 DB 查询。
pub async fn check_rcpt_to(
    state: &AppState,
    peer_ip: IpAddr,
    mail_from: &str,
    rcpt: &str,
) -> RcptVerdict {
    let cfg = &state.config.smtp;
    let rcpt_addr = rcpt
        .trim_matches(|c| c == '<' || c == '>' || c == ' ')
        .to_lowercase();

    // 1. 收件域名白名单
    let domain = match rcpt_addr.rsplit_once('@') {
        Some((_, d)) => d,
        None => return RcptVerdict::Reject,
    };
    if !cfg
        .local_domains
        .iter()
        .any(|d| d.eq_ignore_ascii_case(domain))
    {
        return RcptVerdict::Reject; // 非 shenjianl.cn：550 relay denied
    }

    // 1.5 蜜罐检测：命中则假装接受，DATA 时封禁来源
    if state
        .config
        .abuse
        .honeypot_addresses
        .iter()
        .any(|h| h.eq_ignore_ascii_case(&rcpt_addr))
    {
        return RcptVerdict::Honeypot;
    }

    // 2. 地址必须存在且 active 且未过期
    let mailbox_repo = MailboxRepository::new(state.pool.clone());
    let mailbox = match mailbox_repo.find_active_by_address(&rcpt_addr).await {
        Ok(Some(m)) => m,
        Ok(None) => return RcptVerdict::Reject, // 不存在/已过期：550
        Err(e) => {
            tracing::error!(%e, "查询收件地址失败");
            return RcptVerdict::Defer; // DB 异常：450 让对方重试
        }
    };

    // 3. 收件地址频率限制
    if let Some(redis) = &state.redis_client {
        let key = format!("smtp:rl:rcpt:{}", rcpt_addr);
        match redis
            .sliding_window_allow(&key, cfg.rcpt_per_min, 60_000, 120)
            .await
        {
            Ok(false) => return RcptVerdict::Defer,
            Err(e) => tracing::warn!(%e, "收件地址限流查询失败，fail-open 放行"),
            Ok(true) => {}
        }
    }

    // 4. 单邮箱容量配额
    if mailbox.used_bytes >= mailbox.max_quota_bytes {
        return RcptVerdict::OverQuota;
    }

    // 5. 灰名单 Greylisting。每日额度与积分在 DATA 最终事务中权威校验，
    // 这样隔离邮件不会消耗用户积分或每日次数。
    {
        let greylist = crate::services::greylist_service::GreylistService::new(
            state.redis_client.clone(),
            state.config.smtp.clone(),
        );
        if matches!(
            greylist
                .evaluate(&peer_ip.to_string(), mail_from, &rcpt_addr)
                .await,
            crate::services::greylist_service::GreyResult::Defer
        ) {
            return RcptVerdict::Defer;
        }
    }

    // 蜜罐地址：命中后记录事件并拒绝投递。
    RcptVerdict::Accept(Box::new(mailbox))
}

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

    #[test]
    fn extract_domain_handles_common_formats() {
        assert_eq!(extract_domain("<a@b.com>"), Some("b.com".into()));
        assert_eq!(extract_domain("a@b.com"), Some("b.com".into()));
        assert_eq!(extract_domain("a@B.COM"), Some("b.com".into()));
        assert_eq!(extract_domain(""), None);
        assert_eq!(extract_domain("<noemail>"), None);
    }
}
