package dev.banking.model;

import java.util.concurrent.locks.ReentrantLock;

/**
 * Thread-safe BankAccount implementation.
 *
 * Design choices:
 * - Each account has its OWN ReentrantLock (per-account locking).
 *   This means two threads working on DIFFERENT accounts never block each other.
 *
 * - deposit() and withdraw() simply acquire this account's lock,
 *   modify the balance, then release the lock. Always inside try/finally
 *   so the lock is GUARANTEED to be released even if an exception occurs.
 *
 * - getBalance() also acquires the lock so it never reads a half-written value.
 *
 * - transfer() is the most complex:
 *   It must lock TWO accounts at the same time.
 *   To prevent deadlock we ALWAYS lock the account with the LOWER accountId first.
 *   Example: Thread 1 does A(id=1) -> B(id=2)  →  locks id=1 first, then id=2
 *            Thread 2 does B(id=2) -> A(id=1)  →  ALSO locks id=1 first, then id=2
 *   Because both threads always lock in the same order, they can never deadlock.
 */
public class BankAccount {

    private final int accountId;
    private long balance;

    // One lock per account instance. Never shared with other accounts.
    private final ReentrantLock lock = new ReentrantLock();

    public BankAccount(int accountId, long initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }

    public int getAccountId() {
        return accountId;
    }

    /**
     * Returns the current balance in a thread-safe way.
     *
     * We acquire the lock before reading so we never observe
     * a partially-written value from another thread's deposit/withdraw.
     */
    public long getBalance() {
        lock.lock();
        try {
            return balance;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Adds the given amount to the balance atomically.
     *
     * The lock ensures that if Thread A and Thread B both deposit
     * at the same time, one waits for the other. No update is ever lost.
     */
    public void deposit(long amount) {
        lock.lock();
        try {
            balance += amount;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Subtracts the given amount from the balance atomically.
     *
     * Same guarantee as deposit — no lost updates under concurrency.
     * Negative balances are permitted as per the assignment spec.
     */
    public void withdraw(long amount) {
        lock.lock();
        try {
            balance -= amount;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Transfers the given amount from THIS account to the TARGET account atomically.
     *
     * --- HOW DEADLOCK IS PREVENTED ---
     * A deadlock would happen if:
     *   Thread 1 locks Account A, then waits for Account B
     *   Thread 2 locks Account B, then waits for Account A
     *   → both wait forever.
     *
     * The fix: ALWAYS lock the account with the lower accountId first,
     * regardless of which direction the money is flowing.
     *
     * Thread 1 (A→B): locks id=1 first, then id=2  ✅
     * Thread 2 (B→A): locks id=1 first, then id=2  ✅  (same order!)
     * → Thread 2 simply waits until Thread 1 finishes. No deadlock.
     *
     * --- HOW ATOMICITY IS GUARANTEED ---
     * Both locks are held before any balance changes.
     * No other thread can touch either account during the transfer.
     * If an exception occurs, finally blocks release both locks safely.
     * Money is never lost or created.
     */
    public void transfer(BankAccount target, long amount) {

        // Determine which account to lock first (lower id = first)
        BankAccount first  = this.accountId < target.accountId ? this : target;
        BankAccount second = this.accountId < target.accountId ? target : this;

        first.lock.lock();
        try {
            second.lock.lock();
            try {
                // Both accounts are now locked exclusively.
                // No other thread can deposit, withdraw, or transfer
                // on either account until we release both locks.
                this.balance   -= amount;
                target.balance += amount;
            } finally {
                second.lock.unlock();
            }
        } finally {
            first.lock.unlock();
        }
    }
}
