# CachedSearchService.java

    package com.university.bookstore.search;

    import java.util.List;
    import java.util.Objects;

    import com.university.bookstore.model.Material;
    import com.university.bookstore.repository.MaterialRepository;

    /**
     * Service that integrates Trie-based prefix search with LRU caching for optimal performance.
     * Provides fast prefix-based material searching with intelligent result caching.
     *
     * <p>This service combines the efficiency of Trie data structures for prefix searching
     * with LRU caching to avoid repeated computation for frequently accessed queries.</p>
     *
     * @author Navid Mohaghegh
     * @version 3.0
     * @since 2024-09-15
     */
    public class CachedSearchService {

        private final MaterialTrie trie;
        private final SearchResultCache cache;
        private final MaterialRepository repository;

        /**
         * Creates a new cached search service.
         *
         * @param repository the material repository to search
         * @param cacheSize the maximum number of cached search results
         */
        public CachedSearchService(MaterialRepository repository, int cacheSize) {
            this.repository = Objects.requireNonNull(repository, "Repository cannot be null");
            this.trie = new MaterialTrie();
            this.cache = new SearchResultCache(cacheSize);
            initializeTrie();
        }

        /**
         * Searches for materials by title prefix with caching.
         *
         * @param prefix the title prefix to search for
         * @return list of materials matching the prefix
         */
        public List<Material> searchByPrefix(String prefix) {
            if (prefix == null || prefix.trim().isEmpty()) {
                return List.of();
            }

            String cacheKey = "prefix:" + prefix.toLowerCase().trim();

            // Check cache first
            var cached = cache.get(cacheKey);
            if (cached.isPresent()) {
                return cached.get();
            }

            // Perform search using trie
            List<Material> results = trie.searchByPrefix(prefix);

            // Cache results
            cache.put(cacheKey, results);

            return results;
        }

        /**
         * Searches for materials by title prefix with result limit and caching.
         *
         * @param prefix the title prefix to search for
         * @param limit the maximum number of results to return
         * @return list of materials matching the prefix, limited to the specified count
         */
        public List<Material> searchByPrefixWithLimit(String prefix, int limit) {
            if (prefix == null || prefix.trim().isEmpty() || limit <= 0) {
                return List.of();
            }

            String cacheKey = "prefix:" + prefix.toLowerCase().trim() + ":limit:" + limit;

            // Check cache first
            var cached = cache.get(cacheKey);
            if (cached.isPresent()) {
                return cached.get();
            }

            // Perform search using trie
            List<Material> results = trie.searchByPrefixWithLimit(prefix, limit);

            // Cache results
            cache.put(cacheKey, results);

            return results;
        }

        /**
         * Adds a material to the search index and invalidates relevant cache entries.
         *
         * @param material the material to add
         */
        public void addMaterial(Material material) {
            if (material == null) {
                throw new IllegalArgumentException("Material cannot be null");
            }

            trie.insert(material);
            invalidateCacheForMaterial(material);
        }

        /**
         * Removes a material from the search index and invalidates relevant cache entries.
         *
         * @param material the material to remove
         */
        public void removeMaterial(Material material) {
            if (material == null) {
                return;
            }

            trie.remove(material);
            invalidateCacheForMaterial(material);
        }

        /**
         * Refreshes the search index from the repository.
         * Clears the cache to ensure consistency.
         */
        public void refreshIndex() {
            trie.clear();
            cache.clear();
            initializeTrie();
        }

        /**
         * Gets cache statistics for performance monitoring.
         *
         * @return cache statistics
         */
        public SearchResultCache.CacheStats getCacheStats() {
            return cache.getStats();
        }

        /**
         * Gets the current size of the search index.
         *
         * @return the number of materials in the index
         */
        public int getIndexSize() {
            return trie.size();
        }

        /**
         * Checks if the search index is empty.
         *
         * @return true if no materials are indexed
         */
        public boolean isIndexEmpty() {
            return trie.isEmpty();
        }

        /**
         * Clears the search index and cache.
         */
        public void clear() {
            trie.clear();
            cache.clear();
        }

        /**
         * Initializes the trie with all materials from the repository.
         */
        private void initializeTrie() {
            List<Material> materials = repository.findAll();
            for (Material material : materials) {
                trie.insert(material);
            }
        }

        /**
         * Invalidates cache entries that might be affected by changes to a material.
         *
         * @param material the material that was changed
         */
        private void invalidateCacheForMaterial(Material material) {
            String title = material.getTitle().toLowerCase();

            // Invalidate cache entries for all possible prefixes of the material's title
            for (int i = 1; i <= title.length(); i++) {
                String prefix = title.substring(0, i);
                String cacheKey = "prefix:" + prefix;
                cache.remove(cacheKey);

                // Also remove limit-based cache entries
                for (int limit = 10; limit <= 100; limit += 10) {
                    String limitCacheKey = cacheKey + ":limit:" + limit;
                    cache.remove(limitCacheKey);
                }
            }
        }

        @Override
        public String toString() {
            return String.format("CachedSearchService[IndexSize=%d, CacheSize=%d/%d, HitRatio=%.2f%%]",
                getIndexSize(),
                cache.size(),
                cache.getMaxSize(),
                getCacheStats().getHitRatio() * 100);
        }
    }