//! SMTP 收信后的投递：解析、SPF、评分、入库、附件、容量、积分、推送、审计。

use crate::domain::entities::{email, email_attachment, mailbox};
use crate::infra::auth::spf;
use crate::repositories::{
    audit_repository::{AuditRepository, NewAuditLog},
    quota_repository::QuotaRepository,
};
use crate::services::{
    credit_rule_service::CreditRuleService, credit_service::CreditService, scoring_service,
};
use crate::smtp::parser;
use crate::AppState;
use anyhow::Result;
use sea_orm::{
    sea_query::Expr, ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, TransactionTrait,
};
use std::net::{IpAddr, Ipv4Addr};

/// 单次投递上下文（DATA 阶段使用）。
pub struct DeliveryContext {
    pub mailbox: mailbox::Model,
    pub peer_ip: String,
    pub helo: Option<String>,
    pub mail_from: String,
    pub rcpt_to: String,
}

/// 判断附件 MIME 是否危险（粗糙启发式；可扩展 ClamAV + 严格白名单）
fn is_bad_mime(content_type: &str) -> bool {
    let lower = content_type.to_lowercase();
    lower.contains("x-msdownload")
        || lower.contains("x-executable")
        || lower.contains("x-dosexec")
        || lower.ends_with("exe")
        || lower.contains("bat")
        || lower.contains("scr")
}

/// 提取发件域（小写）
fn sender_domain(addr: &str) -> Option<String> {
    let a = addr.trim_matches(|c| c == '<' || c == '>' || c == ' ');
    a.rsplit_once('@')
        .map(|(_, d)| d.to_lowercase())
        .filter(|d| !d.is_empty())
}

/// 解析、SPF、评分、落库、扣积分、推送、审计。
pub async fn deliver(state: &AppState, ctx: &DeliveryContext, raw: &[u8]) -> Result<()> {
    let parsed = parser::parse(raw)?;
    let size = raw.len() as i64;
    let now = chrono::Utc::now().naive_utc();
    let expires_at = now + chrono::Duration::days(state.config.smtp.email_ttl_days);

    // SPF / DKIM / DMARC 校验
    let peer_ip = ctx
        .peer_ip
        .parse::<IpAddr>()
        .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
    let spf_result =
        spf::check_spf(peer_ip, ctx.helo.as_deref().unwrap_or(""), &ctx.mail_from).await;
    let dkim_result: &str = if spf::detect_dkim_signature(raw) {
        "header_present"
    } else {
        "none"
    };
    let dmarc_result = match sender_domain(&ctx.mail_from) {
        Some(d) => spf::query_dmarc(spf::shared_resolver(), &d).await,
        None => "none",
    };

    // 滥用评分
    let has_bad_attachment = parsed
        .attachments
        .iter()
        .any(|a| is_bad_mime(&a.content_type));
    let abuse_score = scoring_service::compute(
        &state.config.smtp,
        scoring_service::ScoringInput {
            subject: parsed.subject.as_deref(),
            spf: spf_result,
            ptr_present: true, // pipeline onConnect 已做 PTR 硬门控
            has_bad_attachment,
        },
    );
    let quarantined = abuse_score >= state.config.smtp.quarantine_threshold;
    let status = if quarantined {
        "quarantined"
    } else {
        "received"
    };
    let credit_rule = CreditRuleService::new(state.pool.clone()).get().await?;

    // 正文加密（AES-GCM；开发环境未配置密钥时保留明文）。
    let enc_key = crate::utils::crypto::derive_key(&state.config.smtp.body_encryption_key);
    let text_body_stored = crate::utils::crypto::encrypt_optional(&enc_key, parsed.text_body)?;
    let html_body_stored = crate::utils::crypto::encrypt_optional(&enc_key, parsed.html_body)?;

    let active = email::ActiveModel {
        mailbox_id: Set(ctx.mailbox.id),
        user_id: Set(ctx.mailbox.user_id.clone()),
        recipient: Set(ctx.rcpt_to.clone()),
        mail_from: Set(ctx.mail_from.clone()),
        rcpt_to: Set(ctx.rcpt_to.clone()),
        sender_name: Set(parsed.sender_name),
        subject: Set(parsed.subject),
        text_body: Set(text_body_stored),
        html_body: Set(html_body_stored),
        source_ip: Set(ctx.peer_ip.clone()),
        helo: Set(ctx.helo.clone()),
        ptr_result: Set(None),
        spf_result: Set(Some(spf_result.as_str().to_string())),
        dkim_result: Set(Some(dkim_result.to_string())),
        dmarc_result: Set(Some(dmarc_result.to_string())),
        abuse_score: Set(abuse_score),
        status: Set(status.into()),
        size_bytes: Set(size),
        message_id: Set(parsed.message_id),
        expires_at: Set(expires_at),
        ..Default::default()
    };
    let txn = state.pool.begin().await?;
    let operation = async {
        let inserted = active.insert(&txn).await?;

        for attachment in parsed.attachments {
            email_attachment::ActiveModel {
                email_id: Set(inserted.id),
                filename: Set(attachment.filename),
                content_type: Set(attachment.content_type),
                size_bytes: Set(attachment.size),
                content: Set(Some(attachment.content)),
                ..Default::default()
            }
            .insert(&txn)
            .await?;
        }

        if size > ctx.mailbox.max_quota_bytes {
            anyhow::bail!("mailbox quota exceeded");
        }
        let remaining_before_insert = ctx.mailbox.max_quota_bytes - size;
        let mailbox_update = mailbox::Entity::update_many()
            .col_expr(
                mailbox::Column::UsedBytes,
                Expr::col(mailbox::Column::UsedBytes).add(size),
            )
            .col_expr(
                mailbox::Column::UpdatedAt,
                Expr::value(chrono::Utc::now().naive_utc()),
            )
            .filter(mailbox::Column::Id.eq(ctx.mailbox.id))
            .filter(mailbox::Column::Status.eq("active"))
            .filter(mailbox::Column::UsedBytes.lte(remaining_before_insert))
            .exec(&txn)
            .await?;
        if mailbox_update.rows_affected != 1 {
            anyhow::bail!("mailbox quota exceeded");
        }

        if !quarantined {
            QuotaRepository::reserve_email(
                &txn,
                &ctx.mailbox.user_id,
                size,
                credit_rule.daily_emails_limit,
            )
            .await?;
            CreditService::new(state.pool.clone())
                .debit_in_transaction(
                    &txn,
                    &ctx.mailbox.user_id,
                    credit_rule.receive_email_cost,
                    "receive_email",
                    Some(ctx.mailbox.id),
                    Some(inserted.id),
                )
                .await?;
        }

        Ok::<_, anyhow::Error>(inserted)
    }
    .await;

    let inserted = match operation {
        Ok(inserted) => {
            txn.commit().await?;
            inserted
        }
        Err(error) => {
            if let Err(rollback_error) = txn.rollback().await {
                tracing::error!(%rollback_error, "SMTP 投递事务回滚失败");
            }
            return Err(error);
        }
    };

    // 提交成功后再推送，避免客户端看到随后回滚的邮件。
    if !quarantined {
        // WebSocket 实时推送
        let payload = serde_json::json!({
            "event": "new_email",
            "data": {
                "id": inserted.id,
                "mail_from": inserted.mail_from,
                "sender_name": inserted.sender_name,
                "subject": inserted.subject,
                "size_bytes": inserted.size_bytes,
                "received_at": inserted.received_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
            }
        });
        state
            .mail_hub
            .notify(&ctx.mailbox.address, &payload.to_string());
    }

    // 审计日志
    let _ = AuditRepository::new(state.pool.clone())
        .add(NewAuditLog {
            event_type: "smtp_accept".into(),
            action: status.into(),
            source_ip: Some(ctx.peer_ip.clone()),
            helo: ctx.helo.clone(),
            mail_from: Some(ctx.mail_from.clone()),
            rcpt_to: Some(ctx.rcpt_to.clone()),
            reason: if quarantined {
                Some(format!("abuse_score={abuse_score}"))
            } else {
                None
            },
            operator_id: None,
            ..Default::default()
        })
        .await;

    // 滥用计数与自动封禁：域 10min / IP 1min 超阈值自动加临时黑名单
    if let Some(redis) = &state.redis_client {
        // 发件域计数（10 分钟窗口）
        if let Some(domain) = sender_domain(&ctx.mail_from) {
            let key = format!("smtp:counter:domain:{}", domain);
            let count = redis.incr_with_ttl(&key, 600).await.unwrap_or(0);
            let threshold = state.config.abuse.domain_emails_threshold_10min;
            if count == threshold {
                let abuse_svc = crate::services::abuse_service::AbuseService::new(
                    state.pool.clone(),
                    state.config.abuse.clone(),
                );
                let _ = abuse_svc
                    .auto_block_sender(&domain, &format!("exceeded {threshold} emails in 10min"))
                    .await;
                crate::workers::alert::notify(
                    &state.config.abuse.alert_webhook_url,
                    &format!("域名 {domain} 10 分钟内达 {threshold} 封，已自动封禁"),
                )
                .await;
            }
        }
        // 来源 IP 计数（1 分钟窗口）
        let ip_key = format!("smtp:counter:ip:{}", ctx.peer_ip);
        let ip_count = redis.incr_with_ttl(&ip_key, 60).await.unwrap_or(0);
        let ip_threshold = state.config.abuse.ip_connects_threshold_1min;
        if ip_count == ip_threshold {
            let abuse_svc = crate::services::abuse_service::AbuseService::new(
                state.pool.clone(),
                state.config.abuse.clone(),
            );
            let _ = abuse_svc
                .auto_block_ip(
                    &ctx.peer_ip,
                    &format!("exceeded {ip_threshold} emails in 1min"),
                )
                .await;
            crate::workers::alert::notify(
                &state.config.abuse.alert_webhook_url,
                &format!(
                    "IP {} 1 分钟内达 {} 封，已自动封禁",
                    ctx.peer_ip, ip_threshold
                ),
            )
            .await;
        }
    }

    tracing::info!(
        email_id = inserted.id,
        mailbox_id = inserted.mailbox_id,
        from = %inserted.mail_from,
        bytes = size,
        status,
        abuse_score,
        "邮件已接收"
    );
    Ok(())
}
