import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class TransferBank {

    public static void main(String[] args) {
        BankMini bank = new BankMini();
        bank.tambahRekening("Budi", 500_000L);
        bank.tambahRekening("Siti", 300_000L);
        bank.tambahRekening("Dewi", 200_000L);
        bank.cetakSaldo("Saldo awal");

        System.out.println();
        System.out.println("== Skenario 1: transfer sukses, diakhiri commit ==");
        TransaksiMini tx1 = bank.beginTransaction();
        try {
            tx1.debit("Budi", 150_000L);
            tx1.kredit("Siti", 150_000L);
            tx1.commit();
        } catch (SaldoKurangException e) {
            tx1.rollback();
        }
        bank.cetakSaldo("Setelah transfer Budi -> Siti 150000");

        System.out.println();
        System.out.println("== Skenario 2: transfer gagal, diakhiri rollback ==");
        TransaksiMini tx2 = bank.beginTransaction();
        try {
            tx2.debit("Siti", 1_000_000L);   // saldo tidak cukup -> exception
            tx2.kredit("Budi", 1_000_000L);  // tidak pernah tercapai
            tx2.commit();                    // tidak pernah tercapai
        } catch (SaldoKurangException e) {
            System.out.println("gagal   : " + e.getMessage());
            tx2.rollback();
        }
        bank.cetakSaldo("Setelah transfer Siti -> Budi 1000000 gagal");

        System.out.println();
        System.out.println("== Skenario 3: rollback sebagian lewat savepoint ==");
        TransaksiMini tx3 = bank.beginTransaction();
        try {
            tx3.kredit("Budi", 50_000L);
            tx3.setSavepoint("setelah-topup-budi");
            tx3.kredit("Siti", 25_000L);
            tx3.debit("Dewi", 999_999L);     // gagal di sini
            tx3.commit();
        } catch (SaldoKurangException e) {
            System.out.println("gagal   : " + e.getMessage());
            tx3.rollback("setelah-topup-budi"); // batalkan hanya setelah savepoint
            tx3.commit();                        // terapkan sisanya
        }
        bank.cetakSaldo("Setelah skenario savepoint");
    }
}

/** Database mini: saldo rekening yang "persisten". */
class BankMini {

    private final Map<String, Long> saldo = new LinkedHashMap<>();

    void tambahRekening(String nama, long nilaiAwal) {
        saldo.put(nama, nilaiAwal);
    }

    TransaksiMini beginTransaction() {
        System.out.println("[tx] BEGIN (auto-commit dimatikan)");
        return new TransaksiMini(this);
    }

    Map<String, Long> snapshot() {
        return new HashMap<>(saldo);
    }

    void terapkan(Map<String, Long> kerja) {
        saldo.putAll(kerja);
    }

    void cetakSaldo(String judul) {
        System.out.println(judul);
        for (Map.Entry<String, Long> e : saldo.entrySet()) {
            System.out.printf("  %-5s : Rp%d%n", e.getKey(), e.getValue());
        }
    }
}

/** Transaksi mini: kerja di salinan, commit menempelkan, rollback membuang. */
class TransaksiMini {

    private final BankMini bank;
    private final Map<String, Long> kerja;
    private final Map<String, Map<String, Long>> savepoint = new HashMap<>();

    TransaksiMini(BankMini bank) {
        this.bank = bank;
        this.kerja = bank.snapshot();
    }

    void debit(String rekening, long jumlah) throws SaldoKurangException {
        long s = kerja.get(rekening);
        if (s < jumlah) {
            throw new SaldoKurangException(
                    "saldo " + rekening + " tidak cukup: " + s + " < " + jumlah);
        }
        kerja.put(rekening, s - jumlah);
        System.out.printf("[tx] DEBIT  %-5s -%d%n", rekening, jumlah);
    }

    void kredit(String rekening, long jumlah) {
        long s = kerja.get(rekening);
        kerja.put(rekening, s + jumlah);
        System.out.printf("[tx] KREDIT %-5s +%d%n", rekening, jumlah);
    }

    void setSavepoint(String nama) {
        savepoint.put(nama, new HashMap<>(kerja));
        System.out.println("[tx] SAVEPOINT " + nama);
    }

    void rollback() {
        kerja.putAll(bank.snapshot());
        System.out.println("[tx] ROLLBACK (seluruh perubahan dibatalkan)");
    }

    void rollback(String namaSavepoint) {
        Map<String, Long> keadaan = savepoint.get(namaSavepoint);
        if (keadaan == null) {
            throw new IllegalArgumentException("savepoint tidak ada: " + namaSavepoint);
        }
        kerja.putAll(keadaan);
        System.out.println("[tx] ROLLBACK TO SAVEPOINT " + namaSavepoint);
    }

    void commit() {
        bank.terapkan(kerja);
        System.out.println("[tx] COMMIT (perubahan menjadi permanen)");
    }
}

class SaldoKurangException extends Exception {

    SaldoKurangException(String pesan) {
        super(pesan);
    }
}
