package com.storycove.service;

import com.storycove.dto.FileImportResponse;
import com.storycove.dto.PDFImportRequest;
import com.storycove.entity.*;
import com.storycove.service.exception.InvalidFileException;
import com.storycove.service.exception.ResourceNotFoundException;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.regex.Pattern;

@Service
@Transactional
public class PDFImportService {
    private static final Logger log = LoggerFactory.getLogger(PDFImportService.class);

    private static final Pattern PAGE_NUMBER_PATTERN = Pattern.compile("^\\s*\\d+\\s*$");
    private static final int MAX_FILE_SIZE = 300 * 1024 * 1024; // 300MB

    private final StoryService storyService;
    private final AuthorService authorService;
    private final SeriesService seriesService;
    private final TagService tagService;
    private final HtmlSanitizationService sanitizationService;
    private final ImageService imageService;
    private final LibraryService libraryService;

    @Autowired
    public PDFImportService(StoryService storyService,
                           AuthorService authorService,
                           SeriesService seriesService,
                           TagService tagService,
                           HtmlSanitizationService sanitizationService,
                           ImageService imageService,
                           LibraryService libraryService) {
        this.storyService = storyService;
        this.authorService = authorService;
        this.seriesService = seriesService;
        this.tagService = tagService;
        this.sanitizationService = sanitizationService;
        this.imageService = imageService;
        this.libraryService = libraryService;
    }

    public FileImportResponse importPDF(PDFImportRequest request) {
        try {
            MultipartFile pdfFile = request.getPdfFile();

            if (pdfFile == null || pdfFile.isEmpty()) {
                return FileImportResponse.error("PDF file is required", null);
            }

            if (!isValidPDFFile(pdfFile)) {
                return FileImportResponse.error("Invalid PDF file format", pdfFile.getOriginalFilename());
            }

            log.info("Parsing PDF file: {}", pdfFile.getOriginalFilename());
            PDDocument document = parsePDFFile(pdfFile);

            try {
                log.info("Extracting metadata from PDF");
                PDFMetadata metadata = extractMetadata(document, pdfFile.getOriginalFilename());

                // Validate author is provided
                String authorName = determineAuthorName(request, metadata);
                if (authorName == null || authorName.trim().isEmpty()) {
                    return FileImportResponse.error("Author name is required for PDF import. No author found in PDF metadata.", pdfFile.getOriginalFilename());
                }

                log.info("Extracting content and images from PDF");
                PDFContent content = extractContentWithImages(document, request.getExtractImages());

                log.info("Creating story entity from PDF");
                Story story = createStoryFromPDF(metadata, content, request, authorName);

                log.info("Saving story to database: {}", story.getTitle());
                Story savedStory = storyService.create(story);
                log.info("Story saved successfully with ID: {}", savedStory.getId());

                // Process and save embedded images if any were extracted
                if (request.getExtractImages() && !content.getImages().isEmpty()) {
                    try {
                        log.info("Processing {} embedded images for story: {}", content.getImages().size(), savedStory.getId());
                        String updatedContent = processAndSaveImages(content, savedStory.getId());

                        if (!updatedContent.equals(savedStory.getContentHtml())) {
                            savedStory.setContentHtml(updatedContent);
                            savedStory = storyService.update(savedStory.getId(), savedStory);
                            log.info("Story content updated with processed images");
                        }
                    } catch (Exception e) {
                        log.error("Failed to process embedded images for story {}: {}", savedStory.getId(), e.getMessage(), e);
                    }
                }

                log.info("PDF import completed successfully for: {}", savedStory.getTitle());
                FileImportResponse response = FileImportResponse.success(savedStory.getId(), savedStory.getTitle(), "PDF");
                response.setFileName(pdfFile.getOriginalFilename());
                response.setWordCount(savedStory.getWordCount());
                response.setExtractedImages(content.getImages().size());

                return response;

            } finally {
                document.close();
            }

        } catch (Exception e) {
            log.error("PDF import failed with exception: {}", e.getMessage(), e);
            return FileImportResponse.error("Failed to import PDF: " + e.getMessage(),
                request.getPdfFile() != null ? request.getPdfFile().getOriginalFilename() : null);
        }
    }

    private boolean isValidPDFFile(MultipartFile file) {
        String filename = file.getOriginalFilename();
        if (filename == null || !filename.toLowerCase().endsWith(".pdf")) {
            return false;
        }

        if (file.getSize() > MAX_FILE_SIZE) {
            log.warn("PDF file size {} exceeds maximum {}", file.getSize(), MAX_FILE_SIZE);
            return false;
        }

        String contentType = file.getContentType();
        return "application/pdf".equals(contentType) || contentType == null;
    }

    private PDDocument parsePDFFile(MultipartFile pdfFile) throws IOException {
        try (InputStream inputStream = pdfFile.getInputStream()) {
            return Loader.loadPDF(inputStream.readAllBytes());
        } catch (Exception e) {
            throw new InvalidFileException("Failed to parse PDF file: " + e.getMessage());
        }
    }

    private PDFMetadata extractMetadata(PDDocument document, String fileName) {
        PDFMetadata metadata = new PDFMetadata();
        PDDocumentInformation info = document.getDocumentInformation();

        if (info != null) {
            metadata.setTitle(info.getTitle());
            metadata.setAuthor(info.getAuthor());
            metadata.setSubject(info.getSubject());
            metadata.setKeywords(info.getKeywords());
            metadata.setCreator(info.getCreator());
        }

        // Use filename as fallback title
        if (metadata.getTitle() == null || metadata.getTitle().trim().isEmpty()) {
            String titleFromFilename = fileName.replaceAll("\\.pdf$", "").replaceAll("[_-]", " ");
            metadata.setTitle(titleFromFilename);
        }

        metadata.setPageCount(document.getNumberOfPages());

        return metadata;
    }

    private PDFContent extractContentWithImages(PDDocument document, Boolean extractImages) throws IOException {
        PDFContent content = new PDFContent();
        StringBuilder htmlContent = new StringBuilder();
        List<PDFImage> images = new ArrayList<>();

        boolean shouldExtractImages = extractImages != null && extractImages;

        // Extract images first to know their positions
        if (shouldExtractImages) {
            images = extractImagesFromPDF(document);
            log.info("Extracted {} images from PDF", images.size());
        }

        // Extract text with custom stripper to filter headers/footers
        CustomPDFTextStripper stripper = new CustomPDFTextStripper();
        stripper.setSortByPosition(true);

        // Process page by page to insert images at correct positions
        for (int pageNum = 0; pageNum < document.getNumberOfPages(); pageNum++) {
            stripper.setStartPage(pageNum + 1);
            stripper.setEndPage(pageNum + 1);

            String pageText = stripper.getText(document);

            // Filter out obvious page numbers and headers/footers
            pageText = filterHeadersFooters(pageText, pageNum + 1);

            if (pageText != null && !pageText.trim().isEmpty()) {
                // Convert text to HTML paragraphs
                String[] paragraphs = pageText.split("\\n\\s*\\n");

                for (String para : paragraphs) {
                    String trimmed = para.trim();
                    if (!trimmed.isEmpty() && !isLikelyHeaderFooter(trimmed)) {
                        // Normalize soft line wraps (single newlines from PDF layout) to spaces
                        String normalized = trimmed.replaceAll("[ \t]*\n[ \t]*", " ").replaceAll(" {2,}", " ").trim();
                        htmlContent.append("<p>").append(escapeHtml(normalized)).append("</p>\n");
                    }
                }
            }

            // Insert images that belong to this page
            if (shouldExtractImages) {
                for (PDFImage image : images) {
                    if (image.getPageNumber() == pageNum) {
                        // Add placeholder for image (will be replaced with actual path after saving)
                        htmlContent.append("<img data-pdf-image-id=\"")
                                  .append(image.getImageId())
                                  .append("\" alt=\"Image from PDF\" />\n");
                    }
                }
            }
        }

        content.setHtmlContent(htmlContent.toString());
        content.setImages(images);

        return content;
    }

    private List<PDFImage> extractImagesFromPDF(PDDocument document) {
        List<PDFImage> images = new ArrayList<>();
        int imageCounter = 0;

        for (int pageNum = 0; pageNum < document.getNumberOfPages(); pageNum++) {
            try {
                PDPage page = document.getPage(pageNum);

                // Get all images from the page resources
                Iterable<org.apache.pdfbox.cos.COSName> names = page.getResources().getXObjectNames();
                for (org.apache.pdfbox.cos.COSName name : names) {
                    try {
                        org.apache.pdfbox.pdmodel.graphics.PDXObject xObject = page.getResources().getXObject(name);

                        if (xObject instanceof PDImageXObject) {
                            PDImageXObject imageObj = (PDImageXObject) xObject;
                            BufferedImage bImage = imageObj.getImage();

                            // Skip very small images (likely decorative or icons)
                            if (bImage.getWidth() < 50 || bImage.getHeight() < 50) {
                                continue;
                            }

                            // Convert BufferedImage to byte array
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ImageIO.write(bImage, "png", baos);
                            byte[] imageBytes = baos.toByteArray();

                            PDFImage pdfImage = new PDFImage();
                            pdfImage.setImageId("pdf-img-" + imageCounter);
                            pdfImage.setPageNumber(pageNum);
                            pdfImage.setImageData(imageBytes);
                            pdfImage.setWidth(bImage.getWidth());
                            pdfImage.setHeight(bImage.getHeight());

                            images.add(pdfImage);
                            imageCounter++;
                        }
                    } catch (Exception e) {
                        log.warn("Failed to extract image '{}' from page {}: {}", name, pageNum, e.getMessage());
                    }
                }
            } catch (Exception e) {
                log.warn("Failed to process images on page {}: {}", pageNum, e.getMessage());
            }
        }

        return images;
    }

    private String processAndSaveImages(PDFContent content, UUID storyId) throws IOException {
        String htmlContent = content.getHtmlContent();

        // Get current library ID for constructing image URLs
        String currentLibraryId = libraryService.getCurrentLibraryId();
        if (currentLibraryId == null || currentLibraryId.trim().isEmpty()) {
            log.warn("Current library ID is null or empty when processing PDF images for story: {}", storyId);
            currentLibraryId = "default";
        }

        for (PDFImage image : content.getImages()) {
            try {
                // Create a MultipartFile from the image bytes
                MultipartFile imageFile = new PDFImageMultipartFile(
                    image.getImageData(),
                    "pdf-image-" + image.getImageId() + ".png",
                    "image/png"
                );

                // Save the image using ImageService (ImageType.CONTENT saves to content directory)
                String imagePath = imageService.uploadImage(imageFile, ImageService.ImageType.CONTENT);

                // Construct the full URL with library ID
                // imagePath will be like "content/uuid.png"
                String imageUrl = "/api/files/images/" + currentLibraryId + "/" + imagePath;

                // Replace placeholder with actual image URL
                String placeholder = "data-pdf-image-id=\"" + image.getImageId() + "\"";
                String replacement = "src=\"" + imageUrl + "\"";
                htmlContent = htmlContent.replace(placeholder, replacement);

                log.debug("Saved PDF image {} to path: {} (URL: {})", image.getImageId(), imagePath, imageUrl);

            } catch (Exception e) {
                log.error("Failed to save PDF image {}: {}", image.getImageId(), e.getMessage());
                // Remove the placeholder if we failed to save the image
                htmlContent = htmlContent.replaceAll(
                    "<img data-pdf-image-id=\"" + image.getImageId() + "\"[^>]*>",
                    ""
                );
            }
        }

        return htmlContent;
    }

    private String filterHeadersFooters(String text, int pageNumber) {
        if (text == null) return "";

        String[] lines = text.split("\\n");
        if (lines.length <= 2) return text; // Too short to have headers/footers

        StringBuilder filtered = new StringBuilder();

        // Skip first line if it looks like a header
        int startIdx = 0;
        if (lines.length > 1 && isLikelyHeaderFooter(lines[0])) {
            startIdx = 1;
        }

        // Skip last line if it looks like a footer or page number
        int endIdx = lines.length;
        if (lines.length > 1 && isLikelyHeaderFooter(lines[lines.length - 1])) {
            endIdx = lines.length - 1;
        }

        for (int i = startIdx; i < endIdx; i++) {
            filtered.append(lines[i]).append("\n");
        }

        return filtered.toString();
    }

    private boolean isLikelyHeaderFooter(String line) {
        String trimmed = line.trim();

        // Check if it's just a page number
        if (PAGE_NUMBER_PATTERN.matcher(trimmed).matches()) {
            return true;
        }

        // Check if it's very short (likely header/footer)
        if (trimmed.length() < 3) {
            return true;
        }

        // Check for common header/footer patterns
        String lower = trimmed.toLowerCase();
        if (lower.matches(".*page \\d+.*") ||
            lower.matches(".*\\d+ of \\d+.*") ||
            lower.matches("chapter \\d+") ||
            lower.matches("\\d+")) {
            return true;
        }

        return false;
    }

    private String determineAuthorName(PDFImportRequest request, PDFMetadata metadata) {
        // Priority: request.authorName > request.authorId > metadata.author
        if (request.getAuthorName() != null && !request.getAuthorName().trim().isEmpty()) {
            return request.getAuthorName().trim();
        }

        if (request.getAuthorId() != null) {
            try {
                Author author = authorService.findById(request.getAuthorId());
                return author.getName();
            } catch (ResourceNotFoundException e) {
                log.warn("Author ID {} not found", request.getAuthorId());
            }
        }

        if (metadata.getAuthor() != null && !metadata.getAuthor().trim().isEmpty()) {
            return metadata.getAuthor().trim();
        }

        return null;
    }

    private Story createStoryFromPDF(PDFMetadata metadata, PDFContent content,
                                    PDFImportRequest request, String authorName) {
        Story story = new Story();
        story.setTitle(metadata.getTitle() != null ? metadata.getTitle() : "Untitled PDF");
        story.setDescription(metadata.getSubject());
        story.setContentHtml(sanitizationService.sanitize(content.getHtmlContent()));

        // Handle author assignment
        try {
            if (request.getAuthorId() != null) {
                try {
                    Author author = authorService.findById(request.getAuthorId());
                    story.setAuthor(author);
                } catch (ResourceNotFoundException e) {
                    if (request.getCreateMissingAuthor()) {
                        Author newAuthor = createAuthor(authorName);
                        story.setAuthor(newAuthor);
                    }
                }
            } else if (authorName != null && request.getCreateMissingAuthor()) {
                Author author = findOrCreateAuthor(authorName);
                story.setAuthor(author);
            }
        } catch (Exception e) {
            log.error("Error handling author assignment: {}", e.getMessage(), e);
            throw e;
        }

        // Handle series assignment
        try {
            if (request.getSeriesId() != null && request.getSeriesVolume() != null) {
                try {
                    Series series = seriesService.findById(request.getSeriesId());
                    story.setSeries(series);
                    story.setVolume(request.getSeriesVolume());
                } catch (ResourceNotFoundException e) {
                    if (request.getCreateMissingSeries() && request.getSeriesName() != null) {
                        Series newSeries = createSeries(request.getSeriesName());
                        story.setSeries(newSeries);
                        story.setVolume(request.getSeriesVolume());
                    }
                }
            }
        } catch (Exception e) {
            log.error("Error handling series assignment: {}", e.getMessage(), e);
            throw e;
        }

        // Handle tags
        try {
            List<String> allTags = new ArrayList<>();
            if (request.getTags() != null && !request.getTags().isEmpty()) {
                allTags.addAll(request.getTags());
            }

            // Extract keywords from PDF metadata
            if (metadata.getKeywords() != null && !metadata.getKeywords().trim().isEmpty()) {
                String[] keywords = metadata.getKeywords().split("[,;]");
                for (String keyword : keywords) {
                    String trimmed = keyword.trim();
                    if (!trimmed.isEmpty()) {
                        allTags.add(trimmed);
                    }
                }
            }

            // Create tags
            allTags.stream()
                    .distinct()
                    .forEach(tagName -> {
                        try {
                            Tag tag = tagService.findOrCreate(tagName.trim());
                            story.addTag(tag);
                        } catch (Exception e) {
                            log.error("Error creating tag '{}': {}", tagName, e.getMessage(), e);
                        }
                    });
        } catch (Exception e) {
            log.error("Error handling tags: {}", e.getMessage(), e);
            throw e;
        }

        return story;
    }

    private Author findOrCreateAuthor(String authorName) {
        Optional<Author> existingAuthor = authorService.findByNameOptional(authorName);
        if (existingAuthor.isPresent()) {
            return existingAuthor.get();
        }
        return createAuthor(authorName);
    }

    private Author createAuthor(String authorName) {
        Author author = new Author();
        author.setName(authorName);
        return authorService.create(author);
    }

    private Series createSeries(String seriesName) {
        Series series = new Series();
        series.setName(seriesName);
        return seriesService.create(series);
    }

    private String escapeHtml(String text) {
        return text.replace("&", "&amp;")
                   .replace("<", "&lt;")
                   .replace(">", "&gt;")
                   .replace("\"", "&quot;")
                   .replace("'", "&#39;")
                   .replace("\n", "<br/>");
    }

    public List<String> validatePDFFile(MultipartFile file) {
        List<String> errors = new ArrayList<>();

        if (file == null || file.isEmpty()) {
            errors.add("PDF file is required");
            return errors;
        }

        if (!isValidPDFFile(file)) {
            errors.add("Invalid PDF file format. Only .pdf files are supported");
        }

        if (file.getSize() > MAX_FILE_SIZE) {
            errors.add("PDF file size exceeds " + (MAX_FILE_SIZE / 1024 / 1024) + "MB limit");
        }

        try {
            PDDocument document = parsePDFFile(file);
            try {
                if (document.getNumberOfPages() == 0) {
                    errors.add("PDF file contains no pages");
                }
            } finally {
                document.close();
            }
        } catch (Exception e) {
            errors.add("Failed to parse PDF file: " + e.getMessage());
        }

        return errors;
    }

    // Inner classes for data structures

    private static class PDFMetadata {
        private String title;
        private String author;
        private String subject;
        private String keywords;
        private String creator;
        private int pageCount;

        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getAuthor() { return author; }
        public void setAuthor(String author) { this.author = author; }
        public String getSubject() { return subject; }
        public void setSubject(String subject) { this.subject = subject; }
        public String getKeywords() { return keywords; }
        public void setKeywords(String keywords) { this.keywords = keywords; }
        public String getCreator() { return creator; }
        public void setCreator(String creator) { this.creator = creator; }
        public int getPageCount() { return pageCount; }
        public void setPageCount(int pageCount) { this.pageCount = pageCount; }
    }

    private static class PDFContent {
        private String htmlContent;
        private List<PDFImage> images = new ArrayList<>();

        public String getHtmlContent() { return htmlContent; }
        public void setHtmlContent(String htmlContent) { this.htmlContent = htmlContent; }
        public List<PDFImage> getImages() { return images; }
        public void setImages(List<PDFImage> images) { this.images = images; }
    }

    private static class PDFImage {
        private String imageId;
        private int pageNumber;
        private byte[] imageData;
        private int width;
        private int height;

        public String getImageId() { return imageId; }
        public void setImageId(String imageId) { this.imageId = imageId; }
        public int getPageNumber() { return pageNumber; }
        public void setPageNumber(int pageNumber) { this.pageNumber = pageNumber; }
        public byte[] getImageData() { return imageData; }
        public void setImageData(byte[] imageData) { this.imageData = imageData; }
        public int getWidth() { return width; }
        public void setWidth(int width) { this.width = width; }
        public int getHeight() { return height; }
        public void setHeight(int height) { this.height = height; }
    }

    /**
     * Custom PDF text stripper to filter headers/footers
     */
    private static class CustomPDFTextStripper extends PDFTextStripper {
        public CustomPDFTextStripper() throws IOException {
            super();
        }

        @Override
        protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
            super.writeString(text, textPositions);
        }
    }

    /**
     * Custom MultipartFile implementation for PDF images
     */
    private static class PDFImageMultipartFile implements MultipartFile {
        private final byte[] data;
        private final String filename;
        private final String contentType;

        public PDFImageMultipartFile(byte[] data, String filename, String contentType) {
            this.data = data;
            this.filename = filename;
            this.contentType = contentType;
        }

        @Override
        public String getName() {
            return "image";
        }

        @Override
        public String getOriginalFilename() {
            return filename;
        }

        @Override
        public String getContentType() {
            return contentType;
        }

        @Override
        public boolean isEmpty() {
            return data == null || data.length == 0;
        }

        @Override
        public long getSize() {
            return data != null ? data.length : 0;
        }

        @Override
        public byte[] getBytes() {
            return data;
        }

        @Override
        public InputStream getInputStream() {
            return new ByteArrayInputStream(data);
        }

        @Override
        public void transferTo(java.io.File dest) throws IOException {
            try (java.io.FileOutputStream fos = new java.io.FileOutputStream(dest)) {
                fos.write(data);
            }
        }

        @Override
        public void transferTo(java.nio.file.Path dest) throws IOException {
            java.nio.file.Files.write(dest, data);
        }
    }
}
