package com.storycove.service;

import com.storycove.entity.Author;
import com.storycove.entity.Series;
import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import com.storycove.repository.ReadingPositionRepository;
import com.storycove.repository.StoryRepository;
import com.storycove.repository.TagRepository;
import com.storycove.service.exception.DuplicateResourceException;
import com.storycove.service.exception.ResourceNotFoundException;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@Validated
@Transactional
public class StoryService {
    
    private static final Logger logger = LoggerFactory.getLogger(StoryService.class);

    private final StoryRepository storyRepository;
    private final TagRepository tagRepository;
    private final ReadingPositionRepository readingPositionRepository;
    private final AuthorService authorService;
    private final TagService tagService;
    private final SeriesService seriesService;
    private final HtmlSanitizationService sanitizationService;
    private final SearchServiceAdapter searchServiceAdapter;

    @Autowired
    public StoryService(StoryRepository storyRepository,
                       TagRepository tagRepository,
                       ReadingPositionRepository readingPositionRepository,
                       AuthorService authorService,
                       TagService tagService,
                       SeriesService seriesService,
                       HtmlSanitizationService sanitizationService,
                       SearchServiceAdapter searchServiceAdapter) {
        this.storyRepository = storyRepository;
        this.tagRepository = tagRepository;
        this.readingPositionRepository = readingPositionRepository;
        this.authorService = authorService;
        this.tagService = tagService;
        this.seriesService = seriesService;
        this.sanitizationService = sanitizationService;
        this.searchServiceAdapter = searchServiceAdapter;
    }

    @Transactional(readOnly = true)
    public List<Story> findAll() {
        return storyRepository.findAll();
    }
    
    @Transactional(readOnly = true)
    public List<Story> findAllWithAssociations() {
        return storyRepository.findAllWithAssociations();
    }

    @Transactional(readOnly = true)
    public Page<Story> findAll(Pageable pageable) {
        return storyRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public Story findById(UUID id) {
        return storyRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Story", id.toString()));
    }
    

    @Transactional(readOnly = true)
    public Optional<Story> findByIdOptional(UUID id) {
        return storyRepository.findById(id);
    }
    

    @Transactional(readOnly = true)
    public Optional<Story> findByTitle(String title) {
        return storyRepository.findByTitle(title);
    }

    @Transactional(readOnly = true)
    public Optional<Story> findBySourceUrl(String sourceUrl) {
        return storyRepository.findBySourceUrl(sourceUrl);
    }

    @Transactional(readOnly = true)
    public List<Story> searchByTitle(String title) {
        return storyRepository.findByTitleContainingIgnoreCase(title);
    }

    @Transactional(readOnly = true)
    public Page<Story> searchByTitle(String title, Pageable pageable) {
        return storyRepository.findByTitleContainingIgnoreCase(title, pageable);
    }

    @Transactional(readOnly = true)
    public List<Story> findByAuthor(UUID authorId) {
        Author author = authorService.findById(authorId);
        return storyRepository.findByAuthor(author);
    }

    @Transactional(readOnly = true)
    public Page<Story> findByAuthor(UUID authorId, Pageable pageable) {
        Author author = authorService.findById(authorId);
        return storyRepository.findByAuthor(author, pageable);
    }

    @Transactional(readOnly = true)
    public List<Story> findBySeries(UUID seriesId) {
        seriesService.findById(seriesId); // Validate series exists
        return storyRepository.findBySeriesOrderByVolume(seriesId);
    }

    @Transactional(readOnly = true)
    public Page<Story> findBySeries(UUID seriesId, Pageable pageable) {
        Series series = seriesService.findById(seriesId);
        return storyRepository.findBySeries(series, pageable);
    }

    @Transactional(readOnly = true)
    public Optional<Story> findBySeriesAndVolume(UUID seriesId, Double volume) {
        return storyRepository.findBySeriesAndVolume(seriesId, volume);
    }

    @Transactional(readOnly = true)
    public List<Story> findByTag(UUID tagId) {
        Tag tag = tagService.findById(tagId);
        return storyRepository.findByTag(tag);
    }

    @Transactional(readOnly = true)
    public Page<Story> findByTag(UUID tagId, Pageable pageable) {
        Tag tag = tagService.findById(tagId);
        return storyRepository.findByTag(tag, pageable);
    }

    @Transactional(readOnly = true)
    public List<Story> findByTagNames(List<String> tagNames) {
        return storyRepository.findByTagNames(tagNames);
    }

    @Transactional(readOnly = true)
    public Page<Story> findByTagNames(List<String> tagNames, Pageable pageable) {
        return storyRepository.findByTagNames(tagNames, pageable);
    }

    // Favorite and completion status methods removed as these fields were not in spec

    @Transactional(readOnly = true)
    public List<Story> findRecentlyRead(int hours) {
        LocalDateTime since = LocalDateTime.now().minusHours(hours);
        return storyRepository.findRecentlyRead(since);
    }

    @Transactional(readOnly = true)
    public List<Story> findRecentlyAdded() {
        return storyRepository.findRecentlyAdded();
    }

    @Transactional(readOnly = true)
    public Page<Story> findRecentlyAdded(Pageable pageable) {
        return storyRepository.findRecentlyAdded(pageable);
    }

    @Transactional(readOnly = true)
    public List<Story> findTopRated() {
        return storyRepository.findTopRatedStories();
    }

    @Transactional(readOnly = true)
    public Page<Story> findTopRated(Pageable pageable) {
        return storyRepository.findTopRatedStories(pageable);
    }

    @Transactional(readOnly = true)
    public List<Story> findByWordCountRange(Integer minWords, Integer maxWords) {
        return storyRepository.findByWordCountRange(minWords, maxWords);
    }

    @Transactional(readOnly = true)
    public List<Story> searchByKeyword(String keyword) {
        return storyRepository.findByKeyword(keyword);
    }

    @Transactional(readOnly = true)
    public Page<Story> searchByKeyword(String keyword, Pageable pageable) {
        return storyRepository.findByKeyword(keyword, pageable);
    }
    
    @Transactional
    public Story setCoverImage(UUID id, String imagePath) {
        Story story = findById(id);
        
        // Delete old cover if exists
        if (story.getCoverPath() != null && !story.getCoverPath().isEmpty()) {
            // Note: ImageService would be injected here in a real implementation
            // For now, we just update the path
        }
        
        story.setCoverPath(imagePath);
        return storyRepository.save(story);
    }
    
    @Transactional  
    public void removeCoverImage(UUID id) {
        Story story = findById(id);
        
        if (story.getCoverPath() != null && !story.getCoverPath().isEmpty()) {
            // Note: ImageService would be injected here to delete file
            story.setCoverPath(null);
            storyRepository.save(story);
        }
    }
    
    @Transactional
    public Story addTag(UUID storyId, UUID tagId) {
        Story story = findById(storyId);
        Tag tag = tagRepository.findById(tagId)
            .orElseThrow(() -> new ResourceNotFoundException("Tag not found with id: " + tagId));
        
        story.addTag(tag);
        Story savedStory = storyRepository.save(story);
        
        // Update search index with new tag information
        searchServiceAdapter.updateStory(savedStory);
        
        return savedStory;
    }
    
    @Transactional
    public Story removeTag(UUID storyId, UUID tagId) {
        Story story = findById(storyId);
        Tag tag = tagRepository.findById(tagId)
            .orElseThrow(() -> new ResourceNotFoundException("Tag not found with id: " + tagId));
        
        story.removeTag(tag);
        Story savedStory = storyRepository.save(story);
        
        // Update search index with updated tag information
        searchServiceAdapter.updateStory(savedStory);
        
        return savedStory;
    }
    
    @Transactional
    public Story setRating(UUID id, Integer rating) {
        if (rating != null && (rating < 1 || rating > 5)) {
            throw new IllegalArgumentException("Rating must be between 1 and 5");
        }
        
        Story story = findById(id);
        story.setRating(rating);
        Story savedStory = storyRepository.save(story);
        
        // Update search index with new rating
        searchServiceAdapter.updateStory(savedStory);
        
        return savedStory;
    }
    
    @Transactional
    public Story updateReadingProgress(UUID id, Integer position) {
        if (position != null && position < 0) {
            throw new IllegalArgumentException("Reading position must be non-negative");
        }
        
        Story story = findById(id);
        story.updateReadingProgress(position);
        Story savedStory = storyRepository.save(story);
        
        // Update search index with new reading progress
        searchServiceAdapter.updateStory(savedStory);
        
        return savedStory;
    }
    
    @Transactional
    public Story updateReadingStatus(UUID id, Boolean isRead) {
        Story story = findById(id);
        
        if (Boolean.TRUE.equals(isRead)) {
            story.markAsRead();
        } else {
            story.setIsRead(false);
            story.setLastReadAt(LocalDateTime.now());
        }
        
        Story savedStory = storyRepository.save(story);
        
        // Update search index with new reading status
        searchServiceAdapter.updateStory(savedStory);
        
        return savedStory;
    }
    
    @Transactional(readOnly = true)
    public List<Story> findBySeriesOrderByVolume(UUID seriesId) {
        return storyRepository.findBySeriesOrderByVolume(seriesId);
    }
    
    @Transactional(readOnly = true)
    public List<Story> findRecentlyAddedLimited(Pageable pageable) {
        return storyRepository.findRecentlyAdded(pageable).getContent();
    }
    
    @Transactional(readOnly = true)
    public List<Story> findTopRatedStoriesLimited(Pageable pageable) {
        return storyRepository.findTopRatedStories(pageable).getContent();
    }

    public Story create(@Valid Story story) {
        validateStoryForCreate(story);

        // Set up relationships
        if (story.getAuthor() != null && story.getAuthor().getId() != null) {
            Author author = authorService.findById(story.getAuthor().getId());
            story.setAuthor(author);
        }

        if (story.getSeries() != null && story.getSeries().getId() != null) {
            Series series = seriesService.findById(story.getSeries().getId());
            story.setSeries(series);
            validateSeriesVolume(series, story.getVolume());
        }

        Story savedStory = storyRepository.save(story);

        // Handle tags
        if (story.getTags() != null && !story.getTags().isEmpty()) {
            updateStoryTags(savedStory, story.getTags());
        }

        // Index AFTER the transaction commits so that Hibernate has already flushed the entity
        // (setting @CreationTimestamp / @UpdateTimestamp) and all tag relationships are persisted.
        // Indexing inside the transaction would send null timestamps and incomplete tag data to Solr.
        scheduleIndexAfterCommit(savedStory);

        return savedStory;
    }

    public Story createWithTagNames(@Valid Story story, java.util.List<String> tagNames) {
        validateStoryForCreate(story);

        // Set up relationships
        if (story.getAuthor() != null && story.getAuthor().getId() != null) {
            Author author = authorService.findById(story.getAuthor().getId());
            story.setAuthor(author);
        }

        if (story.getSeries() != null && story.getSeries().getId() != null) {
            Series series = seriesService.findById(story.getSeries().getId());
            story.setSeries(series);
            validateSeriesVolume(series, story.getVolume());
        }

        Story savedStory = storyRepository.save(story);

        // Handle tags by names
        if (tagNames != null && !tagNames.isEmpty()) {
            updateStoryTagsByNames(savedStory, tagNames);
        }

        // Index AFTER the transaction commits (same reason as create() above).
        scheduleIndexAfterCommit(savedStory);

        return savedStory;
    }

    /**
     * Schedules Solr indexing to run immediately after the current transaction commits.
     *
     * <p>Indexing inside the transaction would produce an incomplete document:
     * <ul>
     *   <li>Hibernate's {@code @CreationTimestamp} / {@code @UpdateTimestamp} are set during flush
     *       (just before the INSERT), so they are {@code null} until then.</li>
     *   <li>Tag and relationship join-table rows are not yet written to the DB.</li>
     * </ul>
     * By using {@code afterCommit}, the flush has completed, all timestamps and associations
     * are present on the (now detached) entity, and we index a consistent snapshot.</p>
     *
     * <p>Falls back to immediate indexing when no transaction is active.</p>
     */
    private void scheduleIndexAfterCommit(Story story) {
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    searchServiceAdapter.indexStory(story);
                }
            });
        } else {
            searchServiceAdapter.indexStory(story);
        }
    }

    public Story update(UUID id, @Valid Story storyUpdates) {
        Story existingStory = findById(id);
        
        // Check for source URL conflicts if URL is being changed
        if (storyUpdates.getSourceUrl() != null && 
            !storyUpdates.getSourceUrl().equals(existingStory.getSourceUrl()) &&
            storyRepository.existsBySourceUrl(storyUpdates.getSourceUrl())) {
            throw new DuplicateResourceException("Story with source URL", storyUpdates.getSourceUrl());
        }

        updateStoryFields(existingStory, storyUpdates);
        Story updatedStory = storyRepository.save(existingStory);
        
        // Update in search engine
        searchServiceAdapter.updateStory(updatedStory);
        
        return updatedStory;
    }

    public Story updateWithTagNames(UUID id, Object request) {
        Story existingStory = findById(id);
        
        // Update basic fields
        updateStoryFieldsFromRequest(existingStory, request);
        
        // Handle tags if it's an update request with tag names
        if (request instanceof com.storycove.controller.StoryController.UpdateStoryRequest updateReq) {
            if (updateReq.getTagNames() != null) {
                updateStoryTagsByNames(existingStory, updateReq.getTagNames());
            }
        }
        
        Story updatedStory = storyRepository.save(existingStory);
        
        // Update in search engine
        searchServiceAdapter.updateStory(updatedStory);
        
        return updatedStory;
    }

    public Story updateContentOnly(UUID id, String contentHtml) {
        Story existingStory = findById(id);
        existingStory.setContentHtml(contentHtml);

        Story updatedStory = storyRepository.save(existingStory);

        // Update in search engine since content changed
        searchServiceAdapter.updateStory(updatedStory);

        return updatedStory;
    }

    public void delete(UUID id) {
        Story story = findById(id);
        
        // Clean up reading positions first (to avoid foreign key constraint violations)
        readingPositionRepository.deleteByStoryId(id);
        
        // Remove from series if part of one
        if (story.getSeries() != null) {
            story.getSeries().removeStory(story);
        }
        
        // Remove tags (this will update tag usage counts)
        // Create a copy to avoid ConcurrentModificationException
        new ArrayList<>(story.getTags()).forEach(tag -> story.removeTag(tag));
        
        // Delete from search engine first
        searchServiceAdapter.deleteStory(story.getId());
        
        storyRepository.delete(story);
    }

    public Story setCover(UUID id, String coverPath) {
        Story story = findById(id);
        story.setCoverPath(coverPath);
        return storyRepository.save(story);
    }

    public Story removeCover(UUID id) {
        Story story = findById(id);
        story.setCoverPath(null);
        return storyRepository.save(story);
    }

    public Story addToSeries(UUID storyId, UUID seriesId, Double volume) {
        Story story = findById(storyId);
        Series series = seriesService.findById(seriesId);
        
        validateSeriesVolume(series, volume);
        
        story.setSeries(series);
        story.setVolume(volume);
        series.addStory(story);
        
        return storyRepository.save(story);
    }

    public Story removeFromSeries(UUID storyId) {
        Story story = findById(storyId);
        
        if (story.getSeries() != null) {
            story.getSeries().removeStory(story);
            story.setSeries(null);
            story.setVolume(null);
        }
        
        return storyRepository.save(story);
    }

    @Transactional(readOnly = true)
    public boolean existsBySourceUrl(String sourceUrl) {
        return storyRepository.existsBySourceUrl(sourceUrl);
    }

    @Transactional(readOnly = true)
    public Double getAverageWordCount() {
        return storyRepository.findAverageWordCount();
    }

    @Transactional(readOnly = true)
    public Double getOverallAverageRating() {
        return storyRepository.findOverallAverageRating();
    }

    @Transactional(readOnly = true)
    public Long getTotalWordCount() {
        return storyRepository.findTotalWordCount();
    }

    private void validateStoryForCreate(Story story) {
        if (story.getSourceUrl() != null && existsBySourceUrl(story.getSourceUrl())) {
            throw new DuplicateResourceException("Story with source URL", story.getSourceUrl());
        }
    }

    private void validateSeriesVolume(Series series, Double volume) {
        if (volume != null) {
            Optional<Story> existingPart = storyRepository.findBySeriesAndVolume(series.getId(), volume);
            if (existingPart.isPresent()) {
                throw new DuplicateResourceException("Story", "volume " + volume + " of series " + series.getName());
            }
        }
    }

    private void updateStoryFields(Story existing, Story updates) {
        if (updates.getTitle() != null) {
            existing.setTitle(updates.getTitle());
        }
        if (updates.getSummary() != null) {
            existing.setSummary(updates.getSummary());
        }
        if (updates.getDescription() != null) {
            existing.setDescription(updates.getDescription());
        }
        if (updates.getContentHtml() != null) {
            existing.setContentHtml(updates.getContentHtml());
        }
        if (updates.getSourceUrl() != null) {
            existing.setSourceUrl(updates.getSourceUrl());
        }
        if (updates.getCoverPath() != null) {
            existing.setCoverPath(updates.getCoverPath());
        }
        if (updates.getVolume() != null) {
            existing.setVolume(updates.getVolume());
        }
        
        // Handle author update
        if (updates.getAuthor() != null && updates.getAuthor().getId() != null) {
            Author author = authorService.findById(updates.getAuthor().getId());
            existing.setAuthor(author);
        }
        
        // Handle series update
        if (updates.getSeries() != null && updates.getSeries().getId() != null) {
            Series series = seriesService.findById(updates.getSeries().getId());
            existing.setSeries(series);
            if (updates.getVolume() != null) {
                validateSeriesVolume(series, updates.getVolume());
                existing.setVolume(updates.getVolume());
            }
        }
        
        // Handle tags update
        if (updates.getTags() != null) {
            updateStoryTags(existing, updates.getTags());
        }
    }

    private void updateStoryTags(Story story, Set<Tag> newTags) {
        // Remove existing tags - create a copy to avoid ConcurrentModificationException
        Set<Tag> existingTags = new HashSet<>(story.getTags());
        existingTags.forEach(tag -> story.removeTag(tag));
        
        // Add new tags
        for (Tag tag : newTags) {
            Tag managedTag;
            if (tag.getId() != null) {
                managedTag = tagService.findById(tag.getId());
            } else {
                // Try to find existing tag by name or create new one
                managedTag = tagService.findByNameOptional(tag.getName())
                        .orElseGet(() -> tagService.create(tag));
            }
            story.addTag(managedTag);
        }
    }

    private void updateStoryFieldsFromRequest(Story story, Object request) {
        if (request instanceof com.storycove.controller.StoryController.UpdateStoryRequest updateReq) {
            if (updateReq.getTitle() != null) {
                story.setTitle(updateReq.getTitle());
            }
            if (updateReq.getSummary() != null) {
                story.setSummary(updateReq.getSummary());
            }
            if (updateReq.getContentHtml() != null) {
                story.setContentHtml(sanitizationService.sanitize(updateReq.getContentHtml()));
            }
            if (updateReq.getSourceUrl() != null) {
                story.setSourceUrl(updateReq.getSourceUrl());
            }
            if (updateReq.getVolume() != null) {
                story.setVolume(updateReq.getVolume());
            }
            // Handle author - either by ID or by name  
            if (updateReq.getAuthorId() != null) {
                Author author = authorService.findById(updateReq.getAuthorId());
                story.setAuthor(author);
            }
            // Handle series - either by ID or by name
            if (updateReq.getSeriesId() != null) {
                Series series = seriesService.findById(updateReq.getSeriesId());
                story.setSeries(series);
            } else if (updateReq.getSeriesName() != null) {
                if (updateReq.getSeriesName().trim().isEmpty()) {
                    // Empty series name means remove from series
                    story.setSeries(null);
                } else {
                    // Find or create series by name
                    Series series = seriesService.findByNameOptional(updateReq.getSeriesName().trim())
                            .orElseGet(() -> {
                                Series newSeries = new Series();
                                newSeries.setName(updateReq.getSeriesName().trim());
                                return seriesService.create(newSeries);
                            });
                    story.setSeries(series);
                }
            }
        }
    }
    
    private void updateStoryTagsByNames(Story story, java.util.List<String> tagNames) {
        // Clear existing tags first
        Set<Tag> existingTags = new HashSet<>(story.getTags());
        for (Tag existingTag : existingTags) {
            story.removeTag(existingTag);
        }
        
        // Add new tags
        for (String tagName : tagNames) {
            if (tagName != null && !tagName.trim().isEmpty()) {
                Tag tag = tagService.findByNameOptional(tagName.trim().toLowerCase())
                        .orElseGet(() -> {
                            Tag newTag = new Tag();
                            newTag.setName(tagName.trim().toLowerCase());
                            return tagService.create(newTag);
                        });
                story.addTag(tag);
            }
        }
    }
    
    @Transactional(readOnly = true)
    public List<Story> findPotentialDuplicates(String title, String authorName) {
        if (title == null || title.trim().isEmpty() || authorName == null || authorName.trim().isEmpty()) {
            return List.of();
        }
        return storyRepository.findByTitleAndAuthorNameIgnoreCase(title.trim(), authorName.trim());
    }
    
    /**
     * Find a random story based on optional filters.
     * Uses search service for consistency with Library search functionality.
     * Supports text search and multiple tags using the same logic as the Library view.
     * @param searchQuery Optional search query
     * @param tags Optional list of tags to filter by
     * @return Optional containing the random story if found
     */
    @Transactional(readOnly = true)
    public Optional<Story> findRandomStory(String searchQuery, List<String> tags) {
        return findRandomStory(searchQuery, tags, null, null, null, null, null, null, null,
                null, null, null, null, null, null, null, null, null, null, null);
    }
    
    public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed) {
        return findRandomStory(searchQuery, tags, seed, null, null, null, null, null, null,
                null, null, null, null, null, null, null, null, null, null, null);
    }

    /**
     * Find a random story based on optional filters with seed support.
     * Uses search service for consistency with Library search functionality.
     * Supports text search and multiple tags using the same logic as the Library view.
     * @param searchQuery Optional search query
     * @param tags Optional list of tags to filter by
     * @param seed Optional seed for consistent randomization (null for truly random)
     * @return Optional containing the random story if found
     */
    @Transactional(readOnly = true)
    public Optional<Story> findRandomStory(String searchQuery, List<String> tags, Long seed,
                                         Integer minWordCount, Integer maxWordCount, 
                                         String createdAfter, String createdBefore,
                                         String lastReadAfter, String lastReadBefore,
                                         Integer minRating, Integer maxRating, Boolean unratedOnly,
                                         String readingStatus, Boolean hasReadingProgress, 
                                         Boolean hasCoverImage, String sourceDomain, 
                                         String seriesFilter, Integer minTagCount,
                                         Boolean popularOnly, Boolean hiddenGemsOnly) {
        
        // Use search service for consistency with Library search
        try {
            String randomStoryId = searchServiceAdapter.getRandomStoryId(seed);
            if (randomStoryId != null) {
                return storyRepository.findById(UUID.fromString(randomStoryId));
            }
            return Optional.empty();
        } catch (Exception e) {
            // Fallback to database queries if search service fails
            logger.warn("Search service random story lookup failed, falling back to database queries", e);
        }
        
        // Fallback to repository-based implementation (global routing handles library selection)
        return findRandomStoryFromRepository(searchQuery, tags);
    }
    
    
    /**
     * Find random story using repository methods (for default database or when library-aware fails)
     */
    private Optional<Story> findRandomStoryFromRepository(String searchQuery, List<String> tags) {
        // Clean up inputs
        String cleanSearchQuery = (searchQuery != null && !searchQuery.trim().isEmpty()) ? searchQuery.trim() : null;
        List<String> cleanTags = (tags != null) ? tags.stream()
            .filter(tag -> tag != null && !tag.trim().isEmpty())
            .map(String::trim)
            .collect(Collectors.toList()) : List.of();
        
        long totalCount = 0;
        Optional<Story> randomStory = Optional.empty();
        
        if (cleanSearchQuery != null && !cleanTags.isEmpty()) {
            // Both search query and tags
            String searchPattern = "%" + cleanSearchQuery + "%";
            List<String> upperCaseTags = cleanTags.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());
            
            totalCount = storyRepository.countStoriesByTextSearchAndTags(searchPattern, upperCaseTags, cleanTags.size());
            if (totalCount > 0) {
                long randomOffset = (long) (Math.random() * totalCount);
                randomStory = storyRepository.findRandomStoryByTextSearchAndTags(searchPattern, upperCaseTags, cleanTags.size(), randomOffset);
            }
            
        } else if (cleanSearchQuery != null) {
            // Only search query
            String searchPattern = "%" + cleanSearchQuery + "%";
            totalCount = storyRepository.countStoriesByTextSearch(searchPattern);
            if (totalCount > 0) {
                long randomOffset = (long) (Math.random() * totalCount);
                randomStory = storyRepository.findRandomStoryByTextSearch(searchPattern, randomOffset);
            }
            
        } else if (!cleanTags.isEmpty()) {
            // Only tags
            if (cleanTags.size() == 1) {
                // Single tag - use optimized single tag query
                totalCount = storyRepository.countStoriesByTagName(cleanTags.get(0));
                if (totalCount > 0) {
                    long randomOffset = (long) (Math.random() * totalCount);
                    randomStory = storyRepository.findRandomStoryByTagName(cleanTags.get(0), randomOffset);
                }
            } else {
                // Multiple tags
                List<String> upperCaseTags = cleanTags.stream()
                    .map(String::toUpperCase)
                    .collect(Collectors.toList());
                
                totalCount = storyRepository.countStoriesByMultipleTags(upperCaseTags, cleanTags.size());
                if (totalCount > 0) {
                    long randomOffset = (long) (Math.random() * totalCount);
                    randomStory = storyRepository.findRandomStoryByMultipleTags(upperCaseTags, cleanTags.size(), randomOffset);
                }
            }
            
        } else {
            // No filters - get random from all stories
            totalCount = storyRepository.countAllStories();
            if (totalCount > 0) {
                long randomOffset = (long) (Math.random() * totalCount);
                randomStory = storyRepository.findRandomStory(randomOffset);
            }
        }
        
        return randomStory;
    }
    
    
}