use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;

#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Wallet {
    pub id: Uuid,
    pub user_id: Uuid,
    pub balance: i32,
    pub reserved: i32,
    pub updated_at: DateTime<Utc>,
}

#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LedgerEntry {
    pub id: Uuid,
    pub wallet_id: Uuid,
    pub transaction_type: String,
    pub amount: i32,
    pub reference_type: String,
    pub reference_id: Option<Uuid>,
    pub balance_after: Option<i32>,
    pub remarks: Option<String>,
    pub created_at: DateTime<Utc>,
}

pub struct TracecoinWalletRepository;

impl TracecoinWalletRepository {
    pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Wallet, sqlx::Error> {
        sqlx::query_as::<_, Wallet>(
            "SELECT * FROM tracecoin_wallets WHERE user_id = $1",
        )
        .bind(user_id)
        .fetch_one(pool)
        .await
    }

    pub async fn ensure_wallet(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
        sqlx::query(
            r#"
            INSERT INTO tracecoin_wallets (user_id, balance, reserved)
            VALUES ($1, 0, 0)
            ON CONFLICT (user_id) DO NOTHING
            "#,
        )
        .bind(user_id)
        .execute(pool)
        .await?;
        Ok(())
    }

    pub async fn try_reserve_tracecoins(
        pool: &PgPool,
        user_id: Uuid,
        amount: i32,
        reference_id: Uuid,
    ) -> Result<bool, sqlx::Error> {
        let mut tx = pool.begin().await?;

        sqlx::query(
            r#"
            INSERT INTO tracecoin_wallets (user_id, balance, reserved)
            VALUES ($1, 0, 0)
            ON CONFLICT (user_id) DO NOTHING
            "#,
        )
        .bind(user_id)
        .execute(&mut *tx)
        .await?;

        let wallet = sqlx::query_as::<_, Wallet>(
            "SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
        )
        .bind(user_id)
        .fetch_one(&mut *tx)
        .await?;

        if wallet.balance < amount {
            tx.rollback().await?;
            return Ok(false);
        }

        sqlx::query(
            r#"
            UPDATE tracecoin_wallets
            SET balance = balance - $1, reserved = reserved + $1, updated_at = NOW()
            WHERE id = $2
            "#,
        )
        .bind(amount)
        .bind(wallet.id)
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            r#"
            INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
            VALUES ($1, 'RESERVE', $2, 'LEAD_REQUEST', $3)
            "#,
        )
        .bind(wallet.id)
        .bind(amount)
        .bind(reference_id)
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(true)
    }

    pub async fn try_debit_reserved_tracecoins(
        pool: &PgPool,
        user_id: Uuid,
        amount: i32,
        reference_id: Uuid,
    ) -> Result<bool, sqlx::Error> {
        let mut tx = pool.begin().await?;

        let wallet = sqlx::query_as::<_, Wallet>(
            "SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
        )
        .bind(user_id)
        .fetch_optional(&mut *tx)
        .await?;

        let Some(wallet) = wallet else {
            tx.rollback().await?;
            return Ok(false);
        };

        if wallet.reserved < amount {
            tx.rollback().await?;
            return Ok(false);
        }

        sqlx::query(
            r#"
            UPDATE tracecoin_wallets
            SET reserved = reserved - $1, updated_at = NOW()
            WHERE id = $2
            "#,
        )
        .bind(amount)
        .bind(wallet.id)
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            r#"
            INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
            VALUES ($1, 'DEBIT', $2, 'LEAD_ACCEPTED', $3)
            "#,
        )
        .bind(wallet.id)
        .bind(amount)
        .bind(reference_id)
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(true)
    }

    pub async fn try_release_reserved_tracecoins(
        pool: &PgPool,
        user_id: Uuid,
        amount: i32,
        reference_id: Uuid,
        reason: &str,
    ) -> Result<bool, sqlx::Error> {
        let mut tx = pool.begin().await?;

        let wallet = sqlx::query_as::<_, Wallet>(
            "SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
        )
        .bind(user_id)
        .fetch_optional(&mut *tx)
        .await?;

        let Some(wallet) = wallet else {
            tx.rollback().await?;
            return Ok(false);
        };

        if wallet.reserved < amount {
            tx.rollback().await?;
            return Ok(false);
        }

        sqlx::query(
            r#"
            UPDATE tracecoin_wallets
            SET reserved = reserved - $1, balance = balance + $1, updated_at = NOW()
            WHERE id = $2
            "#,
        )
        .bind(amount)
        .bind(wallet.id)
        .execute(&mut *tx)
        .await?;

        sqlx::query(
            r#"
            INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
            VALUES ($1, 'RELEASE', $2, $3, $4)
            "#,
        )
        .bind(wallet.id)
        .bind(amount)
        .bind(reason)
        .bind(reference_id)
        .execute(&mut *tx)
        .await?;

        tx.commit().await?;
        Ok(true)
    }
}
