//! 邮件查询业务：列表 / 详情 / 删除 / 附件。
//! 所有方法均校验归属，防越权访问。

use crate::domain::{
    entities::{email, email_attachment, mailbox},
    vo::email::{AttachmentSummary, EmailDetail, EmailSummary},
};
use crate::repositories::{
    attachment_repository::AttachmentRepository, email_repository::EmailRepository,
};
use anyhow::Result;
use sea_orm::{
    sea_query::Expr, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, TransactionTrait,
};

pub struct EmailService {
    db: DatabaseConnection,
    email_repo: EmailRepository,
    att_repo: AttachmentRepository,
    enc_key: Option<[u8; 32]>,
}

impl EmailService {
    pub fn new(db: DatabaseConnection, smtp_config: crate::config::smtp::SmtpConfig) -> Self {
        let enc_key = crate::utils::crypto::derive_key(&smtp_config.body_encryption_key);
        Self {
            db: db.clone(),
            email_repo: EmailRepository::new(db.clone()),
            att_repo: AttachmentRepository::new(db),
            enc_key,
        }
    }

    pub async fn list_by_mailbox(
        &self,
        mailbox_id: i64,
        limit: u64,
        offset: u64,
    ) -> Result<Vec<EmailSummary>> {
        let list = self
            .email_repo
            .list_by_mailbox(mailbox_id, limit, offset)
            .await?;
        Ok(list.into_iter().map(EmailSummary::from).collect())
    }

    pub async fn get_detail(&self, mailbox_id: i64, email_id: i64) -> Result<EmailDetail> {
        let e = self
            .email_repo
            .find_by_id(email_id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
        if e.mailbox_id != mailbox_id {
            anyhow::bail!("邮件不属于该邮箱");
        }
        if e.status != "received" {
            anyhow::bail!("邮件不存在");
        }
        let atts = self.att_repo.list_by_email(email_id).await?;
        Ok(EmailDetail {
            id: e.id,
            mail_from: e.mail_from,
            sender_name: e.sender_name,
            subject: e.subject,
            text_body: crate::utils::crypto::decrypt_optional(&self.enc_key, e.text_body),
            html_body: crate::utils::crypto::decrypt_optional(&self.enc_key, e.html_body),
            size_bytes: e.size_bytes,
            status: e.status,
            received_at: e.received_at.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
            attachments: atts.into_iter().map(AttachmentSummary::from).collect(),
        })
    }

    pub async fn delete(&self, mailbox_id: i64, email_id: i64) -> Result<()> {
        let txn = self.db.begin().await?;
        let e = email::Entity::find_by_id(email_id)
            .one(&txn)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
        if e.mailbox_id != mailbox_id {
            anyhow::bail!("邮件不属于该邮箱");
        }
        if e.status != "received" {
            anyhow::bail!("邮件不存在");
        }

        email_attachment::Entity::delete_many()
            .filter(email_attachment::Column::EmailId.eq(email_id))
            .exec(&txn)
            .await?;
        email::Entity::delete_by_id(email_id).exec(&txn).await?;

        let updated = mailbox::Entity::update_many()
            .col_expr(
                mailbox::Column::UsedBytes,
                Expr::col(mailbox::Column::UsedBytes).sub(e.size_bytes),
            )
            .filter(mailbox::Column::Id.eq(mailbox_id))
            .filter(mailbox::Column::UsedBytes.gte(e.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(mailbox_id))
                .exec(&txn)
                .await?;
        }
        txn.commit().await?;
        Ok(())
    }

    pub async fn get_attachment(
        &self,
        mailbox_id: i64,
        email_id: i64,
        att_id: i64,
    ) -> Result<(Option<String>, String, Vec<u8>)> {
        let e = self
            .email_repo
            .find_by_id(email_id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮件不存在"))?;
        if e.mailbox_id != mailbox_id {
            anyhow::bail!("邮件不属于该邮箱");
        }
        if e.status != "received" {
            anyhow::bail!("邮件不存在");
        }
        let a = self
            .att_repo
            .find_by_id(att_id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("附件不存在"))?;
        if a.email_id != email_id {
            anyhow::bail!("附件不属于该邮件");
        }
        Ok((a.filename, a.content_type, a.content.unwrap_or_default()))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::database::{DatabaseConfig, DatabaseType};
    use crate::repositories::user_repository::UserRepository;
    use sea_orm::{ActiveModelTrait, Set};

    async fn test_database() -> DatabaseConnection {
        let path = std::env::temp_dir().join(format!(
            "email-unlimit-email-{}.sqlite",
            uuid::Uuid::new_v4()
        ));
        crate::db::init_database(&DatabaseConfig {
            database_type: DatabaseType::SQLite,
            host: None,
            port: None,
            user: None,
            password: None,
            database: None,
            path: Some(path),
            max_connections: 4,
        })
        .await
        .unwrap()
    }

    #[tokio::test]
    async fn deleting_email_releases_mailbox_storage() {
        let db = test_database().await;
        UserRepository::insert_with(
            &db,
            "email-owner".into(),
            "owner@example.com".into(),
            "unused-password-hash".into(),
            "user",
        )
        .await
        .unwrap();
        let now = chrono::Utc::now().naive_utc();
        let mailbox = mailbox::ActiveModel {
            user_id: Set("email-owner".into()),
            address: Set("storage@mail.shenjianl.cn".into()),
            local_part: Set("storage".into()),
            domain: Set("mail.shenjianl.cn".into()),
            access_token_hash: Set("unused".into()),
            note: Set(None),
            status: Set("active".into()),
            max_quota_bytes: Set(1_000),
            used_bytes: Set(500),
            expires_at: Set(now + chrono::Duration::hours(1)),
            ..Default::default()
        }
        .insert(&db)
        .await
        .unwrap();
        let email = email::ActiveModel {
            mailbox_id: Set(mailbox.id),
            user_id: Set("email-owner".into()),
            recipient: Set(mailbox.address.clone()),
            mail_from: Set("sender@example.com".into()),
            rcpt_to: Set(mailbox.address.clone()),
            sender_name: Set(None),
            subject: Set(Some("storage test".into())),
            text_body: Set(Some("body".into())),
            html_body: Set(None),
            source_ip: Set("127.0.0.1".into()),
            helo: Set(Some("localhost".into())),
            ptr_result: Set(None),
            spf_result: Set(None),
            dkim_result: Set(None),
            dmarc_result: Set(None),
            abuse_score: Set(0),
            status: Set("received".into()),
            size_bytes: Set(400),
            message_id: Set(None),
            expires_at: Set(now + chrono::Duration::days(1)),
            ..Default::default()
        }
        .insert(&db)
        .await
        .unwrap();
        let quarantined = email::ActiveModel {
            mailbox_id: Set(mailbox.id),
            user_id: Set("email-owner".into()),
            recipient: Set(mailbox.address.clone()),
            mail_from: Set("blocked@example.com".into()),
            rcpt_to: Set(mailbox.address.clone()),
            sender_name: Set(None),
            subject: Set(Some("quarantined".into())),
            text_body: Set(Some("hidden body".into())),
            html_body: Set(None),
            source_ip: Set("127.0.0.2".into()),
            helo: Set(Some("localhost".into())),
            ptr_result: Set(None),
            spf_result: Set(None),
            dkim_result: Set(None),
            dmarc_result: Set(None),
            abuse_score: Set(100),
            status: Set("quarantined".into()),
            size_bytes: Set(100),
            message_id: Set(None),
            expires_at: Set(now + chrono::Duration::days(1)),
            ..Default::default()
        }
        .insert(&db)
        .await
        .unwrap();

        let config =
            crate::config::app::AppConfig::load_from_path("config/development.toml").unwrap();
        let service = EmailService::new(db.clone(), config.smtp);
        let visible = service.list_by_mailbox(mailbox.id, 20, 0).await.unwrap();
        assert_eq!(visible.len(), 1);
        assert_eq!(visible[0].id, email.id);
        assert!(service
            .get_detail(mailbox.id, quarantined.id)
            .await
            .is_err());
        assert!(service.delete(mailbox.id, quarantined.id).await.is_err());

        service.delete(mailbox.id, email.id).await.unwrap();

        let mailbox = mailbox::Entity::find_by_id(mailbox.id)
            .one(&db)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(mailbox.used_bytes, 100);
        assert!(email::Entity::find_by_id(email.id)
            .one(&db)
            .await
            .unwrap()
            .is_none());
        assert!(email::Entity::find_by_id(quarantined.id)
            .one(&db)
            .await
            .unwrap()
            .is_some());
    }
}
