//! 积分账户、流水、签到和原子扣减。

use crate::domain::entities::{credit_account, credit_check_in, credit_transaction, users};
use crate::repositories::{credit_repository::CreditRepository, quota_repository::QuotaRepository};
use anyhow::Result;
use sea_orm::{
    sea_query::Expr, ActiveModelTrait, ColumnTrait, DatabaseConnection, DatabaseTransaction,
    EntityTrait, QueryFilter, Set, TransactionTrait,
};
use serde::Serialize;

#[derive(Debug, Serialize)]
pub struct CheckInResult {
    pub claimed: bool,
    pub reward_granted: i64,
    pub balance: i64,
    pub check_in_date: chrono::NaiveDate,
}

pub struct CreditService {
    db: DatabaseConnection,
    repo: CreditRepository,
}

impl CreditService {
    pub fn new(db: DatabaseConnection) -> Self {
        let repo = CreditRepository::new(db.clone());
        Self { db, repo }
    }

    pub async fn get_account(&self, user_id: &str) -> Result<Option<credit_account::Model>> {
        self.repo.find_account(user_id).await
    }

    pub async fn backfill_missing_accounts(
        db: &DatabaseConnection,
        register_bonus: i64,
    ) -> Result<u64> {
        let users = users::Entity::find()
            .filter(users::Column::DeletedAt.is_null())
            .all(db)
            .await?;
        let mut created = 0;

        for user in users {
            if credit_account::Entity::find()
                .filter(credit_account::Column::UserId.eq(&user.id))
                .one(db)
                .await?
                .is_some()
            {
                continue;
            }

            let txn = db.begin().await?;
            let operation = async {
                credit_account::ActiveModel {
                    user_id: Set(user.id.clone()),
                    balance: Set(register_bonus),
                    version: Set(0),
                    total_granted: Set(register_bonus),
                    total_consumed: Set(0),
                    ..Default::default()
                }
                .insert(&txn)
                .await?;
                Self::insert_transaction(
                    &txn,
                    &user.id,
                    register_bonus,
                    register_bonus,
                    "migration_bonus",
                    None,
                    None,
                    None,
                    None,
                )
                .await?;
                Ok::<_, sea_orm::DbErr>(())
            }
            .await;

            match operation {
                Ok(()) => {
                    txn.commit().await?;
                    created += 1;
                }
                Err(error) => {
                    txn.rollback().await?;
                    if credit_account::Entity::find()
                        .filter(credit_account::Column::UserId.eq(&user.id))
                        .one(db)
                        .await?
                        .is_none()
                    {
                        return Err(error.into());
                    }
                }
            }
        }

        Ok(created)
    }

    pub async fn get_balance(&self, user_id: &str) -> Result<Option<i64>> {
        Ok(self
            .get_account(user_id)
            .await?
            .map(|account| account.balance))
    }

    pub async fn checked_in_today(&self, user_id: &str) -> Result<Option<credit_check_in::Model>> {
        Ok(
            credit_check_in::Entity::find_by_id((user_id.to_string(), QuotaRepository::today()))
                .one(&self.db)
                .await?,
        )
    }

    pub async fn check_in(
        &self,
        user_id: &str,
        reward: i64,
        reward_balance_cap: i64,
    ) -> Result<CheckInResult> {
        let date = QuotaRepository::today();
        if let Some(existing) = self.checked_in_today(user_id).await? {
            let balance = self
                .get_balance(user_id)
                .await?
                .ok_or_else(|| anyhow::anyhow!("无积分账户"))?;
            return Ok(CheckInResult {
                claimed: false,
                reward_granted: existing.reward_granted,
                balance,
                check_in_date: date,
            });
        }

        let txn = self.db.begin().await?;
        let operation = async {
            let account = credit_account::Entity::find_by_id(user_id)
                .one(&txn)
                .await?
                .ok_or_else(|| anyhow::anyhow!("无积分账户"))?;
            let available = (reward_balance_cap - account.balance).max(0);
            let granted = reward.max(0).min(available);

            credit_check_in::ActiveModel {
                user_id: Set(user_id.into()),
                date: Set(date),
                reward_granted: Set(granted),
                ..Default::default()
            }
            .insert(&txn)
            .await?;

            let balance = if granted > 0 {
                self.grant_in_transaction(&txn, user_id, granted, "daily_check_in", None, None)
                    .await?
            } else {
                account.balance
            };
            Ok::<_, anyhow::Error>((granted, balance))
        }
        .await;

        match operation {
            Ok((reward_granted, balance)) => {
                txn.commit().await?;
                Ok(CheckInResult {
                    claimed: true,
                    reward_granted,
                    balance,
                    check_in_date: date,
                })
            }
            Err(error) => {
                txn.rollback().await?;
                if let Some(existing) = self.checked_in_today(user_id).await? {
                    let balance = self
                        .get_balance(user_id)
                        .await?
                        .ok_or_else(|| anyhow::anyhow!("无积分账户"))?;
                    return Ok(CheckInResult {
                        claimed: false,
                        reward_granted: existing.reward_granted,
                        balance,
                        check_in_date: date,
                    });
                }
                Err(error)
            }
        }
    }

    pub async fn debit_in_transaction(
        &self,
        txn: &DatabaseTransaction,
        user_id: &str,
        amount: i64,
        reason: &str,
        related_mailbox_id: Option<i64>,
        related_email_id: Option<i64>,
    ) -> Result<i64> {
        self.debit_with_metadata(
            txn,
            user_id,
            amount,
            reason,
            related_mailbox_id,
            related_email_id,
            None,
            None,
        )
        .await
    }

    #[allow(clippy::too_many_arguments)]
    async fn debit_with_metadata(
        &self,
        txn: &DatabaseTransaction,
        user_id: &str,
        amount: i64,
        reason: &str,
        related_mailbox_id: Option<i64>,
        related_email_id: Option<i64>,
        description: Option<String>,
        operator_id: Option<String>,
    ) -> Result<i64> {
        if amount <= 0 {
            anyhow::bail!("积分扣减必须大于 0");
        }
        let account = credit_account::Entity::find_by_id(user_id)
            .one(txn)
            .await?
            .ok_or_else(|| anyhow::anyhow!("无积分账户"))?;
        if account.balance < amount {
            anyhow::bail!("积分不足");
        }
        let new_balance = account.balance - amount;
        let updated = credit_account::Entity::update_many()
            .col_expr(credit_account::Column::Balance, Expr::value(new_balance))
            .col_expr(
                credit_account::Column::TotalConsumed,
                Expr::col(credit_account::Column::TotalConsumed).add(amount),
            )
            .col_expr(
                credit_account::Column::Version,
                Expr::value(account.version + 1),
            )
            .filter(credit_account::Column::UserId.eq(user_id))
            .filter(credit_account::Column::Version.eq(account.version))
            .exec(txn)
            .await?;
        if updated.rows_affected != 1 {
            anyhow::bail!("积分扣减并发冲突，请重试");
        }
        Self::insert_transaction(
            txn,
            user_id,
            -amount,
            new_balance,
            reason,
            related_mailbox_id,
            related_email_id,
            description,
            operator_id,
        )
        .await?;
        Ok(new_balance)
    }

    async fn grant_in_transaction(
        &self,
        txn: &DatabaseTransaction,
        user_id: &str,
        amount: i64,
        reason: &str,
        description: Option<String>,
        operator_id: Option<String>,
    ) -> Result<i64> {
        if amount <= 0 {
            anyhow::bail!("积分增加必须大于 0");
        }
        let account = credit_account::Entity::find_by_id(user_id)
            .one(txn)
            .await?
            .ok_or_else(|| anyhow::anyhow!("无积分账户"))?;
        let new_balance = account
            .balance
            .checked_add(amount)
            .ok_or_else(|| anyhow::anyhow!("积分调整超出允许范围"))?;
        let updated = credit_account::Entity::update_many()
            .col_expr(credit_account::Column::Balance, Expr::value(new_balance))
            .col_expr(
                credit_account::Column::TotalGranted,
                Expr::col(credit_account::Column::TotalGranted).add(amount),
            )
            .col_expr(
                credit_account::Column::Version,
                Expr::value(account.version + 1),
            )
            .filter(credit_account::Column::UserId.eq(user_id))
            .filter(credit_account::Column::Version.eq(account.version))
            .exec(txn)
            .await?;
        if updated.rows_affected != 1 {
            anyhow::bail!("积分增加并发冲突，请重试");
        }
        Self::insert_transaction(
            txn,
            user_id,
            amount,
            new_balance,
            reason,
            None,
            None,
            description,
            operator_id,
        )
        .await?;
        Ok(new_balance)
    }

    pub async fn adjust(
        &self,
        user_id: &str,
        delta: i64,
        description: &str,
        operator_id: &str,
    ) -> Result<i64> {
        if delta == 0 {
            anyhow::bail!("积分调整值不能为 0");
        }
        let description = description.trim();
        if description.is_empty() {
            anyhow::bail!("调整原因不能为空");
        }
        let txn = self.db.begin().await?;
        let balance = if delta > 0 {
            self.grant_in_transaction(
                &txn,
                user_id,
                delta,
                "admin_adjust",
                Some(description.into()),
                Some(operator_id.into()),
            )
            .await?
        } else {
            self.debit_with_metadata(
                &txn,
                user_id,
                -delta,
                "admin_adjust",
                None,
                None,
                Some(description.into()),
                Some(operator_id.into()),
            )
            .await?
        };
        txn.commit().await?;
        Ok(balance)
    }

    #[allow(clippy::too_many_arguments)]
    async fn insert_transaction(
        txn: &DatabaseTransaction,
        user_id: &str,
        delta: i64,
        balance_after: i64,
        reason: &str,
        related_mailbox_id: Option<i64>,
        related_email_id: Option<i64>,
        description: Option<String>,
        operator_id: Option<String>,
    ) -> Result<credit_transaction::Model, sea_orm::DbErr> {
        credit_transaction::ActiveModel {
            user_id: Set(user_id.into()),
            delta: Set(delta),
            balance_after: Set(balance_after),
            reason: Set(reason.into()),
            related_mailbox_id: Set(related_mailbox_id),
            related_email_id: Set(related_email_id),
            description: Set(description),
            operator_id: Set(operator_id),
            ..Default::default()
        }
        .insert(txn)
        .await
    }

    pub async fn list_transactions(
        &self,
        user_id: &str,
        limit: u64,
        offset: u64,
    ) -> Result<Vec<credit_transaction::Model>> {
        self.repo.list_transactions(user_id, limit, offset).await
    }
}

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

    async fn test_database() -> DatabaseConnection {
        let path = std::env::temp_dir().join(format!(
            "email-unlimit-credit-{}.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 backfill_is_idempotent_and_audited() {
        let db = test_database().await;
        UserRepository::insert_with(
            &db,
            "legacy-user".into(),
            "legacy@example.com".into(),
            "unused-password-hash".into(),
            "user",
        )
        .await
        .unwrap();

        assert_eq!(
            CreditService::backfill_missing_accounts(&db, 30)
                .await
                .unwrap(),
            1
        );
        assert_eq!(
            CreditService::backfill_missing_accounts(&db, 30)
                .await
                .unwrap(),
            0
        );
        let account = credit_account::Entity::find_by_id("legacy-user")
            .one(&db)
            .await
            .unwrap()
            .unwrap();
        assert_eq!(account.balance, 30);
        assert_eq!(
            credit_transaction::Entity::find()
                .filter(credit_transaction::Column::UserId.eq("legacy-user"))
                .count(&db)
                .await
                .unwrap(),
            1
        );
    }

    #[tokio::test]
    async fn check_in_is_idempotent_and_respects_reward_cap() {
        let db = test_database().await;
        CreditRepository::new(db.clone())
            .create_account("check-in-user".into(), 89, 89)
            .await
            .unwrap();
        let svc = CreditService::new(db.clone());

        let first = svc.check_in("check-in-user", 3, 90).await.unwrap();
        assert!(first.claimed);
        assert_eq!(first.reward_granted, 1);
        assert_eq!(first.balance, 90);

        let repeated = svc.check_in("check-in-user", 3, 90).await.unwrap();
        assert!(!repeated.claimed);
        assert_eq!(repeated.balance, 90);
        assert_eq!(
            credit_transaction::Entity::find()
                .filter(credit_transaction::Column::UserId.eq("check-in-user"))
                .count(&db)
                .await
                .unwrap(),
            1
        );
    }
}
