import java.time.LocalTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Supplier;

public class CacheTtl {

    // ==== Jam virtual: waktu bergerak karena kita memintanya, bukan dinding ====
    static class JamVirtual {

        private long detik = 8 * 3600; // mulai 08:00:00

        void majuMenit(long menit) {
            detik += menit * 60;
        }

        long sekarang() {
            return detik;
        }

        String tampil() {
            return LocalTime.ofSecondOfDay(detik).toString();
        }
    }

    record Entri(String nilai, long kedaluwarsaPada) { }

    record Hasil(boolean hit, String nilai, String catatan) { }

    // ==== Cache TTL ala Caffeine expireAfterWrite ====
    static class Cache {

        private final Map<String, Entri> isi = new LinkedHashMap<>();
        private final JamVirtual jam;
        private final long ttlDetik;
        private int hit = 0;
        private int miss = 0;

        Cache(JamVirtual jam, long ttlMenit) {
            this.jam = jam;
            this.ttlDetik = ttlMenit * 60;
        }

        // Pola cache-aside: padanan method bertanda @Cacheable.
        Hasil ambil(String kunci, Supplier<String> pemuat) {
            Entri e = isi.get(kunci);
            if (e != null && jam.sekarang() < e.kedaluwarsaPada()) {
                hit++;
                return new Hasil(true, e.nilai(), "masih segar");
            }
            miss++;
            String alasan = e == null ? "belum ada di cache"
                    : "kedaluwarsa pada " + LocalTime.ofSecondOfDay(e.kedaluwarsaPada());
            String nilai = pemuat.get();
            isi.put(kunci, new Entri(nilai, jam.sekarang() + ttlDetik));
            return new Hasil(false, nilai, alasan);
        }

        // Padanan @CachePut: perbarui cache bersamaan pemanggilan method.
        void taruh(String kunci, String nilai) {
            isi.put(kunci, new Entri(nilai, jam.sekarang() + ttlDetik));
        }

        // Padanan @CacheEvict: buang entri agar dimuat ulang berikutnya.
        void buang(String kunci) {
            isi.remove(kunci);
        }

        int hit() { return hit; }

        int miss() { return miss; }
    }

    // "Basis data" mahal: kita menghitung setiap pemanggilannya.
    static int bacaBasisData = 0;

    static String muatKatalog() {
        bacaBasisData++;
        return "Katalog[v=" + bacaBasisData + "]";
    }

    public static void main(String[] args) {
        JamVirtual jam = new JamVirtual();
        Cache cache = new Cache(jam, 5); // TTL 5 menit

        ambil(cache, jam);
        jam.majuMenit(1);
        ambil(cache, jam);   // HIT: umur 1 menit
        jam.majuMenit(3);    // sekarang 08:04
        ambil(cache, jam);   // HIT: umur 4 menit
        jam.majuMenit(2);    // sekarang 08:06
        ambil(cache, jam);   // MISS: umur 6 menit > TTL

        System.out.println("[" + jam.tampil() + "] taruh  katalog  -> @CachePut memperbarui entri");
        cache.taruh("katalog", "Katalog[v=manual]");
        jam.majuMenit(1);    // 08:07
        ambil(cache, jam);   // HIT dengan nilai baru

        jam.majuMenit(2);    // 08:09
        System.out.println("[" + jam.tampil() + "] buang  katalog  -> @CacheEvict menghapus entri");
        cache.buang("katalog");
        ambil(cache, jam);   // MISS: dimuat ulang

        System.out.println();
        System.out.println("statistik: hit=" + cache.hit() + " miss=" + cache.miss()
                + " bacaBasisData=" + bacaBasisData);
    }

    static void ambil(Cache cache, JamVirtual jam) {
        Hasil h = cache.ambil("katalog", CacheTtl::muatKatalog);
        System.out.printf("[%s] ambil  katalog  -> %s (%s)  %s%n",
                jam.tampil(), h.hit() ? "HIT " : "MISS", h.catatan(), h.nilai());
    }
}
