package com.storycove.controller;

import com.storycove.dto.*;
import com.storycove.entity.Author;
import com.storycove.entity.Collection;
import com.storycove.entity.Series;
import com.storycove.entity.Story;
import com.storycove.entity.Tag;
import com.storycove.dto.SearchResultDto;
import com.storycove.dto.StorySearchDto;
import com.storycove.service.*;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/stories")
public class StoryController {
    
    private static final Logger logger = LoggerFactory.getLogger(StoryController.class);
    
    private final StoryService storyService;
    private final AuthorService authorService;
    private final SeriesService seriesService;
    private final HtmlSanitizationService sanitizationService;
    private final ImageService imageService;
    private final SearchServiceAdapter searchServiceAdapter;
    private final CollectionService collectionService;
    private final ReadingTimeService readingTimeService;
    private final EPUBImportService epubImportService;
    private final EPUBExportService epubExportService;
    private final PDFImportService pdfImportService;
    private final ZIPImportService zipImportService;
    private final AsyncImageProcessingService asyncImageProcessingService;
    private final ImageProcessingProgressService progressService;
    private final AudioCoveService audioCoveService;

    public StoryController(StoryService storyService,
                          AuthorService authorService,
                          SeriesService seriesService,
                          HtmlSanitizationService sanitizationService,
                          ImageService imageService,
                          CollectionService collectionService,
                          SearchServiceAdapter searchServiceAdapter,
                          ReadingTimeService readingTimeService,
                          EPUBImportService epubImportService,
                          EPUBExportService epubExportService,
                          PDFImportService pdfImportService,
                          ZIPImportService zipImportService,
                          AsyncImageProcessingService asyncImageProcessingService,
                          ImageProcessingProgressService progressService,
                          AudioCoveService audioCoveService) {
        this.storyService = storyService;
        this.authorService = authorService;
        this.seriesService = seriesService;
        this.sanitizationService = sanitizationService;
        this.imageService = imageService;
        this.collectionService = collectionService;
        this.searchServiceAdapter = searchServiceAdapter;
        this.readingTimeService = readingTimeService;
        this.epubImportService = epubImportService;
        this.epubExportService = epubExportService;
        this.pdfImportService = pdfImportService;
        this.zipImportService = zipImportService;
        this.asyncImageProcessingService = asyncImageProcessingService;
        this.progressService = progressService;
        this.audioCoveService = audioCoveService;
    }
    
    @GetMapping
    public ResponseEntity<Page<StorySummaryDto>> getAllStories(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "createdAt") String sortBy,
            @RequestParam(defaultValue = "desc") String sortDir) {
        
        Sort sort = sortDir.equalsIgnoreCase("desc") ? 
            Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
        
        Pageable pageable = PageRequest.of(page, size, sort);
        Page<Story> stories = storyService.findAll(pageable);
        Page<StorySummaryDto> storyDtos = stories.map(this::convertToSummaryDto);
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/random")
    public ResponseEntity<StorySummaryDto> getRandomStory(
            @RequestParam(required = false) String searchQuery,
            @RequestParam(required = false) List<String> tags,
            @RequestParam(required = false) Long seed,
            // Advanced filters
            @RequestParam(required = false) Integer minWordCount,
            @RequestParam(required = false) Integer maxWordCount,
            @RequestParam(required = false) String createdAfter,
            @RequestParam(required = false) String createdBefore,
            @RequestParam(required = false) String lastReadAfter,
            @RequestParam(required = false) String lastReadBefore,
            @RequestParam(required = false) Integer minRating,
            @RequestParam(required = false) Integer maxRating,
            @RequestParam(required = false) Boolean unratedOnly,
            @RequestParam(required = false) String readingStatus,
            @RequestParam(required = false) Boolean hasReadingProgress,
            @RequestParam(required = false) Boolean hasCoverImage,
            @RequestParam(required = false) String sourceDomain,
            @RequestParam(required = false) String seriesFilter,
            @RequestParam(required = false) Integer minTagCount,
            @RequestParam(required = false) Boolean popularOnly,
            @RequestParam(required = false) Boolean hiddenGemsOnly) {
        
        logger.info("Getting random story with filters - searchQuery: {}, tags: {}, seed: {}",
                   searchQuery, tags, seed);
        
        Optional<Story> randomStory = storyService.findRandomStory(searchQuery, tags, seed,
                minWordCount, maxWordCount, createdAfter, createdBefore, lastReadAfter, lastReadBefore,
                minRating, maxRating, unratedOnly, readingStatus, hasReadingProgress, hasCoverImage,
                sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
        
        if (randomStory.isPresent()) {
            StorySummaryDto storyDto = convertToSummaryDto(randomStory.get());
            return ResponseEntity.ok(storyDto);
        } else {
            return ResponseEntity.noContent().build(); // 204 No Content when no stories match filters
        }
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<StoryDto> getStoryById(@PathVariable UUID id) {
        Story story = storyService.findById(id);
        return ResponseEntity.ok(convertToDto(story));
    }
    
    @GetMapping("/{id}/read")
    public ResponseEntity<StoryReadingDto> getStoryForReading(@PathVariable UUID id) {
        logger.info("Getting story {} for reading", id);
        Story story = storyService.findById(id);
        return ResponseEntity.ok(convertToReadingDto(story));
    }
    
    @PostMapping
    public ResponseEntity<StoryDto> createStory(@Valid @RequestBody CreateStoryRequest request) {
        logger.info("Creating new story: {}", request.getTitle());
        Story story = new Story();
        updateStoryFromRequest(story, request);

        Story savedStory = storyService.createWithTagNames(story, request.getTagNames());

        // Process external images in content after saving
        savedStory = processExternalImagesIfNeeded(savedStory);

        logger.info("Successfully created story: {} (ID: {})", savedStory.getTitle(), savedStory.getId());
        return ResponseEntity.status(HttpStatus.CREATED).body(convertToDto(savedStory));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<StoryDto> updateStory(@PathVariable UUID id,
                                               @Valid @RequestBody UpdateStoryRequest request) {
        logger.info("Updating story: {} (ID: {})", request.getTitle(), id);

        // Handle author creation/lookup at controller level before calling service
        if (request.getAuthorName() != null && !request.getAuthorName().trim().isEmpty() && request.getAuthorId() == null) {
            Author author = findOrCreateAuthor(request.getAuthorName().trim());
            request.setAuthorId(author.getId());
            request.setAuthorName(null); // Clear author name since we now have the ID
        }

        Story updatedStory = storyService.updateWithTagNames(id, request);

        // Image processing for updates is handled by the frontend via the dedicated
        // /process-content-images endpoint, so no async processing is triggered here.
        // (Async processing on every PUT caused concurrent duplicate downloads.)

        logger.info("Successfully updated story: {}", updatedStory.getTitle());
        return ResponseEntity.ok(convertToDto(updatedStory));
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteStory(@PathVariable UUID id) {
        logger.info("Deleting story with ID: {}", id);
        storyService.delete(id);
        logger.info("Successfully deleted story with ID: {}", id);
        return ResponseEntity.ok(Map.of("message", "Story deleted successfully"));
    }
    
    @PostMapping("/{id}/cover")
    public ResponseEntity<?> uploadCover(@PathVariable UUID id, @RequestParam("file") MultipartFile file) {
        try {
            String imagePath = imageService.uploadImage(file, ImageService.ImageType.COVER);
            Story story = storyService.setCoverImage(id, imagePath);
            
            return ResponseEntity.ok(Map.of(
                "message", "Cover uploaded successfully",
                "coverPath", story.getCoverPath(),
                "coverUrl", "/api/files/images/" + story.getCoverPath()
            ));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
        }
    }
    
    @DeleteMapping("/{id}/cover")
    public ResponseEntity<?> deleteCover(@PathVariable UUID id) {
        storyService.removeCoverImage(id);
        return ResponseEntity.ok(Map.of("message", "Cover removed successfully"));
    }
    
    @PostMapping("/{id}/tags/{tagId}")
    public ResponseEntity<StoryDto> addTag(@PathVariable UUID id, @PathVariable UUID tagId) {
        Story story = storyService.addTag(id, tagId);
        return ResponseEntity.ok(convertToDto(story));
    }
    
    @DeleteMapping("/{id}/tags/{tagId}")
    public ResponseEntity<StoryDto> removeTag(@PathVariable UUID id, @PathVariable UUID tagId) {
        Story story = storyService.removeTag(id, tagId);
        return ResponseEntity.ok(convertToDto(story));
    }
    
    @PostMapping("/{id}/rating")
    public ResponseEntity<StoryDto> rateStory(@PathVariable UUID id, @RequestBody RatingRequest request) {
        Story story = storyService.setRating(id, request.getRating());
        return ResponseEntity.ok(convertToDto(story));
    }
    
    @PostMapping("/{id}/reading-progress")
    public ResponseEntity<StoryDto> updateReadingProgress(@PathVariable UUID id, @RequestBody ReadingProgressRequest request) {
        logger.info("Updating reading progress for story {} to position {}", id, request.getPosition());
        Story story = storyService.updateReadingProgress(id, request.getPosition());
        return ResponseEntity.ok(convertToDto(story));
    }
    
    @PostMapping("/{id}/reading-status")
    public ResponseEntity<StoryDto> updateReadingStatus(@PathVariable UUID id, @RequestBody ReadingStatusRequest request) {
        logger.info("Updating reading status for story {} to {}", id, request.getIsRead() ? "read" : "unread");
        Story story = storyService.updateReadingStatus(id, request.getIsRead());
        return ResponseEntity.ok(convertToDto(story));
    }

    @PostMapping("/{id}/process-content-images")
    public ResponseEntity<Map<String, Object>> processContentImages(@PathVariable UUID id, @RequestBody ProcessContentImagesRequest request) {
        logger.info("Processing content images for story {}", id);

        try {
            // Process the HTML content to download and replace image URLs
            ImageService.ContentImageProcessingResult result = imageService.processContentImages(request.getHtmlContent(), id);

            // If there are warnings, let the client decide whether to proceed
            if (result.hasWarnings()) {
                return ResponseEntity.ok(Map.of(
                    "processedContent", result.getProcessedContent(),
                    "warnings", result.getWarnings(),
                    "downloadedImages", result.getDownloadedImages(),
                    "hasWarnings", true
                ));
            }

            // Success - no warnings
            return ResponseEntity.ok(Map.of(
                "processedContent", result.getProcessedContent(),
                "downloadedImages", result.getDownloadedImages(),
                "hasWarnings", false
            ));

        } catch (Exception e) {
            logger.error("Failed to process content images for story {}", id, e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Map.of("error", "Failed to process content images: " + e.getMessage()));
        }
    }
    
    @PostMapping("/reindex")
    public ResponseEntity<String> manualReindex() {
        
        try {
            List<Story> allStories = storyService.findAllWithAssociations();
            searchServiceAdapter.bulkIndexStories(allStories);
            return ResponseEntity.ok("Successfully reindexed " + allStories.size() + " stories");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Failed to reindex stories: " + e.getMessage());
        }
    }
    
    @PostMapping("/reindex-typesense")
    public ResponseEntity<Map<String, Object>> reindexStoriesTypesense() {
        try {
            List<Story> allStories = storyService.findAllWithAssociations();
            searchServiceAdapter.bulkIndexStories(allStories);
            return ResponseEntity.ok(Map.of(
                "success", true,
                "message", "Reindexed " + allStories.size() + " stories",
                "count", allStories.size()
            ));
        } catch (Exception e) {
            logger.error("Failed to reindex stories", e);
            return ResponseEntity.ok(Map.of(
                "success", false,
                "error", e.getMessage()
            ));
        }
    }
    
    @PostMapping("/recreate-typesense-collection")
    public ResponseEntity<Map<String, Object>> recreateStoriesCollection() {
        try {
            // This will delete the existing collection and recreate it with correct schema
            List<Story> allStories = storyService.findAllWithAssociations();
            searchServiceAdapter.bulkIndexStories(allStories);
            return ResponseEntity.ok(Map.of(
                "success", true,
                "message", "Recreated stories collection and indexed " + allStories.size() + " stories",
                "count", allStories.size()
            ));
        } catch (Exception e) {
            logger.error("Failed to recreate stories collection", e);
            return ResponseEntity.ok(Map.of(
                "success", false,
                "error", e.getMessage()
            ));
        }
    }
    
    @GetMapping("/search")
    public ResponseEntity<SearchResultDto<StorySearchDto>> searchStories(
            @RequestParam String query,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) List<String> authors,
            @RequestParam(required = false) List<String> tags,
            @RequestParam(required = false) Integer minRating,
            @RequestParam(required = false) Integer maxRating,
            @RequestParam(required = false) String sortBy,
            @RequestParam(required = false) String sortDir,
            @RequestParam(required = false) List<String> facetBy,
            // Advanced filters
            @RequestParam(required = false) Integer minWordCount,
            @RequestParam(required = false) Integer maxWordCount,
            @RequestParam(required = false) String createdAfter,
            @RequestParam(required = false) String createdBefore,
            @RequestParam(required = false) String lastReadAfter,
            @RequestParam(required = false) String lastReadBefore,
            @RequestParam(required = false) Boolean unratedOnly,
            @RequestParam(required = false) String readingStatus,
            @RequestParam(required = false) Boolean hasReadingProgress,
            @RequestParam(required = false) Boolean hasCoverImage,
            @RequestParam(required = false) String sourceDomain,
            @RequestParam(required = false) String seriesFilter,
            @RequestParam(required = false) Integer minTagCount,
            @RequestParam(required = false) Boolean popularOnly,
            @RequestParam(required = false) Boolean hiddenGemsOnly) {
        
        
        // Use SearchServiceAdapter to handle routing between search engines
        try {
            // Convert authors list to single author string (for now, use first author)
            String authorFilter = (authors != null && !authors.isEmpty()) ? authors.get(0) : null;

            // DEBUG: Log all received parameters
            logger.info("CONTROLLER DEBUG - Received parameters:");
            logger.info("  readingStatus: '{}'", readingStatus);
            logger.info("  seriesFilter: '{}'", seriesFilter);
            logger.info("  hasReadingProgress: {}", hasReadingProgress);
            logger.info("  hasCoverImage: {}", hasCoverImage);
            logger.info("  createdAfter: '{}'", createdAfter);
            logger.info("  lastReadAfter: '{}'", lastReadAfter);
            logger.info("  unratedOnly: {}", unratedOnly);

            SearchResultDto<StorySearchDto> results = searchServiceAdapter.searchStories(
                query, tags, authorFilter, seriesFilter, minWordCount, maxWordCount,
                minRating != null ? minRating.floatValue() : null,
                null, // isRead - now handled by readingStatus advanced filter
                null, // isFavorite - now handled by readingStatus advanced filter
                sortBy, sortDir, page, size, facetBy,
                // Advanced filters
                createdAfter, createdBefore, lastReadAfter, lastReadBefore,
                unratedOnly, readingStatus, hasReadingProgress, hasCoverImage,
                sourceDomain, seriesFilter, minTagCount, popularOnly, hiddenGemsOnly);
            return ResponseEntity.ok(results);
        } catch (Exception e) {
            logger.error("Search failed", e);
            return ResponseEntity.internalServerError().body(null);
        }
    }
    
    @GetMapping("/search/suggestions")
    public ResponseEntity<List<String>> getSearchSuggestions(
            @RequestParam String query,
            @RequestParam(defaultValue = "5") int limit) {
        
        // Use SearchServiceAdapter to handle routing between search engines
        try {
            List<String> suggestions = searchServiceAdapter.getTagSuggestions(query, limit);
            return ResponseEntity.ok(suggestions);
        } catch (Exception e) {
            logger.error("Failed to get search suggestions", e);
            return ResponseEntity.ok(new ArrayList<>());
        }
    }
    
    @GetMapping("/author/{authorId}")
    public ResponseEntity<Page<StorySummaryDto>> getStoriesByAuthor(
            @PathVariable UUID authorId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<Story> stories = storyService.findByAuthor(authorId, pageable);
        Page<StorySummaryDto> storyDtos = stories.map(this::convertToSummaryDto);
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/series/{seriesId}")
    public ResponseEntity<List<StorySummaryDto>> getStoriesBySeries(@PathVariable UUID seriesId) {
        List<Story> stories = storyService.findBySeriesOrderByVolume(seriesId);
        List<StorySummaryDto> storyDtos = stories.stream().map(this::convertToSummaryDto).collect(Collectors.toList());
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/tags/{tagName}")
    public ResponseEntity<Page<StorySummaryDto>> getStoriesByTag(
            @PathVariable String tagName,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        Page<Story> stories = storyService.findByTagNames(List.of(tagName), pageable);
        Page<StorySummaryDto> storyDtos = stories.map(this::convertToSummaryDto);
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/recent")
    public ResponseEntity<List<StorySummaryDto>> getRecentStories(@RequestParam(defaultValue = "10") int limit) {
        Pageable pageable = PageRequest.of(0, limit, Sort.by("createdAt").descending());
        List<Story> stories = storyService.findRecentlyAddedLimited(pageable);
        List<StorySummaryDto> storyDtos = stories.stream().map(this::convertToSummaryDto).collect(Collectors.toList());
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/top-rated")
    public ResponseEntity<List<StorySummaryDto>> getTopRatedStories(@RequestParam(defaultValue = "10") int limit) {
        Pageable pageable = PageRequest.of(0, limit);
        List<Story> stories = storyService.findTopRatedStoriesLimited(pageable);
        List<StorySummaryDto> storyDtos = stories.stream().map(this::convertToSummaryDto).collect(Collectors.toList());
        
        return ResponseEntity.ok(storyDtos);
    }
    
    @GetMapping("/{id}/collections")
    public ResponseEntity<List<CollectionDto>> getStoryCollections(@PathVariable UUID id) {
        List<Collection> collections = collectionService.getCollectionsForStory(id);
        List<CollectionDto> collectionDtos = collections.stream()
            .map(this::convertToCollectionDto)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(collectionDtos);
    }
    
    @PostMapping("/batch/add-to-collection")
    public ResponseEntity<Map<String, Object>> addStoriesToCollection(@RequestBody BatchAddToCollectionRequest request) {
        // This endpoint will be implemented once we have the complete collection service
        return ResponseEntity.ok(Map.of("message", "Batch add to collection endpoint - to be implemented"));
    }
    
    private Author findOrCreateAuthor(String authorName) {
        // First try to find existing author by name
        try {
            return authorService.findByName(authorName);
        } catch (Exception e) {
            // Author doesn't exist, create a new one
            Author newAuthor = new Author();
            newAuthor.setName(authorName);
            return authorService.create(newAuthor);
        }
    }
    
    private void updateStoryFromRequest(Story story, Object request) {
        if (request instanceof CreateStoryRequest createReq) {
            story.setTitle(createReq.getTitle());
            story.setSummary(createReq.getSummary());
            story.setDescription(createReq.getDescription());

            story.setContentHtml(sanitizationService.sanitize(createReq.getContentHtml()));

            story.setSourceUrl(createReq.getSourceUrl());
            story.setVolume(createReq.getVolume());
            
            // Handle author - either by ID or by name
            if (createReq.getAuthorId() != null) {
                Author author = authorService.findById(createReq.getAuthorId());
                story.setAuthor(author);
            } else if (createReq.getAuthorName() != null && !createReq.getAuthorName().trim().isEmpty()) {
                Author author = findOrCreateAuthor(createReq.getAuthorName().trim());
                story.setAuthor(author);
            }
            
            // Handle series - either by ID or by name
            if (createReq.getSeriesId() != null) {
                Series series = seriesService.findById(createReq.getSeriesId());
                story.setSeries(series);
            } else if (createReq.getSeriesName() != null && !createReq.getSeriesName().trim().isEmpty()) {
                Series series = seriesService.findOrCreate(createReq.getSeriesName().trim());
                story.setSeries(series);
            }
            
            // Tags are handled in the service layer
        } else if (request instanceof UpdateStoryRequest updateReq) {
            if (updateReq.getTitle() != null) {
                story.setTitle(updateReq.getTitle());
            }
            if (updateReq.getSummary() != null) {
                story.setSummary(updateReq.getSummary());
            }
            if (updateReq.getDescription() != null) {
                story.setDescription(updateReq.getDescription());
            }
            if (updateReq.getContentHtml() != null) {
                logger.info("Content before sanitization (length: {}): {}",
                           updateReq.getContentHtml().length(),
                           updateReq.getContentHtml().substring(0, Math.min(500, updateReq.getContentHtml().length())));
                String sanitizedContent = sanitizationService.sanitize(updateReq.getContentHtml());
                logger.info("Content after sanitization (length: {}): {}",
                           sanitizedContent.length(),
                           sanitizedContent.substring(0, Math.min(500, sanitizedContent.length())));
                story.setContentHtml(sanitizedContent);
            }
            if (updateReq.getSourceUrl() != null) {
                story.setSourceUrl(updateReq.getSourceUrl());
            }
            // Volume will be handled in series logic below
            // Handle author - either by ID or by name
            if (updateReq.getAuthorId() != null) {
                Author author = authorService.findById(updateReq.getAuthorId());
                story.setAuthor(author);
            } else if (updateReq.getAuthorName() != null && !updateReq.getAuthorName().trim().isEmpty()) {
                Author author = findOrCreateAuthor(updateReq.getAuthorName().trim());
                story.setAuthor(author);
            }
            // Handle series - either by ID, by name, or remove from series
            if (updateReq.getSeriesId() != null) {
                Series series = seriesService.findById(updateReq.getSeriesId());
                story.setSeries(series);
                if (updateReq.getVolume() != null) {
                    story.setVolume(updateReq.getVolume());
                }
            } else if (updateReq.getSeriesName() != null) {
                logger.info("Processing series update: seriesName='{}', isEmpty={}", updateReq.getSeriesName(), updateReq.getSeriesName().trim().isEmpty());
                if (updateReq.getSeriesName().trim().isEmpty()) {
                    // Empty series name means remove from series
                    logger.info("Removing story from series");
                    if (story.getSeries() != null) {
                        story.getSeries().removeStory(story);
                        story.setSeries(null);
                        story.setVolume(null);
                        logger.info("Story removed from series");
                    }
                } else {
                    // Non-empty series name means add to series
                    logger.info("Adding story to series: '{}', volume: {}", updateReq.getSeriesName().trim(), updateReq.getVolume());
                    Series series = seriesService.findOrCreate(updateReq.getSeriesName().trim());
                    story.setSeries(series);
                    // Set volume only if series is being set
                    if (updateReq.getVolume() != null) {
                        story.setVolume(updateReq.getVolume());
                        logger.info("Story added to series: {} with volume: {}", series.getName(), updateReq.getVolume());
                    } else {
                        logger.info("Story added to series: {} with no volume", series.getName());
                    }
                }
            }
            
            // Note: Tags are now handled in StoryService.updateWithTagNames()
        }
    }
    
    private StoryDto convertToDto(Story story) {
        StoryDto dto = new StoryDto();
        dto.setId(story.getId());
        dto.setTitle(story.getTitle());
        dto.setSummary(story.getSummary());
        dto.setDescription(story.getDescription());
        dto.setContentHtml(story.getContentHtml());
        dto.setSourceUrl(story.getSourceUrl());
        dto.setCoverPath(story.getCoverPath());
        dto.setWordCount(story.getWordCount());
        dto.setRating(story.getRating());
        dto.setVolume(story.getVolume());
        dto.setCreatedAt(story.getCreatedAt());
        dto.setUpdatedAt(story.getUpdatedAt());

        // Reading progress fields
        dto.setIsRead(story.getIsRead());
        dto.setReadingPosition(story.getReadingPosition());
        dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
        dto.setLastReadAt(story.getLastReadAt());
        dto.setLastCompletedAt(story.getLastCompletedAt());

        if (story.getAuthor() != null) {
            dto.setAuthorId(story.getAuthor().getId());
            dto.setAuthorName(story.getAuthor().getName());
        }

        if (story.getSeries() != null) {
            dto.setSeriesId(story.getSeries().getId());
            dto.setSeriesName(story.getSeries().getName());
        }

        dto.setTags(story.getTags().stream()
            .map(this::convertTagToDto)
            .collect(Collectors.toList()));

        return dto;
    }

    private Integer calculateReadingProgressPercentage(Story story) {
        if (story.getReadingPosition() == null || story.getReadingPosition() == 0) {
            return 0;
        }

        // ALWAYS use contentHtml for consistency (frontend uses contentHtml for position tracking)
        int totalLength = 0;
        if (story.getContentHtml() != null && !story.getContentHtml().isEmpty()) {
            totalLength = story.getContentHtml().length();
        }

        if (totalLength == 0) {
            return 0;
        }

        // Calculate percentage and round to nearest integer
        int percentage = Math.round((float) story.getReadingPosition() * 100 / totalLength);
        return Math.min(100, percentage);
    }

    private StoryReadingDto convertToReadingDto(Story story) {
        StoryReadingDto dto = new StoryReadingDto();
        dto.setId(story.getId());
        dto.setTitle(story.getTitle());
        dto.setSummary(story.getSummary());
        dto.setDescription(story.getDescription());
        dto.setContentHtml(story.getContentHtml());
        dto.setSourceUrl(story.getSourceUrl());
        dto.setCoverPath(story.getCoverPath());
        dto.setWordCount(story.getWordCount());
        dto.setRating(story.getRating());
        dto.setVolume(story.getVolume());
        dto.setCreatedAt(story.getCreatedAt());
        dto.setUpdatedAt(story.getUpdatedAt());

        // Reading progress fields
        dto.setIsRead(story.getIsRead());
        dto.setReadingPosition(story.getReadingPosition());
        dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
        dto.setLastReadAt(story.getLastReadAt());
        dto.setLastCompletedAt(story.getLastCompletedAt());

        if (story.getAuthor() != null) {
            dto.setAuthorId(story.getAuthor().getId());
            dto.setAuthorName(story.getAuthor().getName());
        }

        if (story.getSeries() != null) {
            dto.setSeriesId(story.getSeries().getId());
            dto.setSeriesName(story.getSeries().getName());
        }

        dto.setTags(story.getTags().stream()
            .map(this::convertTagToDto)
            .collect(Collectors.toList()));

        return dto;
    }

    private StorySummaryDto convertToSummaryDto(Story story) {
        StorySummaryDto dto = new StorySummaryDto();
        dto.setId(story.getId());
        dto.setTitle(story.getTitle());
        dto.setSummary(story.getSummary());
        dto.setDescription(story.getDescription());
        dto.setSourceUrl(story.getSourceUrl());
        dto.setCoverPath(story.getCoverPath());
        dto.setWordCount(story.getWordCount());
        dto.setRating(story.getRating());
        dto.setVolume(story.getVolume());
        dto.setCreatedAt(story.getCreatedAt());
        dto.setUpdatedAt(story.getUpdatedAt());
        dto.setPartOfSeries(story.isPartOfSeries());
        
        // Reading progress fields
        dto.setIsRead(story.getIsRead());
        dto.setReadingPosition(story.getReadingPosition());
        dto.setReadingProgressPercentage(calculateReadingProgressPercentage(story));
        dto.setLastReadAt(story.getLastReadAt());
        dto.setLastCompletedAt(story.getLastCompletedAt());

        if (story.getAuthor() != null) {
            dto.setAuthorId(story.getAuthor().getId());
            dto.setAuthorName(story.getAuthor().getName());
        }

        if (story.getSeries() != null) {
            dto.setSeriesId(story.getSeries().getId());
            dto.setSeriesName(story.getSeries().getName());
        }

        dto.setTags(story.getTags().stream()
            .map(this::convertTagToDto)
            .collect(Collectors.toList()));
        
        return dto;
    }
    
    private TagDto convertTagToDto(Tag tag) {
        TagDto tagDto = new TagDto();
        tagDto.setId(tag.getId());
        tagDto.setName(tag.getName());
        tagDto.setColor(tag.getColor());
        tagDto.setDescription(tag.getDescription());
        tagDto.setCreatedAt(tag.getCreatedAt());
        tagDto.setStoryCount(tag.getStories() != null ? tag.getStories().size() : 0);
        tagDto.setAliasCount(tag.getAliases() != null ? tag.getAliases().size() : 0);
        return tagDto;
    }
    
    private CollectionDto convertToCollectionDto(Collection collection) {
        CollectionDto dto = new CollectionDto();
        dto.setId(collection.getId());
        dto.setName(collection.getName());
        dto.setDescription(collection.getDescription());
        dto.setRating(collection.getRating());
        dto.setCoverImagePath(collection.getCoverImagePath());
        dto.setIsArchived(collection.getIsArchived());
        dto.setCreatedAt(collection.getCreatedAt());
        dto.setUpdatedAt(collection.getUpdatedAt());
        
        // For story collections endpoint, we don't need to map the stories themselves
        // to avoid circular references and keep it lightweight
        dto.setStoryCount(collection.getStoryCount());
        dto.setTotalWordCount(collection.getTotalWordCount());
        dto.setEstimatedReadingTime(readingTimeService.calculateReadingTime(collection.getTotalWordCount()));
        dto.setAverageStoryRating(collection.getAverageStoryRating());
        
        return dto;
    }

    private Story processExternalImagesIfNeeded(Story story) {
        try {
            if (story.getContentHtml() != null && !story.getContentHtml().trim().isEmpty()) {
                logger.debug("Starting async image processing for story: {}", story.getId());

                // Start async processing - this returns immediately
                asyncImageProcessingService.processStoryImagesAsync(story.getId(), story.getContentHtml());

                logger.info("Async image processing started for story: {}", story.getId());
            }
        } catch (Exception e) {
            logger.error("Failed to start async image processing for story {}: {}",
                        story.getId(), e.getMessage(), e);
            // Don't fail the entire operation if image processing fails
        }

        return story;
    }

    @GetMapping("/{id}/image-processing-progress")
    public ResponseEntity<Map<String, Object>> getImageProcessingProgress(@PathVariable UUID id) {
        ImageProcessingProgressService.ImageProcessingProgress progress = progressService.getProgress(id);

        if (progress == null) {
            return ResponseEntity.ok(Map.of(
                "isProcessing", false,
                "message", "No active image processing"
            ));
        }

        Map<String, Object> response = Map.of(
            "isProcessing", !progress.isCompleted(),
            "totalImages", progress.getTotalImages(),
            "processedImages", progress.getProcessedImages(),
            "currentImageUrl", progress.getCurrentImageUrl() != null ? progress.getCurrentImageUrl() : "",
            "status", progress.getStatus(),
            "progressPercentage", progress.getProgressPercentage(),
            "completed", progress.isCompleted(),
            "error", progress.getErrorMessage() != null ? progress.getErrorMessage() : ""
        );

        return ResponseEntity.ok(response);
    }

    @GetMapping("/check-duplicate")
    public ResponseEntity<Map<String, Object>> checkDuplicate(
            @RequestParam String title,
            @RequestParam String authorName) {
        try {
            List<Story> duplicates = storyService.findPotentialDuplicates(title, authorName);
            
            Map<String, Object> response = Map.of(
                "hasDuplicates", !duplicates.isEmpty(),
                "count", duplicates.size(),
                "duplicates", duplicates.stream()
                    .map(story -> Map.of(
                        "id", story.getId(),
                        "title", story.getTitle(),
                        "authorName", story.getAuthor() != null ? story.getAuthor().getName() : "",
                        "createdAt", story.getCreatedAt()
                    ))
                    .collect(Collectors.toList())
            );
            
            return ResponseEntity.ok(response);
        } catch (Exception e) {
            logger.error("Error checking for duplicates", e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Failed to check for duplicates"));
        }
    }
    
    // EPUB Import endpoint
    @PostMapping("/epub/import")
    public ResponseEntity<EPUBImportResponse> importEPUB(
            @RequestParam("file") MultipartFile file,
            @RequestParam(required = false) UUID authorId,
            @RequestParam(required = false) String authorName,
            @RequestParam(required = false) UUID seriesId,
            @RequestParam(required = false) String seriesName,
            @RequestParam(required = false) Double seriesVolume,
            @RequestParam(required = false) List<String> tags,
            @RequestParam(defaultValue = "true") Boolean preserveReadingPosition,
            @RequestParam(defaultValue = "false") Boolean overwriteExisting,
            @RequestParam(defaultValue = "true") Boolean createMissingAuthor,
            @RequestParam(defaultValue = "true") Boolean createMissingSeries) {
        
        logger.info("Importing EPUB file: {}", file.getOriginalFilename());
        
        EPUBImportRequest request = new EPUBImportRequest();
        request.setEpubFile(file);
        request.setAuthorId(authorId);
        request.setAuthorName(authorName);
        request.setSeriesId(seriesId);
        request.setSeriesName(seriesName);
        request.setSeriesVolume(seriesVolume);
        request.setTags(tags);
        request.setPreserveReadingPosition(preserveReadingPosition);
        request.setOverwriteExisting(overwriteExisting);
        request.setCreateMissingAuthor(createMissingAuthor);
        request.setCreateMissingSeries(createMissingSeries);
        
        try {
            EPUBImportResponse response = epubImportService.importEPUB(request);
            
            if (response.isSuccess()) {
                logger.info("Successfully imported EPUB: {} (Story ID: {})", 
                    response.getStoryTitle(), response.getStoryId());
                return ResponseEntity.ok(response);
            } else {
                logger.warn("EPUB import failed: {}", response.getMessage());
                return ResponseEntity.badRequest().body(response);
            }
            
        } catch (Exception e) {
            logger.error("Error importing EPUB: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(EPUBImportResponse.error("Internal server error: " + e.getMessage()));
        }
    }
    
    // EPUB Export endpoint
    @PostMapping("/epub/export")
    public ResponseEntity<org.springframework.core.io.Resource> exportEPUB(
            @Valid @RequestBody EPUBExportRequest request) {
        
        logger.info("Exporting story {} to EPUB", request.getStoryId());
        
        try {
            if (!epubExportService.canExportStory(request.getStoryId())) {
                return ResponseEntity.badRequest().build();
            }
            
            org.springframework.core.io.Resource resource = epubExportService.exportStoryAsEPUB(request);
            Story story = storyService.findById(request.getStoryId());
            String filename = epubExportService.getEPUBFilename(story);
            
            logger.info("Successfully exported EPUB: {}", filename);
            
            return ResponseEntity.ok()
                .header("Content-Disposition", "attachment; filename=\"" + filename + "\"")
                .header("Content-Type", "application/epub+zip")
                .body(resource);
                
        } catch (Exception e) {
            logger.error("Error exporting EPUB: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }
    
    // EPUB Export by story ID (GET endpoint)
    @GetMapping("/{id}/epub")
    public ResponseEntity<org.springframework.core.io.Resource> exportStoryAsEPUB(@PathVariable UUID id) {
        logger.info("Exporting story {} to EPUB via GET", id);
        
        EPUBExportRequest request = new EPUBExportRequest(id);
        return exportEPUB(request);
    }
    
    // Validate EPUB file
    @PostMapping("/epub/validate")
    public ResponseEntity<Map<String, Object>> validateEPUBFile(@RequestParam("file") MultipartFile file) {
        logger.info("Validating EPUB file: {}", file.getOriginalFilename());

        try {
            List<String> errors = epubImportService.validateEPUBFile(file);

            Map<String, Object> response = Map.of(
                "valid", errors.isEmpty(),
                "errors", errors,
                "filename", file.getOriginalFilename(),
                "size", file.getSize()
            );

            return ResponseEntity.ok(response);

        } catch (Exception e) {
            logger.error("Error validating EPUB file: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Failed to validate EPUB file"));
        }
    }

    // PDF Import endpoint
    @PostMapping("/pdf/import")
    public ResponseEntity<FileImportResponse> importPDF(
            @RequestParam("file") MultipartFile file,
            @RequestParam(required = false) UUID authorId,
            @RequestParam(required = false) String authorName,
            @RequestParam(required = false) UUID seriesId,
            @RequestParam(required = false) String seriesName,
            @RequestParam(required = false) Double seriesVolume,
            @RequestParam(required = false) List<String> tags,
            @RequestParam(defaultValue = "true") Boolean createMissingAuthor,
            @RequestParam(defaultValue = "true") Boolean createMissingSeries,
            @RequestParam(defaultValue = "true") Boolean extractImages) {

        logger.info("Importing PDF file: {}", file.getOriginalFilename());

        PDFImportRequest request = new PDFImportRequest();
        request.setPdfFile(file);
        request.setAuthorId(authorId);
        request.setAuthorName(authorName);
        request.setSeriesId(seriesId);
        request.setSeriesName(seriesName);
        request.setSeriesVolume(seriesVolume);
        request.setTags(tags);
        request.setCreateMissingAuthor(createMissingAuthor);
        request.setCreateMissingSeries(createMissingSeries);
        request.setExtractImages(extractImages);

        try {
            FileImportResponse response = pdfImportService.importPDF(request);

            if (response.isSuccess()) {
                logger.info("Successfully imported PDF: {} (Story ID: {})",
                    response.getStoryTitle(), response.getStoryId());
                return ResponseEntity.ok(response);
            } else {
                logger.warn("PDF import failed: {}", response.getMessage());
                return ResponseEntity.badRequest().body(response);
            }

        } catch (Exception e) {
            logger.error("Error importing PDF: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(FileImportResponse.error("Internal server error: " + e.getMessage(), file.getOriginalFilename()));
        }
    }

    // Validate PDF file
    @PostMapping("/pdf/validate")
    public ResponseEntity<Map<String, Object>> validatePDFFile(@RequestParam("file") MultipartFile file) {
        logger.info("Validating PDF file: {}", file.getOriginalFilename());

        try {
            List<String> errors = pdfImportService.validatePDFFile(file);

            Map<String, Object> response = Map.of(
                "valid", errors.isEmpty(),
                "errors", errors,
                "filename", file.getOriginalFilename(),
                "size", file.getSize()
            );

            return ResponseEntity.ok(response);

        } catch (Exception e) {
            logger.error("Error validating PDF file: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Failed to validate PDF file"));
        }
    }

    // ZIP Analysis endpoint - Step 1: Upload and analyze ZIP contents
    @PostMapping("/zip/analyze")
    public ResponseEntity<ZIPAnalysisResponse> analyzeZIPFile(@RequestParam("file") MultipartFile file) {
        logger.info("Analyzing ZIP file: {}", file.getOriginalFilename());

        try {
            ZIPAnalysisResponse response = zipImportService.analyzeZIPFile(file);

            if (response.isSuccess()) {
                logger.info("Successfully analyzed ZIP file: {} ({} files found)",
                    file.getOriginalFilename(), response.getTotalFiles());
                return ResponseEntity.ok(response);
            } else {
                logger.warn("ZIP analysis failed: {}", response.getMessage());
                return ResponseEntity.badRequest().body(response);
            }

        } catch (Exception e) {
            logger.error("Error analyzing ZIP file: {}", e.getMessage(), e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(ZIPAnalysisResponse.error("Internal server error: " + e.getMessage()));
        }
    }

    // ZIP Import endpoint - Step 2: Import selected files from analyzed ZIP
    @PostMapping("/zip/import")
    public ResponseEntity<ZIPImportResponse> importFromZIP(@Valid @RequestBody ZIPImportRequest request) {
        logger.info("Importing files from ZIP session: {}", request.getZipSessionId());

        try {
            ZIPImportResponse response = zipImportService.importFromZIP(request);

            logger.info("ZIP import completed: {} total, {} successful, {} failed",
                response.getTotalFiles(), response.getSuccessfulImports(), response.getFailedImports());

            if (response.isSuccess()) {
                return ResponseEntity.ok(response);
            } else {
                return ResponseEntity.badRequest().body(response);
            }

        } catch (Exception e) {
            logger.error("Error importing from ZIP: {}", e.getMessage(), e);
            ZIPImportResponse errorResponse = new ZIPImportResponse();
            errorResponse.setSuccess(false);
            errorResponse.setMessage("Internal server error: " + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
        }
    }

    @PostMapping("/{id}/send-to-audiocove")
    public ResponseEntity<String> sendToAudioCove(@PathVariable UUID id) {
        try {
            String response = audioCoveService.ingestStory(id);
            return ResponseEntity.ok(response);
        } catch (IllegalStateException e) {
            return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(e.getMessage());
        } catch (Exception e) {
            logger.error("Failed to send story {} to AudioCove: {}", id, e.getMessage());
            return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
                    .body("AudioCove request failed: " + e.getMessage());
        }
    }

    // Request DTOs
    public static class CreateStoryRequest {
        private String title;
        private String summary;
        private String description;
        private String contentHtml;
        private String sourceUrl;
        private Double volume;
        private UUID authorId;
        private String authorName;
        private UUID seriesId;
        private String seriesName;
        private List<String> tagNames;
        
        // Getters and setters
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getSummary() { return summary; }
        public void setSummary(String summary) { this.summary = summary; }
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        public String getContentHtml() { return contentHtml; }
        public void setContentHtml(String contentHtml) { this.contentHtml = contentHtml; }
        public String getSourceUrl() { return sourceUrl; }
        public void setSourceUrl(String sourceUrl) { this.sourceUrl = sourceUrl; }
        public Double getVolume() { return volume; }
        public void setVolume(Double volume) { this.volume = volume; }
        public UUID getAuthorId() { return authorId; }
        public void setAuthorId(UUID authorId) { this.authorId = authorId; }
        public String getAuthorName() { return authorName; }
        public void setAuthorName(String authorName) { this.authorName = authorName; }
        public UUID getSeriesId() { return seriesId; }
        public void setSeriesId(UUID seriesId) { this.seriesId = seriesId; }
        public String getSeriesName() { return seriesName; }
        public void setSeriesName(String seriesName) { this.seriesName = seriesName; }
        public List<String> getTagNames() { return tagNames; }
        public void setTagNames(List<String> tagNames) { this.tagNames = tagNames; }
    }
    
    public static class UpdateStoryRequest {
        private String title;
        private String summary;
        private String description;
        private String contentHtml;
        private String sourceUrl;
        private Double volume;
        private UUID authorId;
        private String authorName;
        private UUID seriesId;
        private String seriesName;
        private List<String> tagNames;
        
        // Getters and setters
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getSummary() { return summary; }
        public void setSummary(String summary) { this.summary = summary; }
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        public String getContentHtml() { return contentHtml; }
        public void setContentHtml(String contentHtml) { this.contentHtml = contentHtml; }
        public String getSourceUrl() { return sourceUrl; }
        public void setSourceUrl(String sourceUrl) { this.sourceUrl = sourceUrl; }
        public Double getVolume() { return volume; }
        public void setVolume(Double volume) { this.volume = volume; }
        public UUID getAuthorId() { return authorId; }
        public void setAuthorId(UUID authorId) { this.authorId = authorId; }
        public String getAuthorName() { return authorName; }
        public void setAuthorName(String authorName) { this.authorName = authorName; }
        public UUID getSeriesId() { return seriesId; }
        public void setSeriesId(UUID seriesId) { this.seriesId = seriesId; }
        public String getSeriesName() { return seriesName; }
        public void setSeriesName(String seriesName) { this.seriesName = seriesName; }
        public List<String> getTagNames() { return tagNames; }
        public void setTagNames(List<String> tagNames) { this.tagNames = tagNames; }
    }
    
    public static class RatingRequest {
        private Integer rating;
        
        public Integer getRating() { return rating; }
        public void setRating(Integer rating) { this.rating = rating; }
    }
    
    public static class BatchAddToCollectionRequest {
        private List<UUID> storyIds;
        private UUID collectionId;
        private String newCollectionName;
        
        public List<UUID> getStoryIds() { return storyIds; }
        public void setStoryIds(List<UUID> storyIds) { this.storyIds = storyIds; }
        public UUID getCollectionId() { return collectionId; }
        public void setCollectionId(UUID collectionId) { this.collectionId = collectionId; }
        public String getNewCollectionName() { return newCollectionName; }
        public void setNewCollectionName(String newCollectionName) { this.newCollectionName = newCollectionName; }
    }
}