package stirling.software.common.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;

import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
import com.vladsch.flexmark.util.data.MutableDataSet;

import io.github.pixee.security.Filenames;

import lombok.extern.slf4j.Slf4j;

import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;

@Slf4j
public class PDFToFile {

    private static final Pattern PATTERN =
            Pattern.compile("(!\\[.*?\\])\\((?!images/)([^/)][^)]*?)\\)");
    private final TempFileManager tempFileManager;
    private final RuntimePathConfig runtimePathConfig;

    public PDFToFile(TempFileManager tempFileManager) {
        this(tempFileManager, null);
    }

    public PDFToFile(TempFileManager tempFileManager, RuntimePathConfig runtimePathConfig) {
        this.tempFileManager = tempFileManager;
        this.runtimePathConfig = runtimePathConfig;
    }

    public ResponseEntity<Resource> processPdfToMarkdown(MultipartFile inputFile)
            throws IOException, InterruptedException {
        if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        MutableDataSet options =
                new MutableDataSet()
                        .set(
                                FlexmarkHtmlConverter.MAX_BLANK_LINES,
                                2) // Control max consecutive blank lines
                        .set(
                                FlexmarkHtmlConverter.MAX_TRAILING_BLANK_LINES,
                                1) // Control trailing blank lines
                        .set(
                                FlexmarkHtmlConverter.SETEXT_HEADINGS,
                                true) // Use Setext headings for h1 and h2
                        .set(
                                FlexmarkHtmlConverter.OUTPUT_UNKNOWN_TAGS,
                                false) // Don't output HTML for unknown tags
                        .set(
                                FlexmarkHtmlConverter.TYPOGRAPHIC_QUOTES,
                                true) // Convert quotation marks
                        .set(
                                FlexmarkHtmlConverter.BR_AS_PARA_BREAKS,
                                true) // Convert <br> to paragraph breaks
                        .set(FlexmarkHtmlConverter.CODE_INDENT, "    "); // Indent for code blocks

        FlexmarkHtmlConverter htmlToMarkdownConverter =
                FlexmarkHtmlConverter.builder(options).build();

        String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
        String pdfBaseName = originalPdfFileName;
        if (originalPdfFileName.contains(".")) {
            pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
        }

        String fileName = pdfBaseName + "ToMarkdown.zip";
        TempFile finalOut = tempFileManager.createManagedTempFile(".zip");
        try {
            try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
                    TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) {
                inputFile.transferTo(tempInputFile.getFile());

                List<String> command =
                        new ArrayList<>(
                                Arrays.asList(
                                        "pdftohtml",
                                        "-s",
                                        "-noframes",
                                        "-c",
                                        tempInputFile.getAbsolutePath(),
                                        pdfBaseName));

                ProcessExecutorResult returnCode =
                        ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
                                .runCommandWithOutputHandling(
                                        command, tempOutputDir.getPath().toFile());
                // Process HTML files to Markdown
                File[] outputFiles =
                        Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
                List<File> markdownFiles = new ArrayList<>();
                List<File> imageFiles = new ArrayList<>();

                // Convert HTML files to Markdown and collect image files
                for (File outputFile : outputFiles) {
                    if (outputFile.getName().endsWith(".html")) {
                        String html = Files.readString(outputFile.toPath());
                        String markdown = htmlToMarkdownConverter.convert(html);

                        // Update image references to point to images/ folder
                        markdown = updateImageReferences(markdown);

                        String mdFileName = outputFile.getName().replace(".html", ".md");
                        File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
                        Files.writeString(mdFile.toPath(), markdown);
                        markdownFiles.add(mdFile);
                    } else if (!outputFile.getName().endsWith(".md")) {
                        // Collect non-HTML, non-MD files as images/assets
                        imageFiles.add(outputFile);
                    }
                }

                try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
                        ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
                    // Add markdown files to root of ZIP
                    for (File mdFile : markdownFiles) {
                        ZipEntry mdEntry = new ZipEntry(mdFile.getName());
                        zipOutputStream.putNextEntry(mdEntry);
                        Files.copy(mdFile.toPath(), zipOutputStream);
                        zipOutputStream.closeEntry();
                    }

                    // Add images and other assets to images/ folder
                    for (File imageFile : imageFiles) {
                        ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName());
                        zipOutputStream.putNextEntry(assetEntry);
                        Files.copy(imageFile.toPath(), zipOutputStream);
                        zipOutputStream.closeEntry();
                    }
                }
            }
        } catch (Exception e) {
            finalOut.close();
            throw e;
        }
        return WebResponseUtils.fileToWebResponse(
                finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
    }

    /**
     * Updates image references in markdown to point to the images/ folder. Matches patterns like
     * ![alt](filename.png) and converts to ![alt](images/filename.png)
     */
    private String updateImageReferences(String markdown) {
        // Match markdown image syntax: ![alt text](image.png)
        // Only update if the path doesn't already start with images/
        return PATTERN.matcher(markdown).replaceAll("$1(images/$2)");
    }

    public ResponseEntity<Resource> processPdfToHtml(MultipartFile inputFile)
            throws IOException, InterruptedException {
        if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        // Get the original PDF file name without the extension
        String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
        String pdfBaseName = originalPdfFileName;
        if (originalPdfFileName.contains(".")) {
            pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
        }

        String fileName = pdfBaseName + "ToHtml.zip";
        TempFile finalOut = tempFileManager.createManagedTempFile(".zip");
        try {
            try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
                    TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {

                Path tempInputFile = inputFileTemp.getPath();
                Path tempOutputDir = outputDirTemp.getPath();

                // Save the uploaded file to a temporary location
                inputFile.transferTo(tempInputFile);

                // Run the pdftohtml command with complex output
                List<String> command =
                        new ArrayList<>(
                                Arrays.asList(
                                        "pdftohtml", "-c", tempInputFile.toString(), pdfBaseName));

                ProcessExecutorResult returnCode =
                        ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
                                .runCommandWithOutputHandling(command, tempOutputDir.toFile());

                // Get output files
                File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());

                try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
                        ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
                    for (File outputFile : outputFiles) {
                        ZipEntry entry = new ZipEntry(outputFile.getName());
                        zipOutputStream.putNextEntry(entry);
                        try (FileInputStream fis = new FileInputStream(outputFile)) {
                            IOUtils.copy(fis, zipOutputStream);
                        } catch (IOException e) {
                            log.error("Exception writing zip entry", e);
                        }
                        zipOutputStream.closeEntry();
                    }
                } catch (IOException e) {
                    log.error("Exception writing zip", e);
                }
            }
        } catch (Exception e) {
            finalOut.close();
            throw e;
        }

        return WebResponseUtils.fileToWebResponse(
                finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
    }

    public ResponseEntity<Resource> processPdfToOfficeFormat(
            MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
            throws IOException, InterruptedException {

        if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        // Get the original PDF file name without the extension
        String originalPdfFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());

        if (originalPdfFileName == null || originalPdfFileName.trim().isEmpty()) {
            originalPdfFileName = "output.pdf";
        }
        // Assume file is pdf if no extension
        String pdfBaseName = originalPdfFileName;
        if (originalPdfFileName.contains(".")) {
            pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
        }
        // Validate output format
        List<String> allowedFormats =
                Arrays.asList("doc", "docx", "odt", "ppt", "pptx", "odp", "rtf", "xml", "txt:Text");
        if (!allowedFormats.contains(outputFormat)) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
        }

        String fileName;
        TempFile finalOut =
                tempFileManager.createManagedTempFile("." + resolvePrimaryExtension(outputFormat));
        Path libreOfficeProfile = null;
        try {
            try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
                    TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {

                Path tempInputFile = inputFileTemp.getPath();
                Path tempOutputDir = outputDirTemp.getPath();
                Path unoOutputFile =
                        tempOutputDir.resolve(
                                pdfBaseName + "." + resolvePrimaryExtension(outputFormat));

                // Save the uploaded file to a temporary location
                inputFile.transferTo(tempInputFile);

                // Run the LibreOffice command
                ProcessExecutorResult returnCode = null;
                IOException unoconvertException = null;

                if (isUnoConvertEnabled()) {
                    try {
                        List<String> unoCommand =
                                buildUnoConvertCommand(
                                        tempInputFile,
                                        unoOutputFile,
                                        outputFormat,
                                        libreOfficeFilter);
                        returnCode =
                                ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
                                        .runCommandWithOutputHandling(unoCommand);
                    } catch (IOException e) {
                        unoconvertException = e;
                        log.warn(
                                "Unoconvert command failed ({}). Falling back to soffice command.",
                                e.getMessage());
                    }
                }

                if (returnCode == null) {
                    // Run the LibreOffice command as a fallback
                    libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
                    List<String> command = new ArrayList<>();
                    command.add(runtimePathConfig.getSOfficePath());
                    command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
                    command.add("--headless");
                    command.add("--nologo");
                    command.add("--infilter=" + libreOfficeFilter);
                    command.add("--convert-to");
                    command.add(outputFormat);
                    command.add("--outdir");
                    command.add(tempOutputDir.toString());
                    command.add(tempInputFile.toString());

                    try {
                        returnCode =
                                ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
                                        .runCommandWithOutputHandling(command);
                    } catch (IOException e) {
                        if (unoconvertException != null) {
                            e.addSuppressed(unoconvertException);
                        }
                        throw e;
                    }
                }

                // Get output files
                List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());

                if (outputFiles.size() == 1) {
                    // Return single output file
                    File outputFile = outputFiles.get(0);
                    if ("txt:Text".equals(outputFormat)) {
                        outputFormat = "txt";
                    }
                    fileName = pdfBaseName + "." + outputFormat;
                    FileUtils.copyFile(outputFile, finalOut.getFile());
                } else {
                    // Return output files in a ZIP archive
                    fileName = pdfBaseName + "To" + outputFormat + ".zip";
                    try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
                            ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
                        for (File outputFile : outputFiles) {
                            ZipEntry entry = new ZipEntry(outputFile.getName());
                            zipOutputStream.putNextEntry(entry);
                            try (FileInputStream fis = new FileInputStream(outputFile)) {
                                IOUtils.copy(fis, zipOutputStream);
                            } catch (IOException e) {
                                log.error("Exception writing zip entry", e);
                            }

                            zipOutputStream.closeEntry();
                        }
                    } catch (IOException e) {
                        log.error("Exception writing zip", e);
                    }
                }
            }
        } catch (Exception e) {
            finalOut.close();
            throw e;
        } finally {
            if (libreOfficeProfile != null) {
                FileUtils.deleteQuietly(libreOfficeProfile.toFile());
            }
        }
        return WebResponseUtils.fileToWebResponse(
                finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
    }

    private boolean isUnoConvertEnabled() {
        return runtimePathConfig != null
                && runtimePathConfig.getUnoConvertPath() != null
                && !runtimePathConfig.getUnoConvertPath().isBlank();
    }

    private List<String> buildUnoConvertCommand(
            Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
        List<String> command = new ArrayList<>();
        command.add(runtimePathConfig.getUnoConvertPath());
        command.add("--convert-to");
        command.add(outputFormat);
        if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
            command.add("--input-filter=" + libreOfficeFilter);
        }
        command.add(inputFile.toString());
        command.add(outputFile.toString());
        return command;
    }

    private String resolvePrimaryExtension(String outputFormat) {
        if (outputFormat == null) {
            return "";
        }
        int colonIndex = outputFormat.indexOf(':');
        return colonIndex > 0 ? outputFormat.substring(0, colonIndex) : outputFormat;
    }
}
