//! Secure Tracecoin wallet operations.
//!
//! All wallet mutations flow through this module so the rules live in one
//! place. Every operation:
//!
//!   * Wraps the read + write in a single SQL transaction.
//!   * Uses `lock_tracecoin_wallet()` (SELECT ... FOR UPDATE) so concurrent
//!     callers cannot race the balance.
//!   * Re-checks invariants (`balance >= 0`, `reserved >= 0`) inside the
//!     transaction so the database CHECK constraints never fire.
//!   * Writes a single, atomic ledger entry per call. The `(wallet_id,
//!     reference_id, type)` unique index makes the operation idempotent: a
//!     retry of the same operation with the same reference_id is a no-op
//!     and returns the existing entry instead of double-crediting.
//!   * Stores `balance_after` and `actor_user_id` so the ledger is a
//!     self-contained, auditable history.
//!
//! Money flow (all balances are integer tracecoins; 1 INR = 1 tracecoin):
//!
//!   * `credit_for_payment`         – credit tracecoins after a successful
//!                                    payment (idempotent on payment id).
//!   * `admin_adjust`               – admin ADD/DEDUCT with required reason.
//!   * `reserve_for_lead_request`   – move tracecoins from `balance` to
//!                                    `reserved` so they cannot be spent
//!                                    twice. Fails if not enough balance.
//!   * `release_reservation`        – return reserved tracecoins to balance.
//!   * `confirm_reservation`        – finalize a reservation: deduct from
//!                                    `reserved` permanently (used when a
//!                                    lead is accepted).
//!   * `spend_direct`               – debit balance directly without
//!                                    reservation (used for one-off actions
//!                                    where reservation would be overkill).

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

pub mod hold;

/// What kind of ledger entry is being written.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum LedgerType {
    Credit,
    Debit,
    Reserve,
    Release,
    Adjustment,
}

impl LedgerType {
    pub fn as_str(self) -> &'static str {
        match self {
            LedgerType::Credit => "CREDIT",
            LedgerType::Debit => "DEBIT",
            LedgerType::Reserve => "RESERVE",
            LedgerType::Release => "RELEASE",
            LedgerType::Adjustment => "ADJUSTMENT",
        }
    }
}

/// Why a ledger entry was written. Free-form but a small set of
/// `snake_case` codes is the convention.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LedgerReason {
    Purchase,
    LeadRequestReserve,
    LeadRequestRelease,
    LeadRequestDeduct,
    LeadRequestExpire,
    AdminCredit,
    AdminDebuct,
    Refund,
    AiCredit,
    System,
}

impl LedgerReason {
    pub fn as_str(self) -> &'static str {
        match self {
            LedgerReason::Purchase => "PURCHASE",
            LedgerReason::LeadRequestReserve => "LEAD_REQUEST_RESERVE",
            LedgerReason::LeadRequestRelease => "LEAD_REQUEST_RELEASE",
            LedgerReason::LeadRequestDeduct => "LEAD_REQUEST_DEDUCT",
            LedgerReason::LeadRequestExpire => "LEAD_REQUEST_EXPIRE",
            LedgerReason::AdminCredit => "ADMIN_CREDIT",
            LedgerReason::AdminDebuct => "ADMIN_DEDUCT",
            LedgerReason::Refund => "REFUND",
            LedgerReason::AiCredit => "AI_CREDIT",
            LedgerReason::System => "SYSTEM",
        }
    }
}

/// Public, read-only view of a wallet.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct Wallet {
    pub user_id: Uuid,
    pub balance: i32,
    pub reserved: i32,
    pub available: i32,
    pub updated_at: DateTime<Utc>,
}

/// Public, read-only view of a single ledger entry.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct LedgerEntry {
    pub id: Uuid,
    pub wallet_id: Uuid,
    pub entry_type: String,
    pub amount: i32,
    pub balance_after: Option<i32>,
    pub reason: Option<String>,
    pub reference_id: Option<Uuid>,
    pub actor_user_id: Option<Uuid>,
    pub metadata: Option<serde_json::Value>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug)]
pub enum WalletError {
    /// Database error.
    Db(sqlx::Error),
    /// The caller asked for a credit/debit that would drive the balance
    /// below zero.
    InsufficientFunds {
        available: i32,
        requested: i32,
    },
    /// The caller passed an invalid amount (zero, negative, or above the
    /// safe limit).
    InvalidAmount(i32),
    /// The requested reservation would push `reserved` past `balance`.
    InsufficientUnreserved {
        balance: i32,
        reserved: i32,
        requested: i32,
    },
    /// The reservation has already been released/confirmed. Idempotent
    /// re-runs return `Ok(())` so callers can safely retry.
    AlreadyApplied,
}

impl std::fmt::Display for WalletError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            WalletError::Db(e) => write!(f, "db error: {e}"),
            WalletError::InsufficientFunds { available, requested } => {
                write!(f, "insufficient funds: available={available}, requested={requested}")
            }
            WalletError::InvalidAmount(a) => write!(f, "invalid amount: {a}"),
            WalletError::InsufficientUnreserved { balance, reserved, requested } => write!(
                f,
                "insufficient unreserved: balance={balance}, reserved={reserved}, requested={requested}"
            ),
            WalletError::AlreadyApplied => write!(f, "operation already applied"),
        }
    }
}

impl std::error::Error for WalletError {}

impl From<sqlx::Error> for WalletError {
    fn from(e: sqlx::Error) -> Self {
        WalletError::Db(e)
    }
}

/// Hard upper limit on any single wallet operation. Tracecoins never
/// legitimately need to be added in larger amounts than this.
pub const MAX_AMOUNT: i32 = 1_000_000_000;

/// Read a wallet, creating one if missing. Does NOT lock the row.
pub async fn get_or_create(
    pool: &PgPool,
    user_id: Uuid,
) -> Result<Wallet, WalletError> {
    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?;

    let row = sqlx::query(
        r#"
        SELECT
            user_id,
            balance,
            reserved,
            (balance - reserved) AS available,
            updated_at
        FROM tracecoin_wallets
        WHERE user_id = $1
        "#,
    )
    .bind(user_id)
    .fetch_one(pool)
    .await?;

    Ok(Wallet {
        user_id: row.get("user_id"),
        balance: row.get("balance"),
        reserved: row.get("reserved"),
        available: row.get("available"),
        updated_at: row.get("updated_at"),
    })
}

pub async fn lock_wallet(
    tx: &mut Transaction<'_, Postgres>,
    user_id: Uuid,
) -> Result<(Uuid, i32, i32), WalletError> {
    let row = sqlx::query("SELECT * FROM lock_tracecoin_wallet($1)")
        .bind(user_id)
        .fetch_one(&mut **tx)
        .await?;
    Ok((
        row.get("wallet_id"),
        row.get("balance"),
        row.get("reserved"),
    ))
}

/// Like [`lock_wallet`] but takes the wallet row id directly.
pub async fn lock_wallet_inner(
    tx: &mut Transaction<'_, Postgres>,
    wallet_id: Uuid,
) -> Result<(Uuid, i32, i32), WalletError> {
    // Wrap in a CTE so we can return wallet_id / balance / reserved.
    let row = sqlx::query(
        r#"
        WITH w AS (
            SELECT id, balance, reserved
            FROM tracecoin_wallets
            WHERE id = $1
            FOR UPDATE
        )
        SELECT id AS wallet_id, balance, reserved FROM w
        "#,
    )
    .bind(wallet_id)
    .fetch_optional(&mut **tx)
    .await?
    .ok_or(WalletError::Db(sqlx::Error::RowNotFound))?;

    Ok((
        row.get("wallet_id"),
        row.get("balance"),
        row.get("reserved"),
    ))
}

fn validate_amount(amount: i32) -> Result<(), WalletError> {
    if amount == 0 {
        return Err(WalletError::InvalidAmount(0));
    }
    if amount.unsigned_abs() > MAX_AMOUNT as u32 {
        return Err(WalletError::InvalidAmount(amount));
    }
    Ok(())
}

/// Insert a ledger row inside an existing transaction. Idempotent on
/// `(wallet_id, reference_id, type)` — if a row with that key already
/// exists we fetch and return it instead of double-writing. This lets a
/// single payment credit exactly once while still allowing separate
/// RESERVE / RELEASE / CONFIRM rows for a single lead_request.
async fn write_ledger_entry(
    tx: &mut Transaction<'_, Postgres>,
    wallet_id: Uuid,
    entry_type: LedgerType,
    amount: i32,
    balance_after: i32,
    reason: LedgerReason,
    reference_id: Option<Uuid>,
    actor_user_id: Option<Uuid>,
    metadata: Option<serde_json::Value>,
) -> Result<LedgerEntry, WalletError> {
    if let Some(reference_id) = reference_id {
        let existing = sqlx::query_as::<_, LedgerEntry>(
            r#"
            SELECT
                id, wallet_id,
                type AS entry_type,
                amount,
                balance_after,
                reason,
                reference_id,
                actor_user_id,
                metadata,
                created_at
            FROM tracecoin_ledger
            WHERE wallet_id = $1 AND reference_id = $2 AND type = $3
            "#,
        )
        .bind(wallet_id)
        .bind(reference_id)
        .bind(entry_type.as_str())
        .fetch_optional(&mut **tx)
        .await?;

        if let Some(entry) = existing {
            return Ok(entry);
        }
    }

    let row = sqlx::query(
        r#"
        INSERT INTO tracecoin_ledger (
            wallet_id, type, amount, balance_after, reason,
            reference_id, actor_user_id, metadata
        )
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        RETURNING
            id, wallet_id,
            type AS entry_type,
            amount,
            balance_after,
            reason,
            reference_id,
            actor_user_id,
            metadata,
            created_at
        "#,
    )
    .bind(wallet_id)
    .bind(entry_type.as_str())
    .bind(amount)
    .bind(balance_after)
    .bind(reason.as_str())
    .bind(reference_id)
    .bind(actor_user_id)
    .bind(metadata)
    .fetch_one(&mut **tx)
    .await?;

    Ok(LedgerEntry {
        id: row.get("id"),
        wallet_id: row.get("wallet_id"),
        entry_type: row.get("entry_type"),
        amount: row.get("amount"),
        balance_after: row.get("balance_after"),
        reason: row.get("reason"),
        reference_id: row.get("reference_id"),
        actor_user_id: row.get("actor_user_id"),
        metadata: row.get("metadata"),
        created_at: row.get("created_at"),
    })
}

/// Credit `amount` tracecoins to a user's wallet. Idempotent on
/// `reference_id` — repeated calls with the same reference are a no-op
/// and return the existing ledger entry.
pub async fn credit(
    pool: &PgPool,
    user_id: Uuid,
    amount: i32,
    reason: LedgerReason,
    reference_id: Option<Uuid>,
    actor_user_id: Option<Uuid>,
    metadata: Option<serde_json::Value>,
) -> Result<(Wallet, LedgerEntry), WalletError> {
    validate_amount(amount)?;
    if amount < 0 {
        return Err(WalletError::InvalidAmount(amount));
    }

    let mut tx = pool.begin().await?;
    let (wallet_id, balance, _reserved) = lock_wallet(&mut tx, user_id).await?;

    // Idempotency: if a CREDIT entry already exists for this reference,
    // return it instead of double-crediting.
    if let Some(reference_id) = reference_id {
        if let Some(existing) = sqlx::query_as::<_, LedgerEntry>(
            r#"
            SELECT
                id, wallet_id,
                type AS entry_type,
                amount,
                balance_after,
                reason,
                reference_id,
                actor_user_id,
                metadata,
                created_at
            FROM tracecoin_ledger
            WHERE wallet_id = $1 AND reference_id = $2 AND type = 'CREDIT'
            "#,
        )
        .bind(wallet_id)
        .bind(reference_id)
        .fetch_optional(&mut *tx)
        .await?
        {
            tx.commit().await?;
            let w = get_or_create(pool, user_id).await?;
            return Ok((w, existing));
        }
    }

    let new_balance = balance
        .checked_add(amount)
        .ok_or_else(|| WalletError::InvalidAmount(amount))?;
    if new_balance > MAX_AMOUNT {
        return Err(WalletError::InvalidAmount(amount));
    }

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

    let entry = write_ledger_entry(
        &mut tx,
        wallet_id,
        LedgerType::Credit,
        amount,
        new_balance,
        reason,
        reference_id,
        actor_user_id,
        metadata,
    )
    .await?;

    tx.commit().await?;
    let wallet = get_or_create(pool, user_id).await?;
    Ok((wallet, entry))
}

/// Reserve `amount` tracecoins — moves them from `balance` to `reserved`
/// so the same coins cannot be used by another request. Idempotent on
/// `reference_id`.
pub async fn reserve(
    pool: &PgPool,
    user_id: Uuid,
    amount: i32,
    reference_id: Uuid,
    metadata: Option<serde_json::Value>,
) -> Result<(Wallet, LedgerEntry), WalletError> {
    validate_amount(amount)?;
    if amount < 0 {
        return Err(WalletError::InvalidAmount(amount));
    }

    let mut tx = pool.begin().await?;
    let (wallet_id, balance, reserved) = lock_wallet(&mut tx, user_id).await?;

    // Idempotency: if a RESERVE entry already exists for this reference,
    // return it instead of double-reserving.
    if let Some(existing) = sqlx::query_as::<_, LedgerEntry>(
        r#"
        SELECT
            id, wallet_id,
            type AS entry_type,
            amount,
            balance_after,
            reason,
            reference_id,
            actor_user_id,
            metadata,
            created_at
        FROM tracecoin_ledger
        WHERE wallet_id = $1 AND reference_id = $2 AND type = 'RESERVE'
        "#,
    )
    .bind(wallet_id)
    .bind(reference_id)
    .fetch_optional(&mut *tx)
    .await?
    {
        tx.commit().await?;
        let w = get_or_create(pool, user_id).await?;
        return Ok((w, existing));
    }

    let available = balance - reserved;
    if available < amount {
        return Err(WalletError::InsufficientUnreserved {
            balance,
            reserved,
            requested: amount,
        });
    }

    let new_reserved = reserved
        .checked_add(amount)
        .ok_or_else(|| WalletError::InvalidAmount(amount))?;

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

    let entry = write_ledger_entry(
        &mut tx,
        wallet_id,
        LedgerType::Reserve,
        amount,
        balance,
        LedgerReason::LeadRequestReserve,
        Some(reference_id),
        Some(user_id),
        metadata,
    )
    .await?;

    tx.commit().await?;
    let wallet = get_or_create(pool, user_id).await?;
    Ok((wallet, entry))
}

/// Release a previously-held reservation back to `balance`. Idempotent.
pub async fn release(
    pool: &PgPool,
    user_id: Uuid,
    amount: i32,
    reference_id: Uuid,
    metadata: Option<serde_json::Value>,
) -> Result<(Wallet, LedgerEntry), WalletError> {
    validate_amount(amount)?;
    if amount < 0 {
        return Err(WalletError::InvalidAmount(amount));
    }

    let mut tx = pool.begin().await?;
    let (wallet_id, balance, reserved) = lock_wallet(&mut tx, user_id).await?;

    if let Some(existing) = sqlx::query_as::<_, LedgerEntry>(
        r#"
        SELECT
            id, wallet_id,
            type AS entry_type,
            amount,
            balance_after,
            reason,
            reference_id,
            actor_user_id,
            metadata,
            created_at
        FROM tracecoin_ledger
        WHERE wallet_id = $1 AND reference_id = $2 AND type = 'RELEASE'
        "#,
    )
    .bind(wallet_id)
    .bind(reference_id)
    .fetch_optional(&mut *tx)
    .await?
    {
        tx.commit().await?;
        let w = get_or_create(pool, user_id).await?;
        return Ok((w, existing));
    }

    if reserved < amount {
        return Err(WalletError::InsufficientFunds {
            available: reserved,
            requested: amount,
        });
    }

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

    let entry = write_ledger_entry(
        &mut tx,
        wallet_id,
        LedgerType::Release,
        -amount,
        balance,
        LedgerReason::LeadRequestRelease,
        Some(reference_id),
        Some(user_id),
        metadata,
    )
    .await?;

    tx.commit().await?;
    let wallet = get_or_create(pool, user_id).await?;
    Ok((wallet, entry))
}

/// Confirm a reservation: permanently deduct from `reserved`. Idempotent
/// on `reference_id`.
pub async fn confirm(
    pool: &PgPool,
    user_id: Uuid,
    amount: i32,
    reference_id: Uuid,
    metadata: Option<serde_json::Value>,
) -> Result<(Wallet, LedgerEntry), WalletError> {
    validate_amount(amount)?;
    if amount < 0 {
        return Err(WalletError::InvalidAmount(amount));
    }

    let mut tx = pool.begin().await?;
    let (wallet_id, balance, reserved) = lock_wallet(&mut tx, user_id).await?;

    if let Some(existing) = sqlx::query_as::<_, LedgerEntry>(
        r#"
        SELECT
            id, wallet_id,
            type AS entry_type,
            amount,
            balance_after,
            reason,
            reference_id,
            actor_user_id,
            metadata,
            created_at
        FROM tracecoin_ledger
        WHERE wallet_id = $1 AND reference_id = $2 AND type = 'DEBIT'
        "#,
    )
    .bind(wallet_id)
    .bind(reference_id)
    .fetch_optional(&mut *tx)
    .await?
    {
        tx.commit().await?;
        let w = get_or_create(pool, user_id).await?;
        return Ok((w, existing));
    }

    if reserved < amount {
        return Err(WalletError::InsufficientFunds {
            available: reserved,
            requested: amount,
        });
    }

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

    let entry = write_ledger_entry(
        &mut tx,
        wallet_id,
        LedgerType::Debit,
        -amount,
        balance,
        LedgerReason::LeadRequestDeduct,
        Some(reference_id),
        Some(user_id),
        metadata,
    )
    .await?;

    tx.commit().await?;
    let wallet = get_or_create(pool, user_id).await?;
    Ok((wallet, entry))
}

/// Admin adjustment. `amount` may be positive (credit) or negative (debit).
/// `actor_user_id` is recorded in the ledger for audit.
pub async fn admin_adjust(
    pool: &PgPool,
    user_id: Uuid,
    amount: i32,
    reason: LedgerReason,
    actor_user_id: Uuid,
    metadata: Option<serde_json::Value>,
) -> Result<(Wallet, LedgerEntry), WalletError> {
    validate_amount(amount)?;

    let mut tx = pool.begin().await?;
    let (wallet_id, balance, _reserved) = lock_wallet(&mut tx, user_id).await?;

    let new_balance = balance
        .checked_add(amount)
        .ok_or_else(|| WalletError::InvalidAmount(amount))?;
    if new_balance < 0 {
        return Err(WalletError::InsufficientFunds {
            available: balance,
            requested: amount.unsigned_abs() as i32,
        });
    }
    if new_balance > MAX_AMOUNT {
        return Err(WalletError::InvalidAmount(amount));
    }

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

    let entry_type = if amount >= 0 {
        LedgerType::Credit
    } else {
        LedgerType::Debit
    };

    let entry = write_ledger_entry(
        &mut tx,
        wallet_id,
        entry_type,
        amount,
        new_balance,
        reason,
        None,
        Some(actor_user_id),
        metadata,
    )
    .await?;

    tx.commit().await?;
    let wallet = get_or_create(pool, user_id).await?;
    Ok((wallet, entry))
}

/// Read a paginated ledger history for `user_id`, newest first.
pub async fn list_ledger(
    pool: &PgPool,
    user_id: Uuid,
    page: i64,
    limit: i64,
) -> Result<Vec<LedgerEntry>, WalletError> {
    let limit = limit.clamp(1, 200);
    let page = page.max(1);
    let offset = (page - 1) * limit;

    let rows = sqlx::query_as::<_, LedgerEntry>(
        r#"
        SELECT
            tl.id, tl.wallet_id,
            tl.type AS entry_type,
            tl.amount,
            tl.balance_after,
            tl.reason,
            tl.reference_id,
            tl.actor_user_id,
            tl.metadata,
            tl.created_at
        FROM tracecoin_ledger tl
        JOIN tracecoin_wallets tw ON tw.id = tl.wallet_id
        WHERE tw.user_id = $1
        ORDER BY tl.created_at DESC, tl.id DESC
        LIMIT $2 OFFSET $3
        "#,
    )
    .bind(user_id)
    .bind(limit)
    .bind(offset)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Write an audit log row. We use the existing `audit_logs` table from
/// the wider nxtgauge schema.
///
/// All admin adjustments MUST call this so a balance change is
/// traceable end-to-end.
pub async fn audit(
    pool: &PgPool,
    actor_user_id: Uuid,
    target_user_id: Uuid,
    amount: i32,
    reason: &str,
    ledger_entry_id: Uuid,
) -> Result<(), WalletError> {
    let summary = format!(
        "Wallet adjustment: amount={} reason='{}' target_user={} ledger_entry={}",
        amount, reason, target_user_id, ledger_entry_id
    );
    sqlx::query(
        r#"
        INSERT INTO audit_logs (
            actor_user_id,
            action,
            entity_type,
            entity_id,
            module_key,
            status,
            summary,
            metadata_json
        )
        VALUES ($1, $2, $3, $4, $5, 'SUCCESS', $6, $7)
        "#,
    )
    .bind(actor_user_id)
    .bind("WALLET_ADJUST")
    .bind("tracecoin_wallet")
    .bind(target_user_id)
    .bind("tracecoin")
    .bind(&summary)
    .bind(serde_json::json!({
        "amount": amount,
        "reason": reason,
        "ledger_entry_id": ledger_entry_id,
    }))
    .execute(pool)
    .await?;
    Ok(())
}
