//! 管理后台运维治理接口：服务端分页、批量处置、受控内容查看与审计。

use crate::domain::entities::{
    audit_log, blocked_ip, blocked_sender, credit_account, credit_rule_change, credit_transaction,
    email, email_attachment, email_logs, mailbox, users,
};
use crate::domain::vo::ApiResponse;
use crate::error::ErrorResponse;
use crate::infra::middleware::UserId;
use crate::repositories::{
    attachment_repository::AttachmentRepository,
    audit_repository::{AuditRepository, NewAuditLog},
    blacklist_repository::BlacklistRepository,
    quota_repository::QuotaRepository,
};
use crate::services::{credit_rule_service::CreditRuleService, credit_service::CreditService};
use crate::AppState;
use axum::{
    extract::{Path, Query, State},
    http::{HeaderValue, StatusCode},
    response::{IntoResponse, Response},
    Json,
};
use chrono::{NaiveDateTime, TimeZone, Utc};
use sea_orm::{
    sea_query::Expr, ActiveModelTrait, ColumnTrait, Condition, EntityTrait, PaginatorTrait,
    QueryFilter, QueryOrder, QuerySelect, Set, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::{BTreeMap, HashMap, HashSet};

const DEFAULT_PAGE_SIZE: u64 = 20;
const MAX_PAGE_SIZE: u64 = 100;
const MAX_BULK_ITEMS: usize = 100;

#[derive(Debug, Serialize)]
pub struct PageData<T> {
    pub items: Vec<T>,
    pub page: u64,
    pub page_size: u64,
    pub total: u64,
    pub total_pages: u64,
}

fn page_data<T>(items: Vec<T>, page: u64, page_size: u64, total: u64) -> PageData<T> {
    PageData {
        items,
        page,
        page_size,
        total,
        total_pages: total.div_ceil(page_size),
    }
}

fn paging(page: Option<u64>, page_size: Option<u64>) -> (u64, u64) {
    (
        page.unwrap_or(1).max(1),
        page_size
            .unwrap_or(DEFAULT_PAGE_SIZE)
            .clamp(1, MAX_PAGE_SIZE),
    )
}

fn parse_datetime(value: Option<&str>) -> Result<Option<NaiveDateTime>, ErrorResponse> {
    value
        .map(|raw| {
            chrono::DateTime::parse_from_rfc3339(raw)
                .map(|date| date.with_timezone(&Utc).naive_utc())
                .map_err(|_| ErrorResponse::bad_request("时间格式必须为 RFC3339"))
        })
        .transpose()
}

fn validate_reason(reason: &str) -> Result<String, ErrorResponse> {
    let reason = reason.trim();
    if reason.is_empty() {
        return Err(ErrorResponse::bad_request("操作原因不能为空"));
    }
    if reason.chars().count() > 200 {
        return Err(ErrorResponse::bad_request("操作原因不能超过 200 字"));
    }
    Ok(reason.to_owned())
}

fn unique_ids<T>(ids: Vec<T>) -> Result<Vec<T>, ErrorResponse>
where
    T: Eq + std::hash::Hash + Clone,
{
    if ids.is_empty() {
        return Err(ErrorResponse::bad_request("至少选择一个目标"));
    }
    let mut seen = HashSet::new();
    let ids: Vec<_> = ids
        .into_iter()
        .filter(|id| seen.insert(id.clone()))
        .collect();
    if ids.len() > MAX_BULK_ITEMS {
        return Err(ErrorResponse::bad_request("单次最多操作 100 项"));
    }
    Ok(ids)
}

fn iso(value: NaiveDateTime) -> String {
    value.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
}

#[allow(clippy::too_many_arguments)]
pub(crate) async fn admin_audit(
    state: &AppState,
    operator_id: &str,
    event_type: &str,
    action: &str,
    reason: Option<&str>,
    target_type: &str,
    target_id: impl ToString,
    metadata: Option<Value>,
) -> anyhow::Result<()> {
    AuditRepository::new(state.pool.clone())
        .add(NewAuditLog {
            event_type: event_type.into(),
            action: action.into(),
            reason: reason.map(str::to_owned),
            operator_id: Some(operator_id.into()),
            target_type: Some(target_type.into()),
            target_id: Some(target_id.to_string()),
            metadata_json: metadata.map(|value| value.to_string()),
            ..Default::default()
        })
        .await
}

#[derive(Debug, Serialize)]
pub struct BulkFailure<T> {
    pub id: T,
    pub message: String,
}

#[derive(Debug, Serialize)]
pub struct BulkResult<T> {
    pub batch_id: String,
    pub requested: usize,
    pub succeeded: Vec<T>,
    pub failed: Vec<BulkFailure<T>>,
}

// ===== 用户 =====

#[derive(Debug, Deserialize)]
pub struct UsersQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub q: Option<String>,
    pub role: Option<String>,
    pub status: Option<String>,
    pub created_from: Option<String>,
    pub created_to: Option<String>,
    pub sort: Option<String>,
    pub order: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AdminUserItem {
    pub id: String,
    pub email: String,
    pub role: String,
    pub status: String,
    pub is_current: bool,
    pub mailbox_count: u64,
    pub email_count: u64,
    pub credit_balance: i64,
    pub credit_total_granted: i64,
    pub credit_total_consumed: i64,
    pub created_at: String,
}

pub async fn list_users(
    State(state): State<AppState>,
    UserId(current_user_id): UserId,
    Query(query): Query<UsersQuery>,
) -> Result<Json<ApiResponse<PageData<AdminUserItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = users::Entity::find().filter(users::Column::DeletedAt.is_null());
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(users::Column::Email.contains(q))
                .add(users::Column::Id.contains(q)),
        );
    }
    if let Some(role) = query.role.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(users::Column::Role.eq(role));
    }
    if let Some(status) = query.status.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(users::Column::Status.eq(status));
    }
    if let Some(from) = parse_datetime(query.created_from.as_deref())? {
        find = find.filter(users::Column::CreatedAt.gte(from));
    }
    if let Some(to) = parse_datetime(query.created_to.as_deref())? {
        find = find.filter(users::Column::CreatedAt.lte(to));
    }
    let desc = query.order.as_deref() != Some("asc");
    find = match query.sort.as_deref() {
        Some("email") if desc => find.order_by_desc(users::Column::Email),
        Some("email") => find.order_by_asc(users::Column::Email),
        Some("status") if desc => find.order_by_desc(users::Column::Status),
        Some("status") => find.order_by_asc(users::Column::Status),
        _ if desc => find.order_by_desc(users::Column::CreatedAt),
        _ => find.order_by_asc(users::Column::CreatedAt),
    };
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let ids: Vec<_> = models.iter().map(|user| user.id.clone()).collect();

    let accounts = if ids.is_empty() {
        Vec::new()
    } else {
        credit_account::Entity::find()
            .filter(credit_account::Column::UserId.is_in(ids.clone()))
            .all(&state.pool)
            .await
            .map_err(internal)?
    };
    let account_map: HashMap<_, _> = accounts
        .into_iter()
        .map(|account| (account.user_id.clone(), account))
        .collect();
    let mut mailbox_counts = HashMap::<String, u64>::new();
    let mut email_counts = HashMap::<String, u64>::new();
    if !ids.is_empty() {
        for (user_id, count) in mailbox::Entity::find()
            .select_only()
            .column(mailbox::Column::UserId)
            .column_as(mailbox::Column::Id.count(), "count")
            .filter(mailbox::Column::UserId.is_in(ids.clone()))
            .group_by(mailbox::Column::UserId)
            .into_tuple::<(String, i64)>()
            .all(&state.pool)
            .await
            .map_err(internal)?
        {
            mailbox_counts.insert(user_id, count.max(0) as u64);
        }
        for (user_id, count) in email::Entity::find()
            .select_only()
            .column(email::Column::UserId)
            .column_as(email::Column::Id.count(), "count")
            .filter(email::Column::UserId.is_in(ids))
            .group_by(email::Column::UserId)
            .into_tuple::<(String, i64)>()
            .all(&state.pool)
            .await
            .map_err(internal)?
        {
            email_counts.insert(user_id, count.max(0) as u64);
        }
    }
    let items = models
        .into_iter()
        .map(|user| {
            let account = account_map.get(&user.id);
            AdminUserItem {
                mailbox_count: mailbox_counts.get(&user.id).copied().unwrap_or(0),
                email_count: email_counts.get(&user.id).copied().unwrap_or(0),
                credit_balance: account.map(|value| value.balance).unwrap_or(0),
                credit_total_granted: account.map(|value| value.total_granted).unwrap_or(0),
                credit_total_consumed: account.map(|value| value.total_consumed).unwrap_or(0),
                is_current: user.id == current_user_id,
                id: user.id,
                email: user.email,
                role: user.role,
                status: user.status,
                created_at: iso(user.created_at),
            }
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

#[derive(Debug, Deserialize)]
pub struct UserBulkBody {
    pub ids: Vec<String>,
    pub action: String,
    pub reason: String,
}

pub async fn bulk_users(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<UserBulkBody>,
) -> Result<Json<ApiResponse<BulkResult<String>>>, ErrorResponse> {
    let ids = unique_ids(body.ids)?;
    let reason = validate_reason(&body.reason)?;
    let status = match body.action.as_str() {
        "activate" => "active",
        "suspend" => "suspended",
        "ban" => "banned",
        _ => return Err(ErrorResponse::bad_request("不支持的用户批量操作")),
    };
    let batch_id = uuid::Uuid::new_v4().to_string();
    let mut succeeded = Vec::new();
    let mut failed = Vec::new();
    for id in &ids {
        let result = async {
            if id == &operator_id {
                anyhow::bail!("不能修改当前登录账号");
            }
            let model = users::Entity::find_by_id(id)
                .one(&state.pool)
                .await?
                .ok_or_else(|| anyhow::anyhow!("用户不存在"))?;
            let mut active: users::ActiveModel = model.into();
            active.status = Set(status.into());
            active.update(&state.pool).await?;
            admin_audit(
                &state,
                &operator_id,
                "admin_user_update",
                status,
                Some(&reason),
                "user",
                id,
                Some(json!({"batch_id": batch_id})),
            )
            .await?;
            Ok::<_, anyhow::Error>(())
        }
        .await;
        match result {
            Ok(()) => succeeded.push(id.clone()),
            Err(error) => failed.push(BulkFailure {
                id: id.clone(),
                message: error.to_string(),
            }),
        }
    }
    Ok(Json(ApiResponse::success(BulkResult {
        batch_id,
        requested: ids.len(),
        succeeded,
        failed,
    })))
}

// ===== 邮箱 =====

#[derive(Debug, Deserialize)]
pub struct MailboxesQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub q: Option<String>,
    pub status: Option<String>,
    pub expires_from: Option<String>,
    pub expires_to: Option<String>,
    pub sort: Option<String>,
    pub order: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AdminMailboxItem {
    pub id: i64,
    pub address: String,
    pub user_id: String,
    pub user_email: Option<String>,
    pub note: Option<String>,
    pub status: String,
    pub used_bytes: i64,
    pub max_quota_bytes: i64,
    pub email_count: u64,
    pub expires_at: String,
    pub created_at: String,
}

pub async fn list_mailboxes(
    State(state): State<AppState>,
    Query(query): Query<MailboxesQuery>,
) -> Result<Json<ApiResponse<PageData<AdminMailboxItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = mailbox::Entity::find();
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(mailbox::Column::Address.contains(q))
                .add(mailbox::Column::UserId.contains(q))
                .add(mailbox::Column::Note.contains(q)),
        );
    }
    if let Some(status) = query.status.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(mailbox::Column::Status.eq(status));
    }
    if let Some(from) = parse_datetime(query.expires_from.as_deref())? {
        find = find.filter(mailbox::Column::ExpiresAt.gte(from));
    }
    if let Some(to) = parse_datetime(query.expires_to.as_deref())? {
        find = find.filter(mailbox::Column::ExpiresAt.lte(to));
    }
    let desc = query.order.as_deref() != Some("asc");
    find = match query.sort.as_deref() {
        Some("address") if desc => find.order_by_desc(mailbox::Column::Address),
        Some("address") => find.order_by_asc(mailbox::Column::Address),
        Some("expires_at") if desc => find.order_by_desc(mailbox::Column::ExpiresAt),
        Some("expires_at") => find.order_by_asc(mailbox::Column::ExpiresAt),
        _ if desc => find.order_by_desc(mailbox::Column::CreatedAt),
        _ => find.order_by_asc(mailbox::Column::CreatedAt),
    };
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let user_ids: Vec<_> = models.iter().map(|item| item.user_id.clone()).collect();
    let mailbox_ids: Vec<_> = models.iter().map(|item| item.id).collect();
    let user_map: HashMap<_, _> = if user_ids.is_empty() {
        HashMap::new()
    } else {
        users::Entity::find()
            .filter(users::Column::Id.is_in(user_ids))
            .all(&state.pool)
            .await
            .map_err(internal)?
            .into_iter()
            .map(|user| (user.id, user.email))
            .collect()
    };
    let mut email_counts = HashMap::<i64, u64>::new();
    if !mailbox_ids.is_empty() {
        for (mailbox_id, count) in email::Entity::find()
            .select_only()
            .column(email::Column::MailboxId)
            .column_as(email::Column::Id.count(), "count")
            .filter(email::Column::MailboxId.is_in(mailbox_ids))
            .group_by(email::Column::MailboxId)
            .into_tuple::<(i64, i64)>()
            .all(&state.pool)
            .await
            .map_err(internal)?
        {
            email_counts.insert(mailbox_id, count.max(0) as u64);
        }
    }
    let items = models
        .into_iter()
        .map(|item| AdminMailboxItem {
            id: item.id,
            user_email: user_map.get(&item.user_id).cloned(),
            email_count: email_counts.get(&item.id).copied().unwrap_or(0),
            address: item.address,
            user_id: item.user_id,
            note: item.note,
            status: item.status,
            used_bytes: item.used_bytes,
            max_quota_bytes: item.max_quota_bytes,
            expires_at: iso(item.expires_at),
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

#[derive(Debug, Deserialize)]
pub struct IdBulkBody {
    pub ids: Vec<i64>,
    pub action: String,
    pub reason: String,
    pub ttl_seconds: Option<i64>,
}

pub async fn bulk_mailboxes(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<IdBulkBody>,
) -> Result<Json<ApiResponse<BulkResult<i64>>>, ErrorResponse> {
    let ids = unique_ids(body.ids)?;
    let reason = validate_reason(&body.reason)?;
    if body.action != "revoke" {
        return Err(ErrorResponse::bad_request("不支持的邮箱批量操作"));
    }
    let batch_id = uuid::Uuid::new_v4().to_string();
    let mut succeeded = Vec::new();
    let mut failed = Vec::new();
    for id in &ids {
        let result = async {
            let model = mailbox::Entity::find_by_id(*id)
                .one(&state.pool)
                .await?
                .ok_or_else(|| anyhow::anyhow!("邮箱不存在"))?;
            if model.status == "revoked" {
                anyhow::bail!("邮箱已吊销");
            }
            let mut active: mailbox::ActiveModel = model.into();
            active.status = Set("revoked".into());
            active.update(&state.pool).await?;
            admin_audit(
                &state,
                &operator_id,
                "admin_mailbox_update",
                "revoke",
                Some(&reason),
                "mailbox",
                id,
                Some(json!({"batch_id": batch_id})),
            )
            .await?;
            Ok::<_, anyhow::Error>(())
        }
        .await;
        match result {
            Ok(()) => succeeded.push(*id),
            Err(error) => failed.push(BulkFailure {
                id: *id,
                message: error.to_string(),
            }),
        }
    }
    Ok(Json(ApiResponse::success(BulkResult {
        batch_id,
        requested: ids.len(),
        succeeded,
        failed,
    })))
}

// ===== 邮件 =====

#[derive(Debug, Deserialize)]
pub struct EmailsQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub q: Option<String>,
    pub status: Option<String>,
    pub source_ip: Option<String>,
    pub spf_result: Option<String>,
    pub dkim_result: Option<String>,
    pub dmarc_result: Option<String>,
    pub abuse_min: Option<i32>,
    pub abuse_max: Option<i32>,
    pub received_from: Option<String>,
    pub received_to: Option<String>,
    pub sort: Option<String>,
    pub order: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AdminEmailItem {
    pub id: i64,
    pub mailbox_id: i64,
    pub user_id: String,
    pub recipient: String,
    pub mail_from: String,
    pub sender_name: Option<String>,
    pub subject: Option<String>,
    pub source_ip: String,
    pub spf_result: Option<String>,
    pub dkim_result: Option<String>,
    pub dmarc_result: Option<String>,
    pub abuse_score: i32,
    pub status: String,
    pub size_bytes: i64,
    pub received_at: String,
    pub expires_at: String,
}

pub async fn list_emails(
    State(state): State<AppState>,
    Query(query): Query<EmailsQuery>,
) -> Result<Json<ApiResponse<PageData<AdminEmailItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = email::Entity::find();
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(email::Column::MailFrom.contains(q))
                .add(email::Column::Recipient.contains(q))
                .add(email::Column::Subject.contains(q))
                .add(email::Column::MessageId.contains(q)),
        );
    }
    if let Some(status) = query.status.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email::Column::Status.eq(status));
    }
    if let Some(source_ip) = query.source_ip.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email::Column::SourceIp.contains(source_ip));
    }
    if let Some(value) = query.spf_result.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email::Column::SpfResult.eq(value));
    }
    if let Some(value) = query.dkim_result.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email::Column::DkimResult.eq(value));
    }
    if let Some(value) = query.dmarc_result.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email::Column::DmarcResult.eq(value));
    }
    if let Some(value) = query.abuse_min {
        find = find.filter(email::Column::AbuseScore.gte(value));
    }
    if let Some(value) = query.abuse_max {
        find = find.filter(email::Column::AbuseScore.lte(value));
    }
    if let Some(value) = parse_datetime(query.received_from.as_deref())? {
        find = find.filter(email::Column::ReceivedAt.gte(value));
    }
    if let Some(value) = parse_datetime(query.received_to.as_deref())? {
        find = find.filter(email::Column::ReceivedAt.lte(value));
    }
    let desc = query.order.as_deref() != Some("asc");
    find = match query.sort.as_deref() {
        Some("abuse_score") if desc => find.order_by_desc(email::Column::AbuseScore),
        Some("abuse_score") => find.order_by_asc(email::Column::AbuseScore),
        Some("size_bytes") if desc => find.order_by_desc(email::Column::SizeBytes),
        Some("size_bytes") => find.order_by_asc(email::Column::SizeBytes),
        _ if desc => find.order_by_desc(email::Column::ReceivedAt),
        _ => find.order_by_asc(email::Column::ReceivedAt),
    };
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let items = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .map(|message| AdminEmailItem {
            id: message.id,
            mailbox_id: message.mailbox_id,
            user_id: message.user_id,
            recipient: message.recipient,
            mail_from: message.mail_from,
            sender_name: message.sender_name,
            subject: message.subject,
            source_ip: message.source_ip,
            spf_result: message.spf_result,
            dkim_result: message.dkim_result,
            dmarc_result: message.dmarc_result,
            abuse_score: message.abuse_score,
            status: message.status,
            size_bytes: message.size_bytes,
            received_at: iso(message.received_at),
            expires_at: iso(message.expires_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

pub async fn get_email_detail(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Path(id): Path<i64>,
) -> Result<Json<ApiResponse<Value>>, ErrorResponse> {
    let message = email::Entity::find_by_id(id)
        .one(&state.pool)
        .await
        .map_err(internal)?
        .ok_or_else(|| ErrorResponse::not_found("邮件不存在"))?;
    let attachments = AttachmentRepository::new(state.pool.clone())
        .list_by_email(id)
        .await
        .map_err(internal)?;
    let encryption_key = crate::utils::crypto::derive_key(&state.config.smtp.body_encryption_key);
    admin_audit(
        &state,
        &operator_id,
        "admin_email_view",
        "view",
        None,
        "email",
        id,
        None,
    )
    .await
    .map_err(internal)?;
    Ok(Json(ApiResponse::success(json!({
        "id": message.id,
        "mailbox_id": message.mailbox_id,
        "user_id": message.user_id,
        "recipient": message.recipient,
        "mail_from": message.mail_from,
        "rcpt_to": message.rcpt_to,
        "sender_name": message.sender_name,
        "subject": message.subject,
        "text_body": crate::utils::crypto::decrypt_optional(&encryption_key, message.text_body),
        "html_body": crate::utils::crypto::decrypt_optional(&encryption_key, message.html_body),
        "source_ip": message.source_ip,
        "helo": message.helo,
        "ptr_result": message.ptr_result,
        "spf_result": message.spf_result,
        "dkim_result": message.dkim_result,
        "dmarc_result": message.dmarc_result,
        "abuse_score": message.abuse_score,
        "status": message.status,
        "size_bytes": message.size_bytes,
        "message_id": message.message_id,
        "received_at": iso(message.received_at),
        "expires_at": iso(message.expires_at),
        "attachments": attachments.into_iter().map(|item| json!({
            "id": item.id,
            "filename": item.filename,
            "content_type": item.content_type,
            "size_bytes": item.size_bytes,
        })).collect::<Vec<_>>(),
    }))))
}

pub async fn get_email_attachment(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Path((id, attachment_id)): Path<(i64, i64)>,
) -> Result<Response, ErrorResponse> {
    email::Entity::find_by_id(id)
        .one(&state.pool)
        .await
        .map_err(internal)?
        .ok_or_else(|| ErrorResponse::not_found("邮件不存在"))?;
    let attachment = email_attachment::Entity::find_by_id(attachment_id)
        .one(&state.pool)
        .await
        .map_err(internal)?
        .filter(|item| item.email_id == id)
        .ok_or_else(|| ErrorResponse::not_found("附件不存在"))?;
    admin_audit(
        &state,
        &operator_id,
        "admin_attachment_download",
        "download",
        None,
        "attachment",
        attachment_id,
        Some(json!({"email_id": id})),
    )
    .await
    .map_err(internal)?;
    let filename = attachment.filename.unwrap_or_else(|| "attachment".into());
    let content_type = attachment.content_type;
    let mut response = (StatusCode::OK, attachment.content.unwrap_or_default()).into_response();
    response.headers_mut().insert(
        axum::http::header::CONTENT_TYPE,
        content_type
            .parse()
            .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
    );
    if let Ok(value) = format!("attachment; filename=\"{}\"", filename.replace('"', "_")).parse() {
        response
            .headers_mut()
            .insert(axum::http::header::CONTENT_DISPOSITION, value);
    }
    Ok(response)
}

async fn delete_email_admin(state: &AppState, id: i64) -> anyhow::Result<()> {
    let txn = state.pool.begin().await?;
    let operation = async {
        let message = email::Entity::find_by_id(id)
            .one(&txn)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
        email_attachment::Entity::delete_many()
            .filter(email_attachment::Column::EmailId.eq(id))
            .exec(&txn)
            .await?;
        email::Entity::delete_by_id(id).exec(&txn).await?;
        let updated = mailbox::Entity::update_many()
            .col_expr(
                mailbox::Column::UsedBytes,
                Expr::col(mailbox::Column::UsedBytes).sub(message.size_bytes),
            )
            .filter(mailbox::Column::Id.eq(message.mailbox_id))
            .filter(mailbox::Column::UsedBytes.gte(message.size_bytes))
            .exec(&txn)
            .await?;
        if updated.rows_affected == 0 {
            mailbox::Entity::update_many()
                .col_expr(mailbox::Column::UsedBytes, Expr::value(0))
                .filter(mailbox::Column::Id.eq(message.mailbox_id))
                .exec(&txn)
                .await?;
        }
        Ok::<_, anyhow::Error>(())
    }
    .await;
    match operation {
        Ok(()) => {
            txn.commit().await?;
            Ok(())
        }
        Err(error) => {
            txn.rollback().await?;
            Err(error)
        }
    }
}

async fn release_email_admin(state: &AppState, id: i64) -> anyhow::Result<email::Model> {
    let rules = CreditRuleService::new(state.pool.clone()).get().await?;
    let txn = state.pool.begin().await?;
    let operation = async {
        let message = email::Entity::find_by_id(id)
            .one(&txn)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
        if message.status != "quarantined" {
            anyhow::bail!("邮件不在隔离区");
        }
        QuotaRepository::reserve_email(
            &txn,
            &message.user_id,
            message.size_bytes,
            rules.daily_emails_limit,
        )
        .await?;
        CreditService::new(state.pool.clone())
            .debit_in_transaction(
                &txn,
                &message.user_id,
                rules.receive_email_cost,
                "receive_email",
                Some(message.mailbox_id),
                Some(message.id),
            )
            .await?;
        let mut active: email::ActiveModel = message.clone().into();
        active.status = Set("received".into());
        active.update(&txn).await?;
        Ok::<_, anyhow::Error>(message)
    }
    .await;
    match operation {
        Ok(message) => {
            txn.commit().await?;
            let payload = json!({
                "event": "new_email",
                "data": {
                    "id": message.id,
                    "mail_from": message.mail_from,
                    "sender_name": message.sender_name,
                    "subject": message.subject,
                    "size_bytes": message.size_bytes,
                    "received_at": iso(message.received_at),
                }
            });
            state
                .mail_hub
                .notify(&message.recipient, &payload.to_string());
            Ok(message)
        }
        Err(error) => {
            txn.rollback().await?;
            Err(error)
        }
    }
}

fn sender_domain(value: &str) -> Option<String> {
    value
        .trim()
        .trim_matches(|c| c == '<' || c == '>')
        .rsplit_once('@')
        .map(|(_, domain)| domain.to_lowercase())
        .filter(|domain| !domain.is_empty())
}

pub async fn bulk_emails(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<IdBulkBody>,
) -> Result<Json<ApiResponse<BulkResult<i64>>>, ErrorResponse> {
    let ids = unique_ids(body.ids)?;
    let reason = validate_reason(&body.reason)?;
    if !matches!(
        body.action.as_str(),
        "delete" | "release" | "block_sender" | "block_domain" | "block_ip"
    ) {
        return Err(ErrorResponse::bad_request("不支持的邮件批量操作"));
    }
    let expires_at = body.ttl_seconds.and_then(|seconds| {
        (seconds > 0).then(|| Utc::now().naive_utc() + chrono::Duration::seconds(seconds))
    });
    let batch_id = uuid::Uuid::new_v4().to_string();
    let mut succeeded = Vec::new();
    let mut failed = Vec::new();
    for id in &ids {
        let result = async {
            let message = email::Entity::find_by_id(*id)
                .one(&state.pool)
                .await?
                .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
            match body.action.as_str() {
                "delete" => delete_email_admin(&state, *id).await?,
                "release" => {
                    release_email_admin(&state, *id).await?;
                }
                "block_sender" => {
                    let value = message
                        .mail_from
                        .trim()
                        .trim_matches(|c| c == '<' || c == '>')
                        .to_lowercase();
                    if value.is_empty() {
                        anyhow::bail!("发件地址为空");
                    }
                    let repository = BlacklistRepository::new(state.pool.clone());
                    if repository
                        .find_active_sender("address", &value)
                        .await?
                        .is_none()
                    {
                        repository
                            .add_sender(
                                "address".into(),
                                value,
                                reason.clone(),
                                "manual".into(),
                                expires_at,
                            )
                            .await?;
                    }
                }
                "block_domain" => {
                    let value = sender_domain(&message.mail_from)
                        .ok_or_else(|| anyhow::anyhow!("无法解析发件域"))?;
                    let repository = BlacklistRepository::new(state.pool.clone());
                    if repository
                        .find_active_sender("domain", &value)
                        .await?
                        .is_none()
                    {
                        repository
                            .add_sender(
                                "domain".into(),
                                value,
                                reason.clone(),
                                "manual".into(),
                                expires_at,
                            )
                            .await?;
                    }
                }
                "block_ip" => {
                    let repository = BlacklistRepository::new(state.pool.clone());
                    if repository
                        .find_active_ip(&message.source_ip)
                        .await?
                        .is_none()
                    {
                        repository
                            .add_ip(
                                message.source_ip.clone(),
                                reason.clone(),
                                "manual".into(),
                                expires_at,
                            )
                            .await?;
                    }
                }
                _ => unreachable!(),
            }
            admin_audit(
                &state,
                &operator_id,
                "admin_email_action",
                &body.action,
                Some(&reason),
                "email",
                id,
                Some(json!({"batch_id": batch_id})),
            )
            .await?;
            Ok::<_, anyhow::Error>(())
        }
        .await;
        match result {
            Ok(()) => succeeded.push(*id),
            Err(error) => failed.push(BulkFailure {
                id: *id,
                message: error.to_string(),
            }),
        }
    }
    Ok(Json(ApiResponse::success(BulkResult {
        batch_id,
        requested: ids.len(),
        succeeded,
        failed,
    })))
}

// ===== 黑名单 =====

#[derive(Debug, Deserialize)]
pub struct BlacklistQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub q: Option<String>,
    pub source: Option<String>,
    pub active: Option<bool>,
}

#[derive(Debug, Serialize)]
pub struct SenderBlacklistItem {
    pub id: i64,
    pub kind: String,
    pub value: String,
    pub reason: String,
    pub source: String,
    pub active: bool,
    pub expires_at: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Serialize)]
pub struct IpBlacklistItem {
    pub id: i64,
    pub ip: String,
    pub reason: String,
    pub source: String,
    pub active: bool,
    pub expires_at: Option<String>,
    pub created_at: String,
}

fn blacklist_active(expires_at: Option<NaiveDateTime>) -> bool {
    expires_at.is_none_or(|value| value > Utc::now().naive_utc())
}

pub async fn list_sender_blacklists(
    State(state): State<AppState>,
    Query(query): Query<BlacklistQuery>,
) -> Result<Json<ApiResponse<PageData<SenderBlacklistItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let now = Utc::now().naive_utc();
    let mut find = blocked_sender::Entity::find();
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(blocked_sender::Column::Value.contains(q))
                .add(blocked_sender::Column::Reason.contains(q)),
        );
    }
    if let Some(source) = query.source.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(blocked_sender::Column::Source.eq(source));
    }
    if let Some(active) = query.active {
        let condition = Condition::any()
            .add(blocked_sender::Column::ExpiresAt.is_null())
            .add(blocked_sender::Column::ExpiresAt.gt(now));
        find = if active {
            find.filter(condition)
        } else {
            find.filter(blocked_sender::Column::ExpiresAt.lte(now))
        };
    }
    find = find.order_by_desc(blocked_sender::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let items = models
        .into_iter()
        .map(|item| SenderBlacklistItem {
            id: item.id,
            kind: item.kind,
            value: item.value,
            reason: item.reason,
            source: item.source,
            active: blacklist_active(item.expires_at),
            expires_at: item.expires_at.map(iso),
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

pub async fn list_ip_blacklists(
    State(state): State<AppState>,
    Query(query): Query<BlacklistQuery>,
) -> Result<Json<ApiResponse<PageData<IpBlacklistItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let now = Utc::now().naive_utc();
    let mut find = blocked_ip::Entity::find();
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(blocked_ip::Column::Ip.contains(q))
                .add(blocked_ip::Column::Reason.contains(q)),
        );
    }
    if let Some(source) = query.source.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(blocked_ip::Column::Source.eq(source));
    }
    if let Some(active) = query.active {
        let condition = Condition::any()
            .add(blocked_ip::Column::ExpiresAt.is_null())
            .add(blocked_ip::Column::ExpiresAt.gt(now));
        find = if active {
            find.filter(condition)
        } else {
            find.filter(blocked_ip::Column::ExpiresAt.lte(now))
        };
    }
    find = find.order_by_desc(blocked_ip::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let items = models
        .into_iter()
        .map(|item| IpBlacklistItem {
            id: item.id,
            ip: item.ip,
            reason: item.reason,
            source: item.source,
            active: blacklist_active(item.expires_at),
            expires_at: item.expires_at.map(iso),
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

#[derive(Debug, Deserialize)]
pub struct AddSenderBlacklistBody {
    pub kind: String,
    pub value: String,
    pub reason: String,
    pub ttl_seconds: Option<i64>,
}

pub async fn add_sender_blacklist(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<AddSenderBlacklistBody>,
) -> Result<Json<ApiResponse<SenderBlacklistItem>>, ErrorResponse> {
    let reason = validate_reason(&body.reason)?;
    if !matches!(body.kind.as_str(), "address" | "domain" | "pattern") {
        return Err(ErrorResponse::bad_request("黑名单类型无效"));
    }
    let value = body.value.trim().to_lowercase();
    if value.is_empty() {
        return Err(ErrorResponse::bad_request("黑名单值不能为空"));
    }
    let expires_at = body
        .ttl_seconds
        .filter(|value| *value > 0)
        .map(|value| Utc::now().naive_utc() + chrono::Duration::seconds(value));
    let model = BlacklistRepository::new(state.pool.clone())
        .add_sender(
            body.kind,
            value,
            reason.clone(),
            "manual".into(),
            expires_at,
        )
        .await
        .map_err(internal)?;
    admin_audit(
        &state,
        &operator_id,
        "admin_blacklist_change",
        "add",
        Some(&reason),
        "blocked_sender",
        model.id,
        None,
    )
    .await
    .map_err(internal)?;
    Ok(Json(ApiResponse::success(SenderBlacklistItem {
        id: model.id,
        kind: model.kind,
        value: model.value,
        reason: model.reason,
        source: model.source,
        active: blacklist_active(model.expires_at),
        expires_at: model.expires_at.map(iso),
        created_at: iso(model.created_at),
    })))
}

#[derive(Debug, Deserialize)]
pub struct AddIpBlacklistBody {
    pub ip: String,
    pub reason: String,
    pub ttl_seconds: Option<i64>,
}

pub async fn add_ip_blacklist(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<AddIpBlacklistBody>,
) -> Result<Json<ApiResponse<IpBlacklistItem>>, ErrorResponse> {
    let reason = validate_reason(&body.reason)?;
    let ip = body.ip.trim().to_owned();
    if ip.is_empty() {
        return Err(ErrorResponse::bad_request("IP 不能为空"));
    }
    let expires_at = body
        .ttl_seconds
        .filter(|value| *value > 0)
        .map(|value| Utc::now().naive_utc() + chrono::Duration::seconds(value));
    let model = BlacklistRepository::new(state.pool.clone())
        .add_ip(ip, reason.clone(), "manual".into(), expires_at)
        .await
        .map_err(internal)?;
    admin_audit(
        &state,
        &operator_id,
        "admin_blacklist_change",
        "add",
        Some(&reason),
        "blocked_ip",
        model.id,
        None,
    )
    .await
    .map_err(internal)?;
    Ok(Json(ApiResponse::success(IpBlacklistItem {
        id: model.id,
        ip: model.ip,
        reason: model.reason,
        source: model.source,
        active: blacklist_active(model.expires_at),
        expires_at: model.expires_at.map(iso),
        created_at: iso(model.created_at),
    })))
}

#[derive(Debug, Deserialize)]
pub struct BlacklistBulkBody {
    pub ids: Vec<i64>,
    pub action: String,
    pub reason: String,
}

async fn bulk_unblock(
    state: &AppState,
    operator_id: &str,
    body: BlacklistBulkBody,
    target: &str,
) -> Result<BulkResult<i64>, ErrorResponse> {
    let ids = unique_ids(body.ids)?;
    let reason = validate_reason(&body.reason)?;
    if body.action != "remove" {
        return Err(ErrorResponse::bad_request("仅支持解除黑名单"));
    }
    let batch_id = uuid::Uuid::new_v4().to_string();
    let mut succeeded = Vec::new();
    let mut failed = Vec::new();
    for id in &ids {
        let result = async {
            let deleted = if target == "blocked_sender" {
                blocked_sender::Entity::delete_by_id(*id)
                    .exec(&state.pool)
                    .await?
                    .rows_affected
            } else {
                blocked_ip::Entity::delete_by_id(*id)
                    .exec(&state.pool)
                    .await?
                    .rows_affected
            };
            if deleted == 0 {
                anyhow::bail!("黑名单记录不存在");
            }
            admin_audit(
                state,
                operator_id,
                "admin_blacklist_change",
                "remove",
                Some(&reason),
                target,
                id,
                Some(json!({"batch_id": batch_id})),
            )
            .await?;
            Ok::<_, anyhow::Error>(())
        }
        .await;
        match result {
            Ok(()) => succeeded.push(*id),
            Err(error) => failed.push(BulkFailure {
                id: *id,
                message: error.to_string(),
            }),
        }
    }
    Ok(BulkResult {
        batch_id,
        requested: ids.len(),
        succeeded,
        failed,
    })
}

pub async fn bulk_sender_blacklists(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<BlacklistBulkBody>,
) -> Result<Json<ApiResponse<BulkResult<i64>>>, ErrorResponse> {
    Ok(Json(ApiResponse::success(
        bulk_unblock(&state, &operator_id, body, "blocked_sender").await?,
    )))
}

pub async fn bulk_ip_blacklists(
    State(state): State<AppState>,
    UserId(operator_id): UserId,
    Json(body): Json<BlacklistBulkBody>,
) -> Result<Json<ApiResponse<BulkResult<i64>>>, ErrorResponse> {
    Ok(Json(ApiResponse::success(
        bulk_unblock(&state, &operator_id, body, "blocked_ip").await?,
    )))
}

// ===== 运维审计、积分流水与外发日志 =====

#[derive(Debug, Deserialize)]
pub struct AuditLogsQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub event_type: Option<String>,
    pub action: Option<String>,
    pub operator_id: Option<String>,
    pub source_ip: Option<String>,
    pub q: Option<String>,
    pub created_from: Option<String>,
    pub created_to: Option<String>,
    pub scope: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct AuditLogItem {
    pub id: i64,
    pub event_type: String,
    pub source_ip: Option<String>,
    pub helo: Option<String>,
    pub mail_from: Option<String>,
    pub rcpt_to: Option<String>,
    pub action: String,
    pub reason: Option<String>,
    pub operator_id: Option<String>,
    pub target_type: Option<String>,
    pub target_id: Option<String>,
    pub metadata: Option<Value>,
    pub created_at: String,
}

pub async fn list_audit_logs(
    State(state): State<AppState>,
    Query(query): Query<AuditLogsQuery>,
) -> Result<Json<ApiResponse<PageData<AuditLogItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = audit_log::Entity::find();
    match query.scope.as_deref() {
        Some("admin") => find = find.filter(audit_log::Column::OperatorId.is_not_null()),
        Some("smtp") => find = find.filter(audit_log::Column::OperatorId.is_null()),
        Some("") | None => {}
        Some(_) => return Err(ErrorResponse::bad_request("scope 仅支持 admin 或 smtp")),
    }
    if let Some(value) = query.event_type.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(audit_log::Column::EventType.eq(value));
    }
    if let Some(value) = query.action.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(audit_log::Column::Action.eq(value));
    }
    if let Some(value) = query.operator_id.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(audit_log::Column::OperatorId.eq(value));
    }
    if let Some(value) = query.source_ip.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(audit_log::Column::SourceIp.eq(value));
    }
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(audit_log::Column::Reason.contains(q))
                .add(audit_log::Column::MailFrom.contains(q))
                .add(audit_log::Column::RcptTo.contains(q))
                .add(audit_log::Column::TargetId.contains(q)),
        );
    }
    if let Some(from) = parse_datetime(query.created_from.as_deref())? {
        find = find.filter(audit_log::Column::CreatedAt.gte(from));
    }
    if let Some(to) = parse_datetime(query.created_to.as_deref())? {
        find = find.filter(audit_log::Column::CreatedAt.lte(to));
    }
    find = find.order_by_desc(audit_log::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let items = models
        .into_iter()
        .map(|item| AuditLogItem {
            id: item.id,
            event_type: item.event_type,
            source_ip: item.source_ip,
            helo: item.helo,
            mail_from: item.mail_from,
            rcpt_to: item.rcpt_to,
            action: item.action,
            reason: item.reason,
            operator_id: item.operator_id,
            target_type: item.target_type,
            target_id: item.target_id,
            metadata: item
                .metadata_json
                .as_deref()
                .and_then(|value| serde_json::from_str(value).ok()),
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

#[derive(Debug, Deserialize)]
pub struct CreditTransactionsQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub user_id: Option<String>,
    pub reason: Option<String>,
    pub operator_id: Option<String>,
    pub created_from: Option<String>,
    pub created_to: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct CreditTransactionItem {
    pub id: i64,
    pub user_id: String,
    pub user_email: Option<String>,
    pub delta: i64,
    pub balance_after: i64,
    pub reason: String,
    pub description: Option<String>,
    pub operator_id: Option<String>,
    pub related_mailbox_id: Option<i64>,
    pub related_email_id: Option<i64>,
    pub created_at: String,
}

pub async fn list_credit_transactions(
    State(state): State<AppState>,
    Query(query): Query<CreditTransactionsQuery>,
) -> Result<Json<ApiResponse<PageData<CreditTransactionItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = credit_transaction::Entity::find();
    if let Some(value) = query.user_id.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(credit_transaction::Column::UserId.eq(value));
    }
    if let Some(value) = query.reason.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(credit_transaction::Column::Reason.eq(value));
    }
    if let Some(value) = query.operator_id.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(credit_transaction::Column::OperatorId.eq(value));
    }
    if let Some(from) = parse_datetime(query.created_from.as_deref())? {
        find = find.filter(credit_transaction::Column::CreatedAt.gte(from));
    }
    if let Some(to) = parse_datetime(query.created_to.as_deref())? {
        find = find.filter(credit_transaction::Column::CreatedAt.lte(to));
    }
    find = find.order_by_desc(credit_transaction::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let user_ids: Vec<_> = models.iter().map(|item| item.user_id.clone()).collect();
    let user_map: HashMap<_, _> = users::Entity::find()
        .filter(users::Column::Id.is_in(user_ids))
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .map(|user| (user.id, user.email))
        .collect();
    let items = models
        .into_iter()
        .map(|item| CreditTransactionItem {
            id: item.id,
            user_email: user_map.get(&item.user_id).cloned(),
            user_id: item.user_id,
            delta: item.delta,
            balance_after: item.balance_after,
            reason: item.reason,
            description: item.description,
            operator_id: item.operator_id,
            related_mailbox_id: item.related_mailbox_id,
            related_email_id: item.related_email_id,
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

#[derive(Debug, Deserialize)]
pub struct EmailLogsQuery {
    pub page: Option<u64>,
    pub page_size: Option<u64>,
    pub status: Option<String>,
    pub recipient: Option<String>,
    pub q: Option<String>,
    pub created_from: Option<String>,
    pub created_to: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct EmailLogItem {
    pub id: i64,
    pub user_id: Option<String>,
    pub recipient: String,
    pub kind: String,
    pub status: String,
    pub error: Option<String>,
    pub created_at: String,
}

pub async fn list_email_logs(
    State(state): State<AppState>,
    Query(query): Query<EmailLogsQuery>,
) -> Result<Json<ApiResponse<PageData<EmailLogItem>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let mut find = email_logs::Entity::find();
    if let Some(value) = query.status.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email_logs::Column::Status.eq(value));
    }
    if let Some(value) = query.recipient.as_deref().filter(|v| !v.is_empty()) {
        find = find.filter(email_logs::Column::Recipient.contains(value));
    }
    if let Some(q) = query.q.as_deref().map(str::trim).filter(|q| !q.is_empty()) {
        find = find.filter(
            Condition::any()
                .add(email_logs::Column::Recipient.contains(q))
                .add(email_logs::Column::Error.contains(q)),
        );
    }
    if let Some(from) = parse_datetime(query.created_from.as_deref())? {
        find = find.filter(email_logs::Column::CreatedAt.gte(from));
    }
    if let Some(to) = parse_datetime(query.created_to.as_deref())? {
        find = find.filter(email_logs::Column::CreatedAt.lte(to));
    }
    find = find.order_by_desc(email_logs::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let models = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let items = models
        .into_iter()
        .map(|item| EmailLogItem {
            id: item.id,
            user_id: item.user_id,
            recipient: item.recipient,
            kind: item.kind,
            status: item.status,
            error: item.error,
            created_at: iso(item.created_at),
        })
        .collect();
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

pub async fn list_credit_rule_changes(
    State(state): State<AppState>,
    Query(query): Query<EmailLogsQuery>,
) -> Result<Json<ApiResponse<PageData<credit_rule_change::Model>>>, ErrorResponse> {
    let (page, page_size) = paging(query.page, query.page_size);
    let find =
        credit_rule_change::Entity::find().order_by_desc(credit_rule_change::Column::CreatedAt);
    let total = find.clone().count(&state.pool).await.map_err(internal)?;
    let items = find
        .offset((page - 1) * page_size)
        .limit(page_size)
        .all(&state.pool)
        .await
        .map_err(internal)?;
    Ok(Json(ApiResponse::success(page_data(
        items, page, page_size, total,
    ))))
}

// ===== 运行概览 =====

#[derive(Debug, Deserialize)]
pub struct OverviewQuery {
    pub range: Option<String>,
}

fn increment(map: &mut BTreeMap<String, u64>, key: impl Into<String>) {
    *map.entry(key.into()).or_default() += 1;
}

fn top_values(values: HashMap<String, u64>, limit: usize) -> Vec<Value> {
    let mut values: Vec<_> = values.into_iter().collect();
    values.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0)));
    values
        .into_iter()
        .take(limit)
        .map(|(name, count)| json!({"name": name, "count": count}))
        .collect()
}

pub async fn overview(
    State(state): State<AppState>,
    Query(query): Query<OverviewQuery>,
) -> Result<Json<ApiResponse<Value>>, ErrorResponse> {
    let range = query.range.as_deref().unwrap_or("7d");
    let duration = match range {
        "24h" => chrono::Duration::hours(24),
        "7d" => chrono::Duration::days(7),
        "30d" => chrono::Duration::days(30),
        _ => return Err(ErrorResponse::bad_request("range 仅支持 24h、7d 或 30d")),
    };
    let now = Utc::now();
    let since = (now - duration).naive_utc();
    let local_now = now.with_timezone(&chrono_tz::Asia::Shanghai);
    let local_midnight = local_now
        .date_naive()
        .and_hms_opt(0, 0, 0)
        .expect("valid midnight");
    let today_start = chrono_tz::Asia::Shanghai
        .from_local_datetime(&local_midnight)
        .single()
        .expect("Shanghai has an unambiguous midnight")
        .with_timezone(&Utc)
        .naive_utc();
    let now_naive = now.naive_utc();

    let total_users = users::Entity::find()
        .filter(users::Column::DeletedAt.is_null())
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let today_users = users::Entity::find()
        .filter(users::Column::DeletedAt.is_null())
        .filter(users::Column::CreatedAt.gte(today_start))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let active_mailboxes = mailbox::Entity::find()
        .filter(mailbox::Column::Status.eq("active"))
        .filter(mailbox::Column::ExpiresAt.gt(now_naive))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let expiring_mailboxes = mailbox::Entity::find()
        .filter(mailbox::Column::Status.eq("active"))
        .filter(mailbox::Column::ExpiresAt.gt(now_naive))
        .filter(mailbox::Column::ExpiresAt.lte(now_naive + chrono::Duration::hours(24)))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let total_emails = email::Entity::find()
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let today_emails = email::Entity::find()
        .filter(email::Column::ReceivedAt.gte(today_start))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let quarantined = email::Entity::find()
        .filter(email::Column::Status.eq("quarantined"))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let today_quarantined = email::Entity::find()
        .filter(email::Column::Status.eq("quarantined"))
        .filter(email::Column::ReceivedAt.gte(today_start))
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let storage_bytes: i64 = email::Entity::find()
        .select_only()
        .column(email::Column::SizeBytes)
        .into_tuple::<i64>()
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .sum();
    let active_senders = blocked_sender::Entity::find()
        .filter(
            Condition::any()
                .add(blocked_sender::Column::ExpiresAt.is_null())
                .add(blocked_sender::Column::ExpiresAt.gt(now_naive)),
        )
        .count(&state.pool)
        .await
        .map_err(internal)?;
    let active_ips = blocked_ip::Entity::find()
        .filter(
            Condition::any()
                .add(blocked_ip::Column::ExpiresAt.is_null())
                .add(blocked_ip::Column::ExpiresAt.gt(now_naive)),
        )
        .count(&state.pool)
        .await
        .map_err(internal)?;

    let compact_emails = email::Entity::find()
        .select_only()
        .columns([
            email::Column::ReceivedAt,
            email::Column::Status,
            email::Column::Recipient,
            email::Column::MailFrom,
            email::Column::SourceIp,
            email::Column::SpfResult,
            email::Column::DkimResult,
            email::Column::DmarcResult,
            email::Column::AbuseScore,
        ])
        .filter(email::Column::ReceivedAt.gte(since))
        .into_tuple::<(
            NaiveDateTime,
            String,
            String,
            String,
            String,
            Option<String>,
            Option<String>,
            Option<String>,
            i32,
        )>()
        .all(&state.pool)
        .await
        .map_err(internal)?;
    let rejected_events = audit_log::Entity::find()
        .select_only()
        .columns([audit_log::Column::CreatedAt, audit_log::Column::Action])
        .filter(audit_log::Column::CreatedAt.gte(since))
        .filter(audit_log::Column::Action.eq("reject"))
        .into_tuple::<(NaiveDateTime, String)>()
        .all(&state.pool)
        .await
        .map_err(internal)?;

    let bucket = |value: NaiveDateTime| {
        let local = chrono_tz::Asia::Shanghai.from_utc_datetime(&value);
        if range == "24h" {
            local.format("%m-%d %H:00").to_string()
        } else {
            local.format("%m-%d").to_string()
        }
    };
    let mut received_trend = BTreeMap::new();
    let mut quarantined_trend = BTreeMap::new();
    let mut rejected_trend = BTreeMap::new();
    let mut spf = BTreeMap::new();
    let mut dkim = BTreeMap::new();
    let mut dmarc = BTreeMap::new();
    let mut risk = BTreeMap::new();
    let mut recipients = HashMap::new();
    let mut domains = HashMap::new();
    let mut source_ips = HashMap::new();
    for (
        received_at,
        status,
        recipient,
        mail_from,
        source_ip,
        spf_result,
        dkim_result,
        dmarc_result,
        abuse_score,
    ) in compact_emails
    {
        if status == "quarantined" {
            increment(&mut quarantined_trend, bucket(received_at));
        } else {
            increment(&mut received_trend, bucket(received_at));
        }
        increment(&mut spf, spf_result.unwrap_or_else(|| "unknown".into()));
        increment(&mut dkim, dkim_result.unwrap_or_else(|| "unknown".into()));
        increment(&mut dmarc, dmarc_result.unwrap_or_else(|| "unknown".into()));
        increment(
            &mut risk,
            if abuse_score >= 70 {
                "high"
            } else if abuse_score >= 30 {
                "medium"
            } else {
                "low"
            },
        );
        *recipients.entry(recipient).or_default() += 1;
        *source_ips.entry(source_ip).or_default() += 1;
        if let Some(domain) = sender_domain(&mail_from) {
            *domains.entry(domain).or_default() += 1;
        }
    }
    for (created_at, _) in rejected_events {
        increment(&mut rejected_trend, bucket(created_at));
    }
    let mut trend_keys: Vec<_> = received_trend
        .keys()
        .chain(quarantined_trend.keys())
        .chain(rejected_trend.keys())
        .cloned()
        .collect();
    trend_keys.sort();
    trend_keys.dedup();
    let trend: Vec<_> = trend_keys
        .into_iter()
        .map(|label| {
            json!({
                "label": label,
                "received": received_trend.get(&label).copied().unwrap_or(0),
                "quarantined": quarantined_trend.get(&label).copied().unwrap_or(0),
                "rejected": rejected_trend.get(&label).copied().unwrap_or(0),
            })
        })
        .collect();

    let mut risk_queue = email::Entity::find()
        .filter(email::Column::Status.eq("quarantined"))
        .order_by_desc(email::Column::ReceivedAt)
        .limit(6)
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .map(|item| {
            json!({
                "kind": "quarantine",
                "id": item.id,
                "title": item.subject.unwrap_or_else(|| "(无主题)".into()),
                "detail": format!("{} · 风险分 {}", item.mail_from, item.abuse_score),
                "created_at": iso(item.received_at),
            })
        })
        .collect::<Vec<_>>();
    let risk_events = audit_log::Entity::find()
        .filter(
            Condition::any()
                .add(audit_log::Column::EventType.eq("automatic_block"))
                .add(audit_log::Column::Action.eq("reject")),
        )
        .order_by_desc(audit_log::Column::CreatedAt)
        .limit(6)
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .map(|item| {
            json!({
                "kind": item.event_type,
                "id": item.id,
                "title": item.action,
                "detail": item.reason.unwrap_or_else(|| item.source_ip.unwrap_or_else(|| "无补充信息".into())),
                "created_at": iso(item.created_at),
            })
        });
    risk_queue.extend(risk_events);
    risk_queue.sort_by(|left, right| {
        right["created_at"]
            .as_str()
            .cmp(&left["created_at"].as_str())
    });
    risk_queue.truncate(8);
    let recent_actions = audit_log::Entity::find()
        .filter(audit_log::Column::OperatorId.is_not_null())
        .order_by_desc(audit_log::Column::CreatedAt)
        .limit(8)
        .all(&state.pool)
        .await
        .map_err(internal)?
        .into_iter()
        .map(|item| {
            json!({
                "id": item.id,
                "event_type": item.event_type,
                "action": item.action,
                "operator_id": item.operator_id,
                "target_type": item.target_type,
                "target_id": item.target_id,
                "reason": item.reason,
                "created_at": iso(item.created_at),
            })
        })
        .collect::<Vec<_>>();

    let database_health = state.pool.ping().await.is_ok();
    let (redis_status, queue_depth) = if !state.config.redis.enabled {
        ("disabled", 0usize)
    } else if let Some(redis) = &state.redis_client {
        let status = if redis.ping().await.is_ok() {
            "healthy"
        } else {
            "fault"
        };
        let depth = redis
            .queue_len(crate::infra::mail::worker::MAIL_QUEUE)
            .await
            .unwrap_or_default();
        (status, depth)
    } else {
        ("fault", 0)
    };

    let runtime = state.runtime_health.snapshot().await;
    let generated_at = iso(now_naive);
    Ok(Json(ApiResponse::success(json!({
        "range": range,
        "generated_at": generated_at,
        "metrics": {
            "total_users": total_users,
            "today_users": today_users,
            "active_mailboxes": active_mailboxes,
            "expiring_mailboxes": expiring_mailboxes,
            "today_emails": today_emails,
            "total_emails": total_emails,
            "quarantined": quarantined,
            "today_quarantine_rate": if today_emails == 0 { 0.0 } else { today_quarantined as f64 / today_emails as f64 },
            "storage_bytes": storage_bytes,
            "active_blocks": active_senders + active_ips,
        },
        "trend": trend,
        "security": {
            "spf": spf,
            "dkim": dkim,
            "dmarc": dmarc,
            "risk": risk,
        },
        "health": [
            {"name": "数据库", "status": if database_health { "healthy" } else { "fault" }, "detail": null},
            {"name": "Redis", "status": redis_status, "detail": null},
            {"name": "SMTP 监听", "status": runtime.smtp_status, "detail": runtime.smtp_detail},
            {"name": "外发邮件", "status": runtime.outbound_status, "detail": null},
            {"name": "外发队列", "status": if state.config.email.queue_enabled { redis_status } else { "disabled" }, "detail": format!("待处理 {}", queue_depth)},
            {
                "name": "清理任务",
                "status": runtime.cleanup_status,
                "detail": runtime.cleanup_detail,
                "last_success_at": runtime.cleanup_last_success.map(iso),
                "last_failure_at": runtime.cleanup_last_failure.map(iso),
            },
        ],
        "risk_queue": risk_queue,
        "top": {
            "recipients": top_values(recipients, 6),
            "sender_domains": top_values(domains, 6),
            "source_ips": top_values(source_ips, 6),
        },
        "recent_actions": recent_actions,
    }))))
}

fn internal(error: impl ToString) -> ErrorResponse {
    ErrorResponse::internal(error.to_string())
}
