package com.storycove.service;

import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import com.storycove.entity.TagAlias;
import com.storycove.repository.TagRepository;
import com.storycove.repository.TagAliasRepository;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

@Service
@Validated
@Transactional
public class TagService {

    private static final Logger logger = LoggerFactory.getLogger(TagService.class);

    private final TagRepository tagRepository;
    private final TagAliasRepository tagAliasRepository;
    private SolrService solrService;

    @Autowired
    public TagService(TagRepository tagRepository, TagAliasRepository tagAliasRepository) {
        this.tagRepository = tagRepository;
        this.tagAliasRepository = tagAliasRepository;
    }

    @Autowired(required = false)
    public void setSolrService(SolrService solrService) {
        this.solrService = solrService;
    }

    @Transactional(readOnly = true)
    public List<Tag> findAll() {
        return tagRepository.findAll();
    }

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

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

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

    @Transactional(readOnly = true)
    public Tag findByName(String name) {
        return tagRepository.findByName(name)
                .orElseThrow(() -> new ResourceNotFoundException("Tag", name));
    }

    @Transactional(readOnly = true)
    public Optional<Tag> findByNameOptional(String name) {
        return tagRepository.findByName(name);
    }

    @Transactional(readOnly = true)
    public List<Tag> searchByName(String name) {
        return tagRepository.findByNameContainingIgnoreCase(name);
    }

    @Transactional(readOnly = true)
    public Page<Tag> searchByName(String name, Pageable pageable) {
        return tagRepository.findByNameContainingIgnoreCase(name, pageable);
    }

    @Transactional(readOnly = true)
    public List<Tag> findUsedTags() {
        return tagRepository.findUsedTags();
    }

    @Transactional(readOnly = true)
    public Page<Tag> findUsedTags(Pageable pageable) {
        return tagRepository.findUsedTags(pageable);
    }

    @Transactional(readOnly = true)
    public List<Tag> findMostUsedTags() {
        return tagRepository.findMostUsedTags();
    }

    @Transactional(readOnly = true)
    public Page<Tag> findMostUsedTags(Pageable pageable) {
        return tagRepository.findMostUsedTags(pageable);
    }

    @Transactional(readOnly = true)
    public List<Tag> findPopularTags(Integer threshold) {
        return tagRepository.findPopularTags(threshold);
    }

    @Transactional(readOnly = true)
    public List<Tag> findUnusedTags() {
        return tagRepository.findUnusedTags();
    }

    @Transactional(readOnly = true)
    public List<Tag> findByNames(List<String> names) {
        return tagRepository.findByNames(names);
    }

    @Transactional(readOnly = true)
    public boolean existsByName(String name) {
        return tagRepository.existsByName(name);
    }

    public Tag create(@Valid Tag tag) {
        validateTagForCreate(tag);
        return tagRepository.save(tag);
    }

    public Tag update(UUID id, @Valid Tag tagUpdates) {
        Tag existingTag = findById(id);
        
        // Check for name conflicts if name is being changed
        if (!existingTag.getName().equals(tagUpdates.getName()) && 
            existsByName(tagUpdates.getName())) {
            throw new DuplicateResourceException("Tag", tagUpdates.getName());
        }

        updateTagFields(existingTag, tagUpdates);
        return tagRepository.save(existingTag);
    }

    public void delete(UUID id) {
        Tag tag = findById(id);

        // Remove tag from all stories before deletion and track for reindexing
        List<Story> storiesToReindex = new ArrayList<>();
        if (!tag.getStories().isEmpty()) {
            // Create a copy to avoid ConcurrentModificationException
            List<Story> storiesToUpdate = new ArrayList<>(tag.getStories());
            storiesToUpdate.forEach(story -> {
                story.removeTag(tag);
                storiesToReindex.add(story);
            });
            logger.info("Removed tag '{}' from {} stories before deletion", tag.getName(), storiesToUpdate.size());
        }

        // Remove tag from all collections before deletion
        if (tag.getCollections() != null && !tag.getCollections().isEmpty()) {
            tag.getCollections().forEach(collection -> collection.getTags().remove(tag));
            logger.info("Removed tag '{}' from {} collections before deletion", tag.getName(), tag.getCollections().size());
        }

        tagRepository.delete(tag);
        logger.info("Deleted tag '{}'", tag.getName());

        // Reindex affected stories in Solr
        if (solrService != null && !storiesToReindex.isEmpty()) {
            try {
                for (Story story : storiesToReindex) {
                    solrService.indexStory(story);
                }
                logger.info("Reindexed {} stories after tag deletion", storiesToReindex.size());
            } catch (Exception e) {
                logger.error("Failed to reindex stories after tag deletion", e);
            }
        }
    }

    public List<Tag> deleteUnusedTags() {
        List<Tag> unusedTags = findUnusedTags();
        tagRepository.deleteAll(unusedTags);
        return unusedTags;
    }

    public Tag findOrCreate(String name) {
        return findByNameOptional(name)
                .orElseGet(() -> create(new Tag(name)));
    }

    // Method removed - Tag doesn't have description field per spec

    @Transactional(readOnly = true)
    public long countRecentTags(int days) {
        java.time.LocalDateTime cutoffDate = java.time.LocalDateTime.now().minusDays(days);
        return tagRepository.countRecentTags(cutoffDate);
    }

    @Transactional(readOnly = true)
    public long getTotalCount() {
        return tagRepository.count();
    }

    @Transactional(readOnly = true)
    public long getUsedTagCount() {
        return findUsedTags().size();
    }
    
    @Transactional(readOnly = true)
    public List<Tag> findByNameStartingWith(String prefix, int limit) {
        return tagRepository.findByNameStartingWithIgnoreCase(prefix).stream()
                .limit(limit)
                .collect(java.util.stream.Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    public List<Tag> findMostUsed(int limit) {
        return tagRepository.findMostUsedTags().stream()
                .limit(limit)
                .collect(java.util.stream.Collectors.toList());
    }
    
    @Transactional(readOnly = true)
    public long countAll() {
        return tagRepository.count();
    }
    
    @Transactional(readOnly = true)
    public long countUsedTags() {
        return tagRepository.countUsedTags();
    }
    
    @Transactional(readOnly = true)
    public List<Tag> findTagsUsedByCollections() {
        return tagRepository.findTagsUsedByCollections();
    }

    private void validateTagForCreate(Tag tag) {
        if (existsByName(tag.getName())) {
            throw new DuplicateResourceException("Tag", tag.getName());
        }
    }

    private void updateTagFields(Tag existing, Tag updates) {
        if (updates.getName() != null) {
            existing.setName(updates.getName());
        }
        if (updates.getColor() != null) {
            existing.setColor(updates.getColor());
        }
        if (updates.getDescription() != null) {
            existing.setDescription(updates.getDescription());
        }
    }
    
    // Tag alias management methods
    
    public TagAlias addAlias(UUID tagId, String aliasName) {
        Tag canonicalTag = findById(tagId);
        
        // Check if alias already exists (case-insensitive)
        if (tagAliasRepository.existsByAliasNameIgnoreCase(aliasName)) {
            throw new DuplicateResourceException("Tag alias", aliasName);
        }
        
        // Check if alias name conflicts with existing tag names
        if (tagRepository.existsByNameIgnoreCase(aliasName)) {
            throw new DuplicateResourceException("Tag alias conflicts with existing tag name", aliasName);
        }
        
        TagAlias alias = new TagAlias();
        alias.setAliasName(aliasName);
        alias.setCanonicalTag(canonicalTag);
        alias.setCreatedFromMerge(false);
        
        return tagAliasRepository.save(alias);
    }
    
    public void removeAlias(UUID tagId, UUID aliasId) {
        findById(tagId); // Validate tag exists
        TagAlias alias = tagAliasRepository.findById(aliasId)
            .orElseThrow(() -> new ResourceNotFoundException("Tag alias", aliasId.toString()));
        
        // Verify the alias belongs to the specified tag
        if (!alias.getCanonicalTag().getId().equals(tagId)) {
            throw new IllegalArgumentException("Alias does not belong to the specified tag");
        }
        
        tagAliasRepository.delete(alias);
    }
    
    @Transactional(readOnly = true)
    public Tag resolveTagByName(String name) {
        // First try to find exact tag match
        Optional<Tag> directMatch = tagRepository.findByNameIgnoreCase(name);
        if (directMatch.isPresent()) {
            return directMatch.get();
        }
        
        // Then try to find by alias
        Optional<TagAlias> aliasMatch = tagAliasRepository.findByAliasNameIgnoreCase(name);
        if (aliasMatch.isPresent()) {
            return aliasMatch.get().getCanonicalTag();
        }
        
        return null;
    }
    
    @Transactional
    public Tag mergeTags(List<UUID> sourceTagIds, UUID targetTagId) {
        // Validate target tag exists
        Tag targetTag = findById(targetTagId);
        
        // Validate source tags exist and are different from target
        List<Tag> sourceTags = sourceTagIds.stream()
            .filter(id -> !id.equals(targetTagId)) // Don't merge tag with itself
            .map(this::findById)
            .toList();
        
        if (sourceTags.isEmpty()) {
            throw new IllegalArgumentException("No valid source tags to merge");
        }
        
        // Perform the merge atomically
        for (Tag sourceTag : sourceTags) {
            // Move all stories from source tag to target tag
            // Create a copy to avoid ConcurrentModificationException
            List<Story> storiesToMove = new ArrayList<>(sourceTag.getStories());
            storiesToMove.forEach(story -> {
                story.removeTag(sourceTag);
                story.addTag(targetTag);
            });
            
            // Create alias for the source tag name
            TagAlias alias = new TagAlias();
            alias.setAliasName(sourceTag.getName());
            alias.setCanonicalTag(targetTag);
            alias.setCreatedFromMerge(true);
            tagAliasRepository.save(alias);
            
            // Delete the source tag
            tagRepository.delete(sourceTag);
        }
        
        return tagRepository.save(targetTag);
    }
    
    @Transactional(readOnly = true)
    public List<Tag> findByNameOrAliasStartingWith(String query, int limit) {
        // Find tags that start with the query
        List<Tag> directMatches = tagRepository.findByNameStartingWithIgnoreCase(query.toLowerCase());
        
        // Find tags via aliases that start with the query
        List<TagAlias> aliasMatches = tagAliasRepository.findByAliasNameStartingWithIgnoreCase(query.toLowerCase());
        List<Tag> aliasTagMatches = aliasMatches.stream()
            .map(TagAlias::getCanonicalTag)
            .distinct()
            .toList();
        
        // Combine and deduplicate
        Set<Tag> allMatches = new HashSet<>(directMatches);
        allMatches.addAll(aliasTagMatches);
        
        // Convert to list and limit results
        return allMatches.stream()
            .sorted((a, b) -> a.getName().compareToIgnoreCase(b.getName()))
            .limit(limit)
            .toList();
    }
    
    @Transactional(readOnly = true)
    public com.storycove.controller.TagController.MergePreviewResponse previewMerge(List<UUID> sourceTagIds, UUID targetTagId) {
        // Validate target tag exists
        Tag targetTag = findById(targetTagId);
        
        // Validate source tags exist and are different from target
        List<Tag> sourceTags = sourceTagIds.stream()
            .filter(id -> !id.equals(targetTagId))
            .map(this::findById)
            .toList();
        
        if (sourceTags.isEmpty()) {
            throw new IllegalArgumentException("No valid source tags to merge");
        }
        
        // Calculate preview data
        int targetStoryCount = targetTag.getStories().size();
        
        // Collect all unique stories from all tags (including target) to handle overlaps correctly
        Set<Story> allUniqueStories = new HashSet<>(targetTag.getStories());
        for (Tag sourceTag : sourceTags) {
            allUniqueStories.addAll(sourceTag.getStories());
        }
        int totalStories = allUniqueStories.size();
        
        List<String> aliasesToCreate = sourceTags.stream()
            .map(Tag::getName)
            .toList();
        
        // Create response object using the controller's inner class
        var preview = new com.storycove.controller.TagController.MergePreviewResponse();
        preview.setTargetTagName(targetTag.getName());
        preview.setTargetStoryCount(targetStoryCount);
        preview.setTotalResultStoryCount(totalStories);
        preview.setAliasesToCreate(aliasesToCreate);
        
        return preview;
    }
    
    @Transactional(readOnly = true)
    public List<com.storycove.controller.TagController.TagSuggestion> suggestTags(String title, String content, String summary, int limit) {
        List<com.storycove.controller.TagController.TagSuggestion> suggestions = new ArrayList<>();
        
        // Get all existing tags for matching
        List<Tag> existingTags = findAll();
        
        // Combine all text for analysis
        String combinedText = (title != null ? title : "") + " " + 
                             (summary != null ? summary : "") + " " + 
                             (content != null ? stripHtml(content) : "");
        
        if (combinedText.trim().isEmpty()) {
            return suggestions;
        }
        
        String lowerText = combinedText.toLowerCase();
        
        // Score each existing tag based on how well it matches the content
        for (Tag tag : existingTags) {
            double score = calculateTagRelevanceScore(tag, lowerText, title, summary);
            
            if (score > 0.1) { // Only suggest tags with reasonable confidence
                String reason = generateReason(tag, lowerText, title, summary);
                suggestions.add(new com.storycove.controller.TagController.TagSuggestion(
                    tag.getName(), score, reason
                ));
            }
        }
        
        // Sort by confidence score (descending) and limit results
        return suggestions.stream()
            .sorted((a, b) -> Double.compare(b.getConfidence(), a.getConfidence()))
            .limit(limit)
            .collect(java.util.stream.Collectors.toList());
    }
    
    private double calculateTagRelevanceScore(Tag tag, String lowerText, String title, String summary) {
        String tagName = tag.getName().toLowerCase();
        double score = 0.0;
        
        // Exact matches get highest score
        if (lowerText.contains(" " + tagName + " ") || lowerText.startsWith(tagName + " ") || lowerText.endsWith(" " + tagName)) {
            score += 0.8;
        }
        
        // Partial matches in title get high score
        if (title != null && title.toLowerCase().contains(tagName)) {
            score += 0.6;
        }
        
        // Partial matches in summary get medium score
        if (summary != null && summary.toLowerCase().contains(tagName)) {
            score += 0.4;
        }
        
        // Word-based matching (split tag name and look for individual words)
        String[] tagWords = tagName.split("[\\s-_]+");
        int matchedWords = 0;
        for (String word : tagWords) {
            if (word.length() > 2 && lowerText.contains(word)) {
                matchedWords++;
            }
        }
        if (tagWords.length > 0) {
            score += 0.3 * ((double) matchedWords / tagWords.length);
        }
        
        // Boost score based on tag popularity (more used tags are more likely to be relevant)
        int storyCount = tag.getStories() != null ? tag.getStories().size() : 0;
        if (storyCount > 0) {
            score += Math.min(0.2, storyCount * 0.01); // Small boost, capped at 0.2
        }
        
        return Math.min(1.0, score); // Cap at 1.0
    }
    
    private String generateReason(Tag tag, String lowerText, String title, String summary) {
        String tagName = tag.getName().toLowerCase();
        
        if (title != null && title.toLowerCase().contains(tagName)) {
            return "Found in title";
        }
        
        if (summary != null && summary.toLowerCase().contains(tagName)) {
            return "Found in summary";
        }
        
        if (lowerText.contains(" " + tagName + " ") || lowerText.startsWith(tagName + " ") || lowerText.endsWith(" " + tagName)) {
            return "Exact match in content";
        }
        
        String[] tagWords = tagName.split("[\\s-_]+");
        for (String word : tagWords) {
            if (word.length() > 2 && lowerText.contains(word)) {
                return "Related keywords found";
            }
        }
        
        return "Similar content";
    }
    
    private String stripHtml(String html) {
        if (html == null) return "";
        // Simple HTML tag removal - replace with a proper HTML parser if needed
        return html.replaceAll("<[^>]+>", " ").replaceAll("\\s+", " ").trim();
    }
}