package com.storycove.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.storycove.event.StoryContentUpdatedEvent;

@Service
public class ImageService {

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

    private static final Set<String> ALLOWED_CONTENT_TYPES = Set.of(
        "image/jpeg", "image/jpg", "image/png", "image/webp"
    );

    private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
        "jpg", "jpeg", "png", "webp"
    );
    
    @Value("${storycove.images.upload-dir:/app/images}")
    private String baseUploadDir;
    
    @Autowired
    private LibraryService libraryService;

    @Autowired
    private StoryService storyService;

    @Autowired
    private AuthorService authorService;

    @Autowired
    private CollectionService collectionService;
    
    private String getUploadDir() {
        String libraryPath = libraryService.getCurrentImagePath();
        return baseUploadDir + libraryPath;
    }
    
    @Value("${storycove.images.cover.max-width:800}")
    private int coverMaxWidth;
    
    @Value("${storycove.images.cover.max-height:1200}")
    private int coverMaxHeight;
    
    @Value("${storycove.images.avatar.max-size:400}")
    private int avatarMaxSize;
    
    @Value("${storycove.images.max-file-size:5242880}") // 5MB default
    private long maxFileSize;

    @Value("${storycove.app.public-url:http://localhost:6925}")
    private String publicUrl;

    public enum ImageType {
        COVER("covers"),
        AVATAR("avatars"),
        CONTENT("content");

        private final String directory;

        ImageType(String directory) {
            this.directory = directory;
        }

        public String getDirectory() {
            return directory;
        }
    }
    
    public String uploadImage(MultipartFile file, ImageType imageType) throws IOException {
        validateFile(file);
        
        // Create directories if they don't exist
        Path typeDir = Paths.get(getUploadDir(), imageType.getDirectory());
        Files.createDirectories(typeDir);
        
        // Generate unique filename
        String originalFilename = file.getOriginalFilename();
        String extension = getFileExtension(originalFilename);
        String filename = UUID.randomUUID().toString() + "." + extension;
        Path filePath = typeDir.resolve(filename);
        
        // Process and save image
        if (extension.equals("webp")) {
            // ImageIO does not support WebP natively; save verbatim without resize
            Files.write(filePath, file.getBytes());
        } else {
            BufferedImage processedImage = processImage(file, imageType);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(processedImage, extension.equals("jpg") ? "jpeg" : extension, baos);
            Files.write(filePath, baos.toByteArray());
        }
        
        // Return relative path for database storage
        return imageType.getDirectory() + "/" + filename;
    }
    
    public boolean deleteImage(String imagePath) {
        if (imagePath == null || imagePath.trim().isEmpty()) {
            return false;
        }
        
        try {
            Path fullPath = Paths.get(getUploadDir(), imagePath);
            return Files.deleteIfExists(fullPath);
        } catch (IOException e) {
            return false;
        }
    }
    
    public Path getImagePath(String imagePath) {
        return Paths.get(getUploadDir(), imagePath);
    }
    
    public boolean imageExists(String imagePath) {
        if (imagePath == null || imagePath.trim().isEmpty()) {
            return false;
        }
        
        return Files.exists(getImagePath(imagePath));
    }
    
    public boolean imageExistsInLibrary(String imagePath, String libraryId) {
        if (imagePath == null || imagePath.trim().isEmpty() || libraryId == null) {
            return false;
        }
        
        return Files.exists(getImagePathInLibrary(imagePath, libraryId));
    }
    
    public Path getImagePathInLibrary(String imagePath, String libraryId) {
        String libraryPath = libraryService.getImagePathForLibrary(libraryId);
        return Paths.get(baseUploadDir + libraryPath, imagePath);
    }
    
    private void validateFile(MultipartFile file) throws IOException {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("File is empty");
        }
        
        if (file.getSize() > maxFileSize) {
            throw new IllegalArgumentException("File size exceeds maximum allowed size of " + maxFileSize + " bytes");
        }
        
        String contentType = file.getContentType();
        if (!ALLOWED_CONTENT_TYPES.contains(contentType)) {
            throw new IllegalArgumentException("File type not allowed. Allowed types: " + ALLOWED_CONTENT_TYPES);
        }
        
        String filename = file.getOriginalFilename();
        if (filename == null || !hasValidExtension(filename)) {
            throw new IllegalArgumentException("Invalid file extension. Allowed extensions: " + ALLOWED_EXTENSIONS);
        }
    }
    
    private BufferedImage processImage(MultipartFile file, ImageType imageType) throws IOException {
        BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(file.getBytes()));
        
        if (originalImage == null) {
            throw new IOException("Cannot read image file");
        }
        
        // Calculate new dimensions
        Dimension newSize = calculateNewSize(originalImage, imageType);
        
        // Resize image if necessary
        if (newSize.width != originalImage.getWidth() || newSize.height != originalImage.getHeight()) {
            return resizeImage(originalImage, newSize.width, newSize.height);
        }
        
        return originalImage;
    }
    
    private Dimension calculateNewSize(BufferedImage image, ImageType imageType) {
        int originalWidth = image.getWidth();
        int originalHeight = image.getHeight();
        
        int maxWidth, maxHeight;
        
        switch (imageType) {
            case COVER:
                maxWidth = coverMaxWidth;
                maxHeight = coverMaxHeight;
                break;
            case AVATAR:
                maxWidth = avatarMaxSize;
                maxHeight = avatarMaxSize;
                break;
            case CONTENT:
                // Content images are not resized
                return new Dimension(originalWidth, originalHeight);
            default:
                return new Dimension(originalWidth, originalHeight);
        }
        
        // Calculate scaling factor
        double scaleX = (double) maxWidth / originalWidth;
        double scaleY = (double) maxHeight / originalHeight;
        double scale = Math.min(scaleX, scaleY);
        
        if (scale >= 1.0) {
            // No scaling needed
            return new Dimension(originalWidth, originalHeight);
        }
        
        int newWidth = (int) Math.round(originalWidth * scale);
        int newHeight = (int) Math.round(originalHeight * scale);
        
        return new Dimension(newWidth, newHeight);
    }
    
    private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
        BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = resizedImage.createGraphics();
        
        // Enable high-quality rendering
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        
        g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
        g2d.dispose();
        
        return resizedImage;
    }
    
    private String getFileExtension(String filename) {
        if (filename == null || filename.lastIndexOf('.') == -1) {
            return "";
        }
        return filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
    }
    
    private boolean hasValidExtension(String filename) {
        String extension = getFileExtension(filename);
        return ALLOWED_EXTENSIONS.contains(extension);
    }

    /**
     * Upload a content image for a specific story, storing it in the story's subfolder.
     * Use this instead of uploadImage(file, ImageType.CONTENT) for story content images.
     */
    public String uploadContentImage(MultipartFile file, UUID storyId) throws IOException {
        validateFile(file);

        Path contentDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory(), storyId.toString());
        Files.createDirectories(contentDir);

        String originalFilename = file.getOriginalFilename();
        String extension = getFileExtension(originalFilename);
        String filename = UUID.randomUUID().toString() + "." + extension;
        Path filePath = contentDir.resolve(filename);

        // Content images are stored verbatim — no resizing needed, preserves all formats including WebP
        Files.write(filePath, file.getBytes());

        return ImageType.CONTENT.getDirectory() + "/" + storyId.toString() + "/" + filename;
    }

    // Content image processing methods

    /**
     * Process HTML content and download all referenced images, replacing URLs with local paths
     */
    public ContentImageProcessingResult processContentImages(String htmlContent, UUID storyId) {
        logger.debug("Processing content images for story: {}, content length: {}", storyId,
                   htmlContent != null ? htmlContent.length() : 0);

        List<String> warnings = new ArrayList<>();
        List<String> downloadedImages = new ArrayList<>();

        if (htmlContent == null || htmlContent.trim().isEmpty()) {
            logger.debug("No content to process for story: {}", storyId);
            return new ContentImageProcessingResult(htmlContent, warnings, downloadedImages);
        }

        // Find all img tags with src attributes
        Pattern imgPattern = Pattern.compile("<img[^>]+src=[\"']([^\"']+)[\"'][^>]*>", Pattern.CASE_INSENSITIVE);
        Matcher matcher = imgPattern.matcher(htmlContent);

        int imageCount = 0;
        int externalImageCount = 0;

        StringBuffer processedContent = new StringBuffer();

        while (matcher.find()) {
            String fullImgTag = matcher.group(0);
            String imageUrl = matcher.group(1);
            imageCount++;

            logger.debug("Found image #{}: {} in tag: {}", imageCount, imageUrl, fullImgTag);

            try {
                // Skip if it's already a local path, data URL, or from this application
                if (!isExternalUrl(imageUrl)) {
                    logger.debug("Skipping local/internal URL: {}", imageUrl);
                    matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag));
                    continue;
                }

                externalImageCount++;
                logger.debug("Processing external image #{}: {}", externalImageCount, imageUrl);

                // Download and store the image
                String localPath = downloadImageFromUrl(imageUrl, storyId);
                downloadedImages.add(localPath);

                // Generate local URL
                String localUrl = getLocalImageUrl(storyId, localPath);
                logger.debug("Downloaded image: {} -> {}", imageUrl, localUrl);

                // Replace the src attribute with the local path - handle both single and double quotes
                String newImgTag = fullImgTag
                    .replaceFirst("src=\"" + Pattern.quote(imageUrl) + "\"", "src=\"" + localUrl + "\"")
                    .replaceFirst("src='" + Pattern.quote(imageUrl) + "'", "src=\"" + localUrl + "\"");

                // If replacement didn't work, try a more generic approach
                if (newImgTag.equals(fullImgTag)) {
                    logger.warn("Standard replacement failed for image URL: {}, trying generic replacement", imageUrl);
                    newImgTag = fullImgTag.replaceAll("src\\s*=\\s*[\"']?" + Pattern.quote(imageUrl) + "[\"']?", "src=\"" + localUrl + "\"");
                }

                logger.debug("Replaced img tag: {} -> {}", fullImgTag, newImgTag);
                matcher.appendReplacement(processedContent, Matcher.quoteReplacement(newImgTag));

            } catch (Exception e) {
                logger.error("Failed to download image: {} - {}", imageUrl, e.getMessage(), e);
                warnings.add("Failed to download image: " + imageUrl + " - " + e.getMessage());
                // Keep original URL in case of failure
                matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag));
            }
        }

        matcher.appendTail(processedContent);

        logger.info("Finished processing images for story: {}. Found {} total images, {} external. Downloaded {} images, {} warnings",
                   storyId, imageCount, externalImageCount, downloadedImages.size(), warnings.size());

        return new ContentImageProcessingResult(processedContent.toString(), warnings, downloadedImages);
    }

    /**
     * Functional interface for progress callbacks during image processing
     */
    @FunctionalInterface
    public interface ImageProcessingProgressCallback {
        void onProgress(String currentImageUrl, int processedCount, int totalCount);
    }

    /**
     * Process content images with progress callbacks for async processing
     */
    public ContentImageProcessingResult processContentImagesWithProgress(String htmlContent, UUID storyId, ImageProcessingProgressCallback progressCallback) {
        logger.debug("Processing content images with progress for story: {}, content length: {}", storyId,
                   htmlContent != null ? htmlContent.length() : 0);

        List<String> warnings = new ArrayList<>();
        List<String> downloadedImages = new ArrayList<>();

        if (htmlContent == null || htmlContent.trim().isEmpty()) {
            logger.debug("No content to process for story: {}", storyId);
            return new ContentImageProcessingResult(htmlContent, warnings, downloadedImages);
        }

        // Find all img tags with src attributes
        Pattern imgPattern = Pattern.compile("<img[^>]+src=[\"']([^\"']+)[\"'][^>]*>", Pattern.CASE_INSENSITIVE);
        Matcher matcher = imgPattern.matcher(htmlContent);

        // First pass: count external images
        List<String> externalImages = new ArrayList<>();
        Matcher countMatcher = imgPattern.matcher(htmlContent);
        while (countMatcher.find()) {
            String imageUrl = countMatcher.group(1);
            if (isExternalUrl(imageUrl)) {
                externalImages.add(imageUrl);
            }
        }

        int totalExternalImages = externalImages.size();
        int processedCount = 0;

        StringBuffer processedContent = new StringBuffer();
        matcher.reset(); // Reset the matcher for processing

        while (matcher.find()) {
            String fullImgTag = matcher.group(0);
            String imageUrl = matcher.group(1);

            logger.debug("Found image: {} in tag: {}", imageUrl, fullImgTag);

            try {
                // Skip if it's already a local path, data URL, or from this application
                if (!isExternalUrl(imageUrl)) {
                    logger.debug("Skipping local/internal URL: {}", imageUrl);
                    matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag));
                    continue;
                }

                // Call progress callback
                if (progressCallback != null) {
                    progressCallback.onProgress(imageUrl, processedCount, totalExternalImages);
                }

                logger.debug("Processing external image #{}: {}", processedCount + 1, imageUrl);

                // Download and store the image
                String localPath = downloadImageFromUrl(imageUrl, storyId);
                downloadedImages.add(localPath);

                // Generate local URL
                String localUrl = getLocalImageUrl(storyId, localPath);
                logger.debug("Downloaded image: {} -> {}", imageUrl, localUrl);

                // Replace the src attribute with the local path
                String newImgTag = fullImgTag
                    .replaceFirst("src=\"" + Pattern.quote(imageUrl) + "\"", "src=\"" + localUrl + "\"")
                    .replaceFirst("src='" + Pattern.quote(imageUrl) + "'", "src='" + localUrl + "'");

                matcher.appendReplacement(processedContent, Matcher.quoteReplacement(newImgTag));
                processedCount++;

            } catch (Exception e) {
                logger.warn("Failed to download image: {} - Error: {}", imageUrl, e.getMessage());
                warnings.add("Failed to download image: " + imageUrl + " - " + e.getMessage());
                matcher.appendReplacement(processedContent, Matcher.quoteReplacement(fullImgTag));
            }
        }

        matcher.appendTail(processedContent);

        logger.info("Processed {} external images for story: {} (Total: {}, Downloaded: {}, Warnings: {})",
                   processedCount, storyId, processedCount, downloadedImages.size(), warnings.size());

        return new ContentImageProcessingResult(processedContent.toString(), warnings, downloadedImages);
    }

    /**
     * Check if a URL is external (not from this application).
     * Returns true if the URL should be downloaded, false if it's already local.
     */
    private boolean isExternalUrl(String url) {
        if (url == null || url.trim().isEmpty()) {
            return false;
        }

        // Skip data URLs
        if (url.startsWith("data:")) {
            return false;
        }

        // Skip relative URLs (local paths)
        if (url.startsWith("/")) {
            return false;
        }

        // Skip URLs that are already pointing to our API
        if (url.contains("/api/files/images/")) {
            return false;
        }

        // Check if URL starts with the public URL (our own domain)
        if (publicUrl != null && !publicUrl.trim().isEmpty()) {
            String normalizedUrl = url.trim().toLowerCase();
            String normalizedPublicUrl = publicUrl.trim().toLowerCase();

            // Remove trailing slash from public URL for comparison
            if (normalizedPublicUrl.endsWith("/")) {
                normalizedPublicUrl = normalizedPublicUrl.substring(0, normalizedPublicUrl.length() - 1);
            }

            if (normalizedUrl.startsWith(normalizedPublicUrl)) {
                logger.debug("URL is from this application (matches publicUrl): {}", url);
                return false;
            }
        }

        // If it's an HTTP(S) URL that didn't match our filters, it's external
        if (url.startsWith("http://") || url.startsWith("https://")) {
            logger.debug("URL is external: {}", url);
            return true;
        }

        // For any other format, consider it non-external (safer default)
        return false;
    }

    /**
     * Download an image from a URL and store it locally
     */
    private String downloadImageFromUrl(String imageUrl, UUID storyId) throws IOException {
        URL url = new URL(imageUrl);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // Set a reasonable user agent to avoid blocks
        connection.setRequestProperty("User-Agent", "Mozilla/5.0 (StoryCove Image Processor)");
        connection.setConnectTimeout(30000); // 30 seconds
        connection.setReadTimeout(30000);

        try (InputStream inputStream = connection.getInputStream()) {
            // Get content type to determine file extension
            String contentType = connection.getContentType();
            String extension = getExtensionFromContentType(contentType);

            if (extension == null) {
                // Try to extract from URL
                extension = getExtensionFromUrl(imageUrl);
            }

            if (extension == null || !ALLOWED_EXTENSIONS.contains(extension.toLowerCase())) {
                throw new IllegalArgumentException("Unsupported image format: " + contentType);
            }

            // Create directories for content images
            Path contentDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory(), storyId.toString());
            Files.createDirectories(contentDir);

            // Generate unique filename
            String filename = UUID.randomUUID().toString() + "." + extension.toLowerCase();
            Path filePath = contentDir.resolve(filename);

            // Read and validate the image
            byte[] imageData = inputStream.readAllBytes();
            ByteArrayInputStream bais = new ByteArrayInputStream(imageData);
            BufferedImage image = ImageIO.read(bais);

            if (image == null) {
                throw new IOException("Invalid image format");
            }

            // Save the image
            Files.write(filePath, imageData);

            // Return relative path
            return ImageType.CONTENT.getDirectory() + "/" + storyId.toString() + "/" + filename;

        } finally {
            connection.disconnect();
        }
    }

    /**
     * Generate local image URL for serving
     */
    private String getLocalImageUrl(UUID storyId, String imagePath) {
        String currentLibraryId = libraryService.getCurrentLibraryId();
        if (currentLibraryId == null || currentLibraryId.trim().isEmpty()) {
            logger.warn("Current library ID is null or empty when generating local image URL for story: {}", storyId);
            return "/api/files/images/default/" + imagePath;
        }
        String localUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath;
        logger.debug("Generated local image URL: {} for story: {}", localUrl, storyId);
        return localUrl;
    }

    /**
     * Get file extension from content type
     */
    private String getExtensionFromContentType(String contentType) {
        if (contentType == null) return null;

        switch (contentType.toLowerCase()) {
            case "image/jpeg":
            case "image/jpg":
                return "jpg";
            case "image/png":
                return "png";
            case "image/webp":
                return "webp";
            default:
                return null;
        }
    }

    /**
     * Extract file extension from URL
     */
    private String getExtensionFromUrl(String url) {
        try {
            String path = new URL(url).getPath();
            int lastDot = path.lastIndexOf('.');
            if (lastDot > 0 && lastDot < path.length() - 1) {
                return path.substring(lastDot + 1).toLowerCase();
            }
        } catch (Exception ignored) {
        }
        return null;
    }

    /**
     * Cleanup orphaned content images that are no longer referenced in any story
     */
    public ContentImageCleanupResult cleanupOrphanedContentImages(boolean dryRun) {
        logger.info("Starting orphaned content image cleanup (dryRun: {})", dryRun);

        final Set<String> referencedImages;
        List<String> orphanedImages = new ArrayList<>();
        List<String> errors = new ArrayList<>();
        long totalSizeBytes = 0;
        int foldersToDelete = 0;

        // Step 1: Collect all image references from all story content
        logger.debug("Scanning all story content for image references...");
        referencedImages = collectAllImageReferences();
        logger.debug("Found {} unique image references in story content", referencedImages.size());

        try {
            // Step 2: Scan the content images directory
            Path contentImagesDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory());

            if (!Files.exists(contentImagesDir)) {
                logger.debug("Content images directory does not exist: {}", contentImagesDir);
                return new ContentImageCleanupResult(orphanedImages, 0, 0, referencedImages.size(), errors, dryRun);
            }

            logger.debug("Scanning content images directory: {}", contentImagesDir);

            // Walk through all story directories
            Files.walk(contentImagesDir, 2)
                .filter(Files::isDirectory)
                .filter(path -> !path.equals(contentImagesDir)) // Skip the root content directory
                .filter(path -> !isSynologySystemPath(path)) // Skip Synology system directories
                .forEach(storyDir -> {
                    try {
                        String storyId = storyDir.getFileName().toString();
                        logger.debug("Checking story directory: {}", storyId);

                        // Check if this story still exists
                        boolean storyExists = storyService.findByIdOptional(UUID.fromString(storyId)).isPresent();

                        if (!storyExists) {
                            logger.debug("Found orphaned story directory (story deleted): {}", storyId);
                            // Mark entire directory for deletion
                            try {
                                Files.walk(storyDir)
                                    .filter(Files::isRegularFile)
                                    .filter(path -> !isSynologySystemPath(path)) // Skip Synology system files
                                    .filter(path -> isValidImageFile(path)) // Only process actual image files
                                    .forEach(file -> {
                                        try {
                                            long size = Files.size(file);
                                            orphanedImages.add(file.toString());
                                            // Add to total size (will be updated in main scope)
                                        } catch (IOException e) {
                                            errors.add("Failed to get size for " + file + ": " + e.getMessage());
                                        }
                                    });
                            } catch (IOException e) {
                                errors.add("Failed to scan orphaned story directory " + storyDir + ": " + e.getMessage());
                            }
                            return;
                        }

                        // Check individual files in the story directory
                        try {
                            Files.walk(storyDir)
                                .filter(Files::isRegularFile)
                                .filter(path -> !isSynologySystemPath(path)) // Skip Synology system files
                                .filter(path -> isValidImageFile(path)) // Only process actual image files
                                .forEach(imageFile -> {
                                    try {
                                        String filename = imageFile.getFileName().toString();

                                        // Only consider it orphaned if it's not in our referenced filenames
                                        if (!referencedImages.contains(filename)) {
                                            logger.debug("Found orphaned image: {}", filename);
                                            orphanedImages.add(imageFile.toString());
                                        } else {
                                            logger.debug("Image file is referenced, keeping: {}", filename);
                                        }
                                    } catch (Exception e) {
                                        errors.add("Error checking image file " + imageFile + ": " + e.getMessage());
                                    }
                                });
                        } catch (IOException e) {
                            errors.add("Failed to scan story directory " + storyDir + ": " + e.getMessage());
                        }

                    } catch (Exception e) {
                        errors.add("Error processing story directory " + storyDir + ": " + e.getMessage());
                    }
                });

            // Calculate total size and count empty directories
            for (String orphanedImage : orphanedImages) {
                try {
                    Path imagePath = Paths.get(orphanedImage);
                    if (Files.exists(imagePath)) {
                        totalSizeBytes += Files.size(imagePath);
                    }
                } catch (IOException e) {
                    errors.add("Failed to get size for " + orphanedImage + ": " + e.getMessage());
                }
            }

            // Count empty directories that would be removed
            try {
                foldersToDelete = (int) Files.walk(contentImagesDir)
                    .filter(Files::isDirectory)
                    .filter(path -> !path.equals(contentImagesDir))
                    .filter(this::isDirectoryEmptyOrWillBeEmpty)
                    .count();
            } catch (IOException e) {
                errors.add("Failed to count empty directories: " + e.getMessage());
            }

            // Step 3: Delete orphaned files if not dry run
            if (!dryRun && !orphanedImages.isEmpty()) {
                logger.debug("Deleting {} orphaned images...", orphanedImages.size());

                Set<Path> directoriesToCheck = new HashSet<>();

                for (String orphanedImage : orphanedImages) {
                    try {
                        Path imagePath = Paths.get(orphanedImage);
                        if (Files.exists(imagePath)) {
                            directoriesToCheck.add(imagePath.getParent());
                            Files.delete(imagePath);
                            logger.debug("Deleted orphaned image: {}", imagePath);
                        }
                    } catch (IOException e) {
                        errors.add("Failed to delete " + orphanedImage + ": " + e.getMessage());
                    }
                }

                // Clean up empty directories
                for (Path dir : directoriesToCheck) {
                    try {
                        if (Files.exists(dir) && isDirEmpty(dir)) {
                            Files.delete(dir);
                            logger.debug("Deleted empty story directory: {}", dir);
                        }
                    } catch (IOException e) {
                        errors.add("Failed to delete empty directory " + dir + ": " + e.getMessage());
                    }
                }
            }

            logger.info("Orphaned content image cleanup completed. Found {} orphaned files ({} bytes)",
                       orphanedImages.size(), totalSizeBytes);

        } catch (Exception e) {
            logger.error("Error during orphaned content image cleanup", e);
            errors.add("General cleanup error: " + e.getMessage());
        }

        return new ContentImageCleanupResult(orphanedImages, totalSizeBytes, foldersToDelete, referencedImages.size(), errors, dryRun);
    }

    /**
     * Collect all image filenames referenced in content (UUID-based filenames only)
     */
    private Set<String> collectAllImageReferences() {
        Set<String> referencedFilenames = new HashSet<>();

        try {
            // Get all stories
            List<com.storycove.entity.Story> allStories = storyService.findAllWithAssociations();

            // Pattern to match local image URLs in content
            Pattern imagePattern = Pattern.compile("src=[\"']([^\"']*(?:content/[^\"']*\\.(jpg|jpeg|png|webp)))[\"']", Pattern.CASE_INSENSITIVE);

            for (com.storycove.entity.Story story : allStories) {
                // Add story cover image filename if present
                if (story.getCoverPath() != null && !story.getCoverPath().trim().isEmpty()) {
                    String filename = extractFilename(story.getCoverPath());
                    if (filename != null) {
                        referencedFilenames.add(filename);
                        logger.debug("Found cover image filename in story {}: {}", story.getId(), filename);
                    }
                }

                // Add author avatar image filename if present
                if (story.getAuthor() != null && story.getAuthor().getAvatarImagePath() != null && !story.getAuthor().getAvatarImagePath().trim().isEmpty()) {
                    String filename = extractFilename(story.getAuthor().getAvatarImagePath());
                    if (filename != null) {
                        referencedFilenames.add(filename);
                        logger.debug("Found avatar image filename for author {}: {}", story.getAuthor().getId(), filename);
                    }
                }

                // Add content images from HTML
                if (story.getContentHtml() != null) {
                    Matcher matcher = imagePattern.matcher(story.getContentHtml());

                    while (matcher.find()) {
                        String imageSrc = matcher.group(1);

                        // Extract just the filename from the URL
                        String filename = extractFilename(imageSrc);
                        if (filename != null && isUuidBasedFilename(filename)) {
                            referencedFilenames.add(filename);
                            logger.debug("Found content image filename in story {}: {}", story.getId(), filename);
                        }
                    }
                }
            }

            // Also get all authors separately to catch avatars for authors without stories
            List<com.storycove.entity.Author> allAuthors = authorService.findAll();
            for (com.storycove.entity.Author author : allAuthors) {
                if (author.getAvatarImagePath() != null && !author.getAvatarImagePath().trim().isEmpty()) {
                    String filename = extractFilename(author.getAvatarImagePath());
                    if (filename != null) {
                        referencedFilenames.add(filename);
                        logger.debug("Found standalone avatar image filename for author {}: {}", author.getId(), filename);
                    }
                }
            }

            // Also get all collections to catch cover images
            List<com.storycove.entity.Collection> allCollections = collectionService.findAllWithTags();
            for (com.storycove.entity.Collection collection : allCollections) {
                if (collection.getCoverImagePath() != null && !collection.getCoverImagePath().trim().isEmpty()) {
                    String filename = extractFilename(collection.getCoverImagePath());
                    if (filename != null) {
                        referencedFilenames.add(filename);
                        logger.debug("Found collection cover image filename for collection {}: {}", collection.getId(), filename);
                    }
                }
            }

        } catch (Exception e) {
            logger.error("Error collecting image references from stories", e);
        }

        return referencedFilenames;
    }

    /**
     * Convert an image src attribute to relative file path
     */
    private String convertSrcToRelativePath(String src) {
        try {
            // Handle both /api/files/images/libraryId/content/... and relative content/... paths
            if (src.contains("/content/")) {
                int contentIndex = src.indexOf("/content/");
                return src.substring(contentIndex + 1); // Remove leading slash, keep "content/..."
            }
        } catch (Exception e) {
            logger.debug("Failed to convert src to relative path: {}", src);
        }
        return null;
    }

    /**
     * Convert absolute file path to relative path from upload directory
     */
    private String convertAbsolutePathToRelative(String absolutePath) {
        try {
            if (absolutePath == null || absolutePath.trim().isEmpty()) {
                return null;
            }

            Path absPath = Paths.get(absolutePath);
            Path uploadDirPath = Paths.get(getUploadDir());

            // If the path is already relative to upload dir, return as-is
            if (!absPath.isAbsolute()) {
                return absolutePath.replace('\\', '/');
            }

            // Try to make it relative to the upload directory
            if (absPath.startsWith(uploadDirPath)) {
                Path relativePath = uploadDirPath.relativize(absPath);
                return relativePath.toString().replace('\\', '/');
            }

            // If it's not under upload directory, check if it's library-specific path
            String libraryPath = libraryService.getCurrentImagePath();
            Path baseUploadPath = Paths.get(baseUploadDir);

            if (absPath.startsWith(baseUploadPath)) {
                Path relativePath = baseUploadPath.relativize(absPath);
                String relativeStr = relativePath.toString().replace('\\', '/');

                // Remove library prefix if present to make it library-agnostic for comparison
                if (relativeStr.startsWith(libraryPath.substring(1))) { // Remove leading slash from library path
                    return relativeStr.substring(libraryPath.length() - 1); // Keep the leading slash
                }
                return relativeStr;
            }

            // Fallback: just use the filename portion if it's in the right structure
            String fileName = absPath.getFileName().toString();
            if (fileName.matches(".*\\.(jpg|jpeg|png|webp)$")) {
                // Try to preserve directory structure if it looks like covers/ or avatars/
                Path parent = absPath.getParent();
                if (parent != null) {
                    String parentName = parent.getFileName().toString();
                    if (parentName.equals("covers") || parentName.equals("avatars")) {
                        return parentName + "/" + fileName;
                    }
                }
                return fileName;
            }

        } catch (Exception e) {
            logger.debug("Failed to convert absolute path to relative: {}", absolutePath, e);
        }
        return null;
    }

    /**
     * Get relative image path from absolute file path
     */
    private String getRelativeImagePath(Path imageFile) {
        try {
            Path uploadDir = Paths.get(getUploadDir());
            Path relativePath = uploadDir.relativize(imageFile);
            return relativePath.toString().replace('\\', '/'); // Normalize path separators
        } catch (Exception e) {
            logger.debug("Failed to get relative path for: {}", imageFile);
            return imageFile.toString();
        }
    }

    /**
     * Check if directory is empty or will be empty after cleanup
     */
    private boolean isDirectoryEmptyOrWillBeEmpty(Path dir) {
        try {
            return Files.walk(dir)
                .filter(Files::isRegularFile)
                .count() == 0;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Check if directory is empty
     */
    private boolean isDirEmpty(Path dir) {
        try {
            return Files.list(dir).count() == 0;
        } catch (IOException e) {
            return false;
        }
    }

    /**
     * Clean up content images for a story
     */
    public void deleteContentImages(UUID storyId) {
        try {
            Path contentDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory(), storyId.toString());
            if (Files.exists(contentDir)) {
                Files.walk(contentDir)
                    .sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(java.io.File::delete);
            }
        } catch (IOException e) {
            // Log but don't throw - this is cleanup
            System.err.println("Failed to clean up content images for story " + storyId + ": " + e.getMessage());
        }
    }

    /**
     * Migrate EPUB content images that were incorrectly stored flat in content/
     * instead of content/{storyId}/.
     */
    public EpubImageMigrationResult migrateEpubContentImages(boolean dryRun) {
        logger.info("Starting EPUB content image migration (dryRun: {})", dryRun);

        List<Map<String, Object>> movedFiles = new ArrayList<>();
        List<String> errors = new ArrayList<>();
        int movedCount = 0;
        int unmatchedCount = 0;

        try {
            Path contentDir = Paths.get(getUploadDir(), ImageType.CONTENT.getDirectory());
            if (!Files.exists(contentDir)) {
                logger.debug("Content images directory does not exist: {}", contentDir);
                return new EpubImageMigrationResult(movedFiles, movedCount, unmatchedCount, errors, dryRun);
            }

            // Collect flat image files (depth 1 only — files directly in content/, not in subdirs)
            List<Path> flatFiles;
            try (var stream = Files.list(contentDir)) {
                flatFiles = stream
                    .filter(Files::isRegularFile)
                    .filter(p -> !isSynologySystemPath(p))
                    .filter(this::isValidImageFile)
                    .toList();
            }

            if (flatFiles.isEmpty()) {
                logger.info("No flat EPUB images found to migrate");
                return new EpubImageMigrationResult(movedFiles, movedCount, unmatchedCount, errors, dryRun);
            }

            logger.info("Found {} flat content images to process", flatFiles.size());

            // Load all stories once
            List<com.storycove.entity.Story> allStories = storyService.findAllWithAssociations();

            for (Path flatFile : flatFiles) {
                try {
                    String filename = flatFile.getFileName().toString();
                    logger.debug("Processing flat image: {}", filename);

                    // Find stories that reference this file by filename in their content HTML
                    List<com.storycove.entity.Story> referencingStories = allStories.stream()
                        .filter(s -> s.getContentHtml() != null && s.getContentHtml().contains("content/" + filename))
                        .toList();

                    long fileSize = Files.exists(flatFile) ? Files.size(flatFile) : 0;

                    if (referencingStories.isEmpty()) {
                        logger.debug("No story references flat image: {}", filename);
                        unmatchedCount++;
                        Map<String, Object> entry = new HashMap<>();
                        entry.put("fileName", filename);
                        entry.put("filePath", flatFile.toString());
                        entry.put("fileSize", fileSize);
                        entry.put("formattedSize", formatFileSize(fileSize));
                        entry.put("storyId", null);
                        entry.put("storyTitle", null);
                        entry.put("action", "unmatched");
                        movedFiles.add(entry);
                        continue;
                    }

                    // Move/copy to each referencing story's subdirectory
                    boolean firstStory = true;
                    for (com.storycove.entity.Story story : referencingStories) {
                        String storyId = story.getId().toString();
                        Path targetDir = contentDir.resolve(storyId);
                        Path targetFile = targetDir.resolve(filename);

                        // Build the new URL for this story's library
                        String currentLibraryId = libraryService.getCurrentLibraryId();
                        String oldUrl = "/api/files/images/" + currentLibraryId + "/content/" + filename;
                        String newUrl = "/api/files/images/" + currentLibraryId + "/content/" + storyId + "/" + filename;

                        Map<String, Object> entry = new HashMap<>();
                        entry.put("fileName", filename);
                        entry.put("filePath", flatFile.toString());
                        entry.put("targetPath", targetFile.toString());
                        entry.put("fileSize", fileSize);
                        entry.put("formattedSize", formatFileSize(fileSize));
                        entry.put("storyId", storyId);
                        entry.put("storyTitle", story.getTitle());
                        entry.put("action", referencingStories.size() > 1 ? "copy" : "move");
                        movedFiles.add(entry);

                        if (!dryRun) {
                            Files.createDirectories(targetDir);
                            if (firstStory && referencingStories.size() == 1) {
                                Files.move(flatFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
                            } else {
                                Files.copy(flatFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
                            }

                            // Update story content to point to the new path
                            String updatedContent = story.getContentHtml().replace(oldUrl, newUrl);
                            if (!updatedContent.equals(story.getContentHtml())) {
                                story.setContentHtml(updatedContent);
                                storyService.update(story.getId(), story);
                                logger.debug("Updated story {} content with new image path", storyId);
                            }
                        }

                        firstStory = false;
                        movedCount++;
                    }

                    // If multiple stories referenced the same file, delete the original after copying
                    if (!dryRun && referencingStories.size() > 1 && Files.exists(flatFile)) {
                        Files.deleteIfExists(flatFile);
                    }

                } catch (Exception e) {
                    logger.error("Error migrating flat image {}: {}", flatFile.getFileName(), e.getMessage(), e);
                    errors.add("Failed to migrate " + flatFile.getFileName() + ": " + e.getMessage());
                }
            }

            logger.info("EPUB image migration completed. Moved: {}, Unmatched: {}, Errors: {}",
                movedCount, unmatchedCount, errors.size());

        } catch (Exception e) {
            logger.error("Error during EPUB image migration", e);
            errors.add("General migration error: " + e.getMessage());
        }

        return new EpubImageMigrationResult(movedFiles, movedCount, unmatchedCount, errors, dryRun);
    }

    private String formatFileSize(long bytes) {
        if (bytes < 1024) return bytes + " B";
        if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
        if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
        return String.format("%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
    }

    /**
     * Result class for content image processing
     */
    public static class ContentImageProcessingResult {
        private final String processedContent;
        private final List<String> warnings;
        private final List<String> downloadedImages;

        public ContentImageProcessingResult(String processedContent, List<String> warnings, List<String> downloadedImages) {
            this.processedContent = processedContent;
            this.warnings = warnings;
            this.downloadedImages = downloadedImages;
        }

        public String getProcessedContent() { return processedContent; }
        public List<String> getWarnings() { return warnings; }
        public List<String> getDownloadedImages() { return downloadedImages; }
        public boolean hasWarnings() { return !warnings.isEmpty(); }
    }

    /**
     * Result class for orphaned image cleanup
     */
    public static class ContentImageCleanupResult {
        private final List<String> orphanedImages;
        private final long totalSizeBytes;
        private final int foldersToDelete;
        private final int totalReferencedImages;
        private final List<String> errors;
        private final boolean dryRun;

        public ContentImageCleanupResult(List<String> orphanedImages, long totalSizeBytes, int foldersToDelete,
                                       int totalReferencedImages, List<String> errors, boolean dryRun) {
            this.orphanedImages = orphanedImages;
            this.totalSizeBytes = totalSizeBytes;
            this.foldersToDelete = foldersToDelete;
            this.totalReferencedImages = totalReferencedImages;
            this.errors = errors;
            this.dryRun = dryRun;
        }

        public List<String> getOrphanedImages() { return orphanedImages; }
        public long getTotalSizeBytes() { return totalSizeBytes; }
        public int getFoldersToDelete() { return foldersToDelete; }
        public int getTotalReferencedImages() { return totalReferencedImages; }
        public List<String> getErrors() { return errors; }
        public boolean isDryRun() { return dryRun; }
        public boolean hasErrors() { return !errors.isEmpty(); }

        public String getFormattedSize() {
            if (totalSizeBytes < 1024) return totalSizeBytes + " B";
            if (totalSizeBytes < 1024 * 1024) return String.format("%.1f KB", totalSizeBytes / 1024.0);
            if (totalSizeBytes < 1024 * 1024 * 1024) return String.format("%.1f MB", totalSizeBytes / (1024.0 * 1024.0));
            return String.format("%.1f GB", totalSizeBytes / (1024.0 * 1024.0 * 1024.0));
        }
    }

    /**
     * Result class for EPUB content image migration
     */
    public static class EpubImageMigrationResult {
        private final List<Map<String, Object>> movedFiles;
        private final int movedCount;
        private final int unmatchedCount;
        private final List<String> errors;
        private final boolean dryRun;

        public EpubImageMigrationResult(List<Map<String, Object>> movedFiles, int movedCount,
                                        int unmatchedCount, List<String> errors, boolean dryRun) {
            this.movedFiles = movedFiles;
            this.movedCount = movedCount;
            this.unmatchedCount = unmatchedCount;
            this.errors = errors;
            this.dryRun = dryRun;
        }

        public List<Map<String, Object>> getMovedFiles() { return movedFiles; }
        public int getMovedCount() { return movedCount; }
        public int getUnmatchedCount() { return unmatchedCount; }
        public List<String> getErrors() { return errors; }
        public boolean isDryRun() { return dryRun; }
        public boolean hasErrors() { return !errors.isEmpty(); }
    }

    /**
     * Check if a path is a Synology system path that should be ignored
     */
    private boolean isSynologySystemPath(Path path) {
        String pathStr = path.toString();
        String fileName = path.getFileName().toString();

        // Skip Synology metadata directories and files
        return pathStr.contains("@eaDir") ||
               fileName.startsWith("@") ||
               fileName.contains("@SynoEAStream") ||
               fileName.startsWith(".") ||
               fileName.equals("Thumbs.db") ||
               fileName.equals(".DS_Store");
    }

    /**
     * Check if a file is a valid image file (not a system/metadata file)
     */
    private boolean isValidImageFile(Path path) {
        if (isSynologySystemPath(path)) {
            return false;
        }

        String fileName = path.getFileName().toString().toLowerCase();
        return fileName.endsWith(".jpg") ||
               fileName.endsWith(".jpeg") ||
               fileName.endsWith(".png") ||
               fileName.endsWith(".gif") ||
               fileName.endsWith(".webp");
    }

    /**
     * Extract filename from a path or URL
     */
    private String extractFilename(String pathOrUrl) {
        if (pathOrUrl == null || pathOrUrl.trim().isEmpty()) {
            return null;
        }

        try {
            // Remove query parameters if present
            if (pathOrUrl.contains("?")) {
                pathOrUrl = pathOrUrl.substring(0, pathOrUrl.indexOf("?"));
            }

            // Get the last part after slash
            String filename = pathOrUrl.substring(pathOrUrl.lastIndexOf("/") + 1);

            // Remove any special Synology suffixes
            filename = filename.replace("@SynoEAStream", "");

            return filename.trim().isEmpty() ? null : filename;
        } catch (Exception e) {
            logger.debug("Failed to extract filename from: {}", pathOrUrl);
            return null;
        }
    }

    /**
     * Check if a filename follows UUID pattern (indicates it's our generated file)
     */
    private boolean isUuidBasedFilename(String filename) {
        if (filename == null || filename.trim().isEmpty()) {
            return false;
        }

        // Remove extension
        String nameWithoutExt = filename;
        int lastDot = filename.lastIndexOf(".");
        if (lastDot > 0) {
            nameWithoutExt = filename.substring(0, lastDot);
        }

        // Check if it matches UUID pattern (8-4-4-4-12 hex characters)
        return nameWithoutExt.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
    }

    /**
     * Event listener for story content updates - processes external images asynchronously
     */
    @EventListener
    @Async
    public void handleStoryContentUpdated(StoryContentUpdatedEvent event) {
        logger.info("Processing images for {} story {} after content update",
                   event.isNewStory() ? "new" : "updated", event.getStoryId());

        try {
            ContentImageProcessingResult result = processContentImages(event.getContentHtml(), event.getStoryId());

            // If content was changed, we need to update the story (but this could cause circular events)
            // Instead, let's just log the results for now and let the controller handle updates if needed
            if (result.hasWarnings()) {
                logger.warn("Image processing warnings for story {}: {}", event.getStoryId(), result.getWarnings());
            }
            if (!result.getDownloadedImages().isEmpty()) {
                logger.info("Downloaded {} external images for story {}: {}",
                           result.getDownloadedImages().size(), event.getStoryId(), result.getDownloadedImages());
            }

            // TODO: If content was changed, we might need a way to update the story without triggering another event
            if (!result.getProcessedContent().equals(event.getContentHtml())) {
                logger.info("Story {} content was processed and external images were replaced with local URLs", event.getStoryId());
                // For now, just log that processing occurred - the original content processing already handles updates
            }

        } catch (Exception e) {
            logger.error("Failed to process images for story {}: {}", event.getStoryId(), e.getMessage(), e);
        }
    }
}