# ModernSearchCache.java

    package com.university.bookstore.search;

    import java.time.Duration;
    import java.time.Instant;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicLong;
    import java.util.concurrent.atomic.LongAdder;
    import java.util.function.Function;
    import java.util.logging.Logger;

    import com.university.bookstore.model.Material;

    /**
     * Modern high-performance cache implementation with advanced features.
     * Features:
     * - Time-based and size-based eviction
     * - Async loading with CompletableFuture
     * - Statistics tracking
     * - Warm-up and refresh capabilities
     * - Thread-safe operations
     *
     * @author Navid Mohaghegh
     * @version 4.0
     * @since 2024-09-15
     */
    public class ModernSearchCache implements AutoCloseable {

        private static final Logger LOGGER = Logger.getLogger(ModernSearchCache.class.getName());

        /**
         * Cache entry with metadata.
         */
        private record CacheEntry(
            List<Material> value,
            Instant createdAt,
            Instant lastAccessedAt,
            AtomicLong accessCount
        ) {
            /**
             * Creates a new cache entry.
             */
            public static CacheEntry of(List<Material> value) {
                return new CacheEntry(
                    Collections.unmodifiableList(new ArrayList<>(value)),
                    Instant.now(),
                    Instant.now(),
                    new AtomicLong(1)
                );
            }

            /**
             * Records an access to this entry.
             */
            public CacheEntry recordAccess() {
                accessCount.incrementAndGet();
                return new CacheEntry(value, createdAt, Instant.now(), accessCount);
            }

            /**
             * Checks if the entry is expired.
             */
            public boolean isExpired(Duration ttl) {
                return Duration.between(createdAt, Instant.now()).compareTo(ttl) > 0;
            }

            /**
             * Checks if the entry is stale (not accessed recently).
             */
            public boolean isStale(Duration idleTime) {
                return Duration.between(lastAccessedAt, Instant.now()).compareTo(idleTime) > 0;
            }
        }

        /**
         * Cache statistics record.
         *
         * @param hitCount number of cache hits
         * @param missCount number of cache misses
         * @param evictionCount number of cache evictions
         * @param loadCount number of cache loads
         * @param hitRate cache hit rate (0.0 to 1.0)
         * @param averageLoadTime average time to load data
         * @param size current cache size
         * @param totalAccessCount total number of cache accesses
         */
        public record CacheStats(
            long hitCount,
            long missCount,
            long evictionCount,
            long loadCount,
            double hitRate,
            double averageLoadTime,
            int size,
            long totalAccessCount
        ) {
            /**
             * Creates a summary string.
             */
            public String getSummary() {
                return String.format(
                    """
                    Cache Statistics:
                    - Hit Rate: %.2f%%
                    - Total Hits: %d
                    - Total Misses: %d
                    - Evictions: %d
                    - Cache Size: %d
                    - Average Load Time: %.2f ms
                    - Total Accesses: %d
                    """,
                    hitRate * 100, hitCount, missCount, evictionCount,
                    size, averageLoadTime, totalAccessCount
                );
            }
        }

        private final Map<String, CacheEntry> cache;
        private final ExecutorService loadingExecutor;
        private final ScheduledExecutorService maintenanceExecutor;
        private final int maxSize;
        private final Duration ttl;
        private final Duration idleTime;

        // Statistics
        private final LongAdder hitCount = new LongAdder();
        private final LongAdder missCount = new LongAdder();
        private final LongAdder evictionCount = new LongAdder();
        private final LongAdder loadCount = new LongAdder();
        private final LongAdder totalLoadTime = new LongAdder();

        private volatile boolean closed = false;

        /**
         * Creates a new modern cache with default settings.
         */
        public ModernSearchCache() {
            this(1000, Duration.ofMinutes(10), Duration.ofMinutes(5));
        }

        /**
         * Creates a new modern cache with custom settings.
         *
         * @param maxSize maximum number of entries
         * @param ttl time to live for entries
         * @param idleTime maximum idle time before eviction
         */
        public ModernSearchCache(int maxSize, Duration ttl, Duration idleTime) {
            this.maxSize = maxSize;
            this.ttl = ttl;
            this.idleTime = idleTime;
            this.cache = new ConcurrentHashMap<>();

            // Use virtual threads if available, otherwise use cached thread pool
            this.loadingExecutor = Executors.newCachedThreadPool(r -> {
                Thread t = new Thread(r, "Cache-Loader");
                t.setDaemon(true);
                return t;
            });

            this.maintenanceExecutor = Executors.newScheduledThreadPool(1, r -> {
                Thread t = new Thread(r, "Cache-Maintenance");
                t.setDaemon(true);
                return t;
            });

            // Schedule periodic maintenance
            scheduleMaintenance();
        }

        private void scheduleMaintenance() {
            // Periodic cleanup of expired and stale entries
            maintenanceExecutor.scheduleWithFixedDelay(
                this::performMaintenance,
                1, 1, TimeUnit.MINUTES
            );

            // Periodic statistics logging
            maintenanceExecutor.scheduleWithFixedDelay(
                () -> LOGGER.info(getStats().getSummary()),
                5, 5, TimeUnit.MINUTES
            );
        }

        private void performMaintenance() {
            if (closed) return;

            int removed = 0;
            Iterator<Map.Entry<String, CacheEntry>> iterator = cache.entrySet().iterator();

            while (iterator.hasNext()) {
                Map.Entry<String, CacheEntry> entry = iterator.next();
                CacheEntry cacheEntry = entry.getValue();

                if (cacheEntry.isExpired(ttl) || cacheEntry.isStale(idleTime)) {
                    iterator.remove();
                    evictionCount.increment();
                    removed++;
                }
            }

            if (removed > 0) {
                LOGGER.fine("Maintenance: Removed " + removed + " expired/stale entries");
            }

            // Size-based eviction if needed
            if (cache.size() > maxSize) {
                performSizeBasedEviction();
            }
        }

        private void performSizeBasedEviction() {
            int toRemove = cache.size() - maxSize;
            if (toRemove <= 0) return;

            // Remove least recently accessed entries
            List<Map.Entry<String, CacheEntry>> entries = new ArrayList<>(cache.entrySet());
            entries.sort(Comparator.comparing(e -> e.getValue().lastAccessedAt));

            for (int i = 0; i < toRemove && i < entries.size(); i++) {
                cache.remove(entries.get(i).getKey());
                evictionCount.increment();
            }

            LOGGER.fine("Size-based eviction: Removed " + toRemove + " entries");
        }

        /**
         * Gets a value from the cache or loads it if not present.
         *
         * @param key the cache key
         * @param loader function to load the value if not cached
         * @return the cached or loaded value
         */
        public List<Material> get(String key, Function<String, List<Material>> loader) {
            Objects.requireNonNull(key, "Key cannot be null");
            Objects.requireNonNull(loader, "Loader cannot be null");
            ensureNotClosed();

            CacheEntry entry = cache.get(key);

            if (entry != null && !entry.isExpired(ttl)) {
                // Cache hit
                hitCount.increment();
                cache.put(key, entry.recordAccess());
                return new ArrayList<>(entry.value);
            }

            // Cache miss
            missCount.increment();

            // Load synchronously
            long startTime = System.currentTimeMillis();
            List<Material> value = loader.apply(key);
            long loadTime = System.currentTimeMillis() - startTime;

            loadCount.increment();
            totalLoadTime.add(loadTime);

            if (value != null && !value.isEmpty()) {
                put(key, value);
            }

            return value != null ? new ArrayList<>(value) : new ArrayList<>();
        }

        /**
         * Gets a value from the cache or loads it asynchronously.
         *
         * @param key the cache key
         * @param asyncLoader async function to load the value
         * @return CompletableFuture with the result
         */
        public CompletableFuture<List<Material>> getAsync(
                String key,
                Function<String, CompletableFuture<List<Material>>> asyncLoader) {

            Objects.requireNonNull(key, "Key cannot be null");
            Objects.requireNonNull(asyncLoader, "Async loader cannot be null");
            ensureNotClosed();

            CacheEntry entry = cache.get(key);

            if (entry != null && !entry.isExpired(ttl)) {
                // Cache hit
                hitCount.increment();
                cache.put(key, entry.recordAccess());
                return CompletableFuture.completedFuture(new ArrayList<>(entry.value));
            }

            // Cache miss - load asynchronously
            missCount.increment();

            long startTime = System.currentTimeMillis();

            return asyncLoader.apply(key)
                .thenApply(value -> {
                    long loadTime = System.currentTimeMillis() - startTime;
                    loadCount.increment();
                    totalLoadTime.add(loadTime);

                    if (value != null && !value.isEmpty()) {
                        put(key, value);
                    }

                    return value != null ? new ArrayList<>(value) : new ArrayList<>();
                });
        }

        /**
         * Puts a value in the cache.
         *
         * @param key the cache key
         * @param value the value to cache
         */
        public void put(String key, List<Material> value) {
            Objects.requireNonNull(key, "Key cannot be null");
            Objects.requireNonNull(value, "Value cannot be null");
            ensureNotClosed();

            // Check size limit
            if (cache.size() >= maxSize && !cache.containsKey(key)) {
                performSizeBasedEviction();
            }

            cache.put(key, CacheEntry.of(value));
        }

        /**
         * Invalidates a cache entry.
         *
         * @param key the key to invalidate
         * @return true if an entry was removed
         */
        public boolean invalidate(String key) {
            ensureNotClosed();
            CacheEntry removed = cache.remove(key);
            if (removed != null) {
                evictionCount.increment();
            }
            return removed != null;
        }

        /**
         * Invalidates all cache entries.
         */
        public void invalidateAll() {
            ensureNotClosed();
            int size = cache.size();
            cache.clear();
            evictionCount.add(size);
            LOGGER.info("Cache cleared: " + size + " entries invalidated");
        }

        /**
         * Invalidates entries matching a predicate.
         *
         * @param predicate the predicate to test keys
         * @return number of entries invalidated
         */
        public int invalidateIf(java.util.function.Predicate<String> predicate) {
            ensureNotClosed();
            int removed = 0;

            Iterator<String> iterator = cache.keySet().iterator();
            while (iterator.hasNext()) {
                if (predicate.test(iterator.next())) {
                    iterator.remove();
                    evictionCount.increment();
                    removed++;
                }
            }

            return removed;
        }

        /**
         * Refreshes a cache entry asynchronously.
         *
         * @param key the key to refresh
         * @param loader the loader function
         * @return CompletableFuture with the refreshed value
         */
        public CompletableFuture<List<Material>> refresh(
                String key,
                Function<String, List<Material>> loader) {

            return CompletableFuture.supplyAsync(() -> {
                invalidate(key);
                return get(key, loader);
            }, loadingExecutor);
        }

        /**
         * Warms up the cache with predefined keys.
         *
         * @param keys keys to warm up
         * @param loader the loader function
         * @return CompletableFuture that completes when warm-up is done
         */
        public CompletableFuture<Void> warmUp(
                Collection<String> keys,
                Function<String, List<Material>> loader) {

            List<CompletableFuture<Void>> futures = keys.stream()
                .map(key -> CompletableFuture.runAsync(
                    () -> get(key, loader),
                    loadingExecutor
                ))
                .toList();

            return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
        }

        /**
         * Gets the current cache size.
         *
         * @return number of cached entries
         */
        public int size() {
            return cache.size();
        }

        /**
         * Checks if the cache is empty.
         *
         * @return true if no entries are cached
         */
        public boolean isEmpty() {
            return cache.isEmpty();
        }

        /**
         * Checks if a key is cached.
         *
         * @param key the key to check
         * @return true if the key is cached and not expired
         */
        public boolean containsKey(String key) {
            CacheEntry entry = cache.get(key);
            return entry != null && !entry.isExpired(ttl);
        }

        /**
         * Gets cache statistics.
         *
         * @return current cache statistics
         */
        public CacheStats getStats() {
            long hits = hitCount.sum();
            long misses = missCount.sum();
            long total = hits + misses;
            double hitRate = total > 0 ? (double) hits / total : 0.0;
            double avgLoadTime = loadCount.sum() > 0
                ? (double) totalLoadTime.sum() / loadCount.sum()
                : 0.0;

            return new CacheStats(
                hits,
                misses,
                evictionCount.sum(),
                loadCount.sum(),
                hitRate,
                avgLoadTime,
                cache.size(),
                total
            );
        }

        /**
         * Resets cache statistics.
         */
        public void resetStats() {
            hitCount.reset();
            missCount.reset();
            evictionCount.reset();
            loadCount.reset();
            totalLoadTime.reset();
            LOGGER.info("Cache statistics reset");
        }

        private void ensureNotClosed() {
            if (closed) {
                throw new IllegalStateException("Cache has been closed");
            }
        }

        @Override
        public void close() {
            if (!closed) {
                closed = true;

                // Log final statistics
                LOGGER.info("Closing cache. Final stats: " + getStats().getSummary());

                // Clear cache
                cache.clear();

                // Shutdown executors
                loadingExecutor.shutdown();
                maintenanceExecutor.shutdown();

                try {
                    if (!loadingExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                        loadingExecutor.shutdownNow();
                    }
                    if (!maintenanceExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                        maintenanceExecutor.shutdownNow();
                    }
                } catch (InterruptedException e) {
                    loadingExecutor.shutdownNow();
                    maintenanceExecutor.shutdownNow();
                    Thread.currentThread().interrupt();
                }
            }
        }
    }