//! 临时邮箱地址业务：创建 / 列表 / 吊销 / 轮换 token。

use crate::config::smtp::SmtpConfig;
use crate::domain::dto::mailbox::CreateMailboxRequest;
use crate::domain::entities::{credit_rule, mailbox};
use crate::domain::vo::mailbox::{CreateMailboxResult, MailboxVO};
use crate::repositories::mailbox_repository::MailboxRepository;
use crate::repositories::quota_repository::QuotaRepository;
use crate::services::credit_service::CreditService;
use crate::utils::mailbox_token;
use anyhow::Result;
use sea_orm::{
    ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait,
    PaginatorTrait, QueryFilter, Set, TransactionTrait,
};

pub struct MailboxService {
    db: DatabaseConnection,
    repo: MailboxRepository,
    smtp_config: SmtpConfig,
}

impl MailboxService {
    pub fn new(db: DatabaseConnection, smtp_config: SmtpConfig) -> Self {
        Self {
            repo: MailboxRepository::new(db.clone()),
            db,
            smtp_config,
        }
    }

    /// 创建邮箱、占用每日额度并扣减积分，任何一步失败都会整体回滚。
    pub async fn create_with_billing(
        &self,
        user_id: &str,
        req: CreateMailboxRequest,
        credit_rule: &credit_rule::Model,
    ) -> Result<CreateMailboxResult> {
        let txn = self.db.begin().await?;
        let operation = async {
            QuotaRepository::reserve_mailbox(&txn, user_id, credit_rule.daily_mailboxes_limit)
                .await?;

            let result = self.create_in(&txn, user_id, req).await?;
            CreditService::new(self.db.clone())
                .debit_in_transaction(
                    &txn,
                    user_id,
                    credit_rule.create_mailbox_cost,
                    "create_mailbox",
                    Some(result.mailbox.id),
                    None,
                )
                .await?;
            Ok::<_, anyhow::Error>(result)
        }
        .await;

        match operation {
            Ok(result) => {
                txn.commit().await?;
                Ok(result)
            }
            Err(error) => {
                if let Err(rollback_error) = txn.rollback().await {
                    tracing::error!(%rollback_error, "创建邮箱事务回滚失败");
                }
                Err(error)
            }
        }
    }

    async fn create_in<C>(
        &self,
        db: &C,
        user_id: &str,
        req: CreateMailboxRequest,
    ) -> Result<CreateMailboxResult>
    where
        C: ConnectionTrait,
    {
        let domain = req
            .domain
            .clone()
            .unwrap_or_else(|| {
                self.smtp_config
                    .local_domains
                    .first()
                    .cloned()
                    .unwrap_or_default()
            })
            .to_lowercase();
        if domain.is_empty()
            || !self
                .smtp_config
                .local_domains
                .iter()
                .any(|d| d.eq_ignore_ascii_case(&domain))
        {
            anyhow::bail!("不支持的域名");
        }

        let local_part = match req.local_part.clone() {
            Some(lp) => {
                let lp = lp.to_lowercase();
                if !is_valid_local_part(&lp) {
                    anyhow::bail!("local part 仅允许小写字母、数字与 . _ -");
                }
                lp
            }
            None => self.generate_unique_local_part(db, &domain).await?,
        };
        let address = format!("{local_part}@{domain}").to_lowercase();
        if mailbox::Entity::find()
            .filter(mailbox::Column::Address.eq(&address))
            .count(db)
            .await?
            > 0
        {
            anyhow::bail!("地址已存在");
        }

        let now = chrono::Utc::now().naive_utc();
        let ttl_hours = req
            .ttl_hours
            .unwrap_or(self.smtp_config.mailbox_ttl_hours)
            .max(1);
        let expires_at = now + chrono::Duration::hours(ttl_hours);

        // 先用 placeholder hash 插入拿到自增 id，再写入真实 token 的 hash
        let placeholder = uuid::Uuid::new_v4().to_string();
        let model = mailbox::ActiveModel {
            user_id: Set(user_id.into()),
            address: Set(address),
            local_part: Set(local_part),
            domain: Set(domain),
            access_token_hash: Set(mailbox_token::hash_token(&placeholder)),
            note: Set(req.note),
            status: Set("active".into()),
            max_quota_bytes: Set(self.smtp_config.mailbox_max_quota_bytes),
            used_bytes: Set(0),
            expires_at: Set(expires_at),
            ..Default::default()
        };
        let m = model.insert(db).await?;

        let secret = mailbox_token::generate_secret();
        let token = mailbox_token::build_token(m.id, &secret);
        let mut active: mailbox::ActiveModel = m.clone().into();
        active.access_token_hash = Set(mailbox_token::hash_token(&secret));
        active.update(db).await?;

        Ok(CreateMailboxResult {
            mailbox: MailboxVO::from(m),
            access_token: token,
        })
    }

    pub async fn list_by_user(&self, user_id: &str) -> Result<Vec<MailboxVO>> {
        let list = self.repo.list_by_user(user_id).await?;
        Ok(list.into_iter().map(MailboxVO::from).collect())
    }

    pub async fn revoke(&self, user_id: &str, id: i64) -> Result<()> {
        let m = self
            .repo
            .find_by_id(id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮箱不存在"))?;
        if m.user_id != user_id {
            anyhow::bail!("无权操作该邮箱");
        }
        self.repo.revoke(id).await
    }

    pub async fn rotate_token(&self, user_id: &str, id: i64) -> Result<String> {
        let m = self
            .repo
            .find_by_id(id)
            .await?
            .ok_or_else(|| anyhow::anyhow!("邮箱不存在"))?;
        if m.user_id != user_id {
            anyhow::bail!("无权操作该邮箱");
        }
        let secret = mailbox_token::generate_secret();
        let token = mailbox_token::build_token(id, &secret);
        self.repo
            .update_token_hash(id, mailbox_token::hash_token(&secret))
            .await?;
        Ok(token)
    }

    async fn generate_unique_local_part<C>(&self, db: &C, domain: &str) -> Result<String>
    where
        C: ConnectionTrait,
    {
        let len = self.smtp_config.address_local_part_len;
        for _ in 0..10 {
            let candidate = random_local_part(len);
            let addr = format!("{candidate}@{domain}").to_lowercase();
            if mailbox::Entity::find()
                .filter(mailbox::Column::Address.eq(&addr))
                .count(db)
                .await?
                == 0
            {
                return Ok(candidate);
            }
        }
        anyhow::bail!("生成唯一地址失败，请重试");
    }
}

fn is_valid_local_part(s: &str) -> bool {
    !s.is_empty()
        && s.len() <= 64
        && s.bytes().all(|b| {
            b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
        })
}

fn random_local_part(len: usize) -> String {
    use rand::Rng;
    const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
    let mut rng = rand::thread_rng();
    (0..len)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len());
            CHARSET[idx] as char
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::database::{DatabaseConfig, DatabaseType};
    use crate::repositories::credit_repository::CreditRepository;

    async fn test_database() -> DatabaseConnection {
        let path = std::env::temp_dir().join(format!(
            "email-unlimit-mailbox-{}.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 insufficient_credit_rolls_back_mailbox_and_quota() {
        let db = test_database().await;
        CreditRepository::new(db.clone())
            .create_account("user-rollback".into(), 0, 0)
            .await
            .unwrap();
        let config =
            crate::config::app::AppConfig::load_from_path("config/development.toml").unwrap();
        let service = MailboxService::new(db.clone(), config.smtp);
        let mut credit =
            crate::services::credit_rule_service::CreditRuleService::ensure_defaults(&db)
                .await
                .unwrap();
        credit.create_mailbox_cost = 1;
        credit.daily_mailboxes_limit = 5;

        let result = service
            .create_with_billing(
                "user-rollback",
                CreateMailboxRequest {
                    local_part: Some("rollback-check".into()),
                    domain: None,
                    note: None,
                    ttl_hours: None,
                },
                &credit,
            )
            .await;

        assert!(result.is_err());
        assert!(MailboxRepository::new(db.clone())
            .list_by_user("user-rollback")
            .await
            .unwrap()
            .is_empty());
        assert!(QuotaRepository::new(db)
            .get_today("user-rollback")
            .await
            .unwrap()
            .is_none());
    }
}
