package stirling.software.common.util;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import io.github.pixee.security.ZipSecurity;

import stirling.software.common.model.api.converters.HTMLToPdfRequest;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;

public class FileToPdf {

    public static byte[] convertHtmlToPdf(
            String weasyprintPath,
            HTMLToPdfRequest request,
            byte[] fileBytes,
            String fileName,
            TempFileManager tempFileManager,
            CustomHtmlSanitizer customHtmlSanitizer)
            throws IOException, InterruptedException {

        try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
            try (TempFile tempInputFile =
                    new TempFile(
                            tempFileManager,
                            fileName.toLowerCase(Locale.ROOT).endsWith(".html")
                                    ? ".html"
                                    : ".zip")) {

                if (fileName.toLowerCase(Locale.ROOT).endsWith(".html")) {
                    String sanitizedHtml =
                            sanitizeHtmlContent(
                                    new String(fileBytes, StandardCharsets.UTF_8),
                                    customHtmlSanitizer);
                    Files.writeString(tempInputFile.getPath(), sanitizedHtml);
                } else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
                    Files.write(tempInputFile.getPath(), fileBytes);
                    sanitizeHtmlFilesInZip(
                            tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
                } else {
                    throw ExceptionUtils.createHtmlFileRequiredException();
                }

                List<String> command = new ArrayList<>();
                command.add(weasyprintPath);
                command.add("-e");
                command.add("utf-8");
                command.add("-v");
                command.add("--pdf-forms");
                command.add(tempInputFile.getAbsolutePath());
                command.add(tempOutputFile.getAbsolutePath());

                ProcessExecutorResult returnCode =
                        ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
                                .runCommandWithOutputHandling(command);

                return Files.readAllBytes(tempOutputFile.getPath());
            } // tempInputFile auto-closed
        } // tempOutputFile auto-closed
    }

    private static String sanitizeHtmlContent(
            String htmlContent, CustomHtmlSanitizer customHtmlSanitizer) {
        return customHtmlSanitizer.sanitize(htmlContent);
    }

    private static void sanitizeHtmlFilesInZip(
            Path zipFilePath,
            TempFileManager tempFileManager,
            CustomHtmlSanitizer customHtmlSanitizer)
            throws IOException {
        try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
            try (ZipInputStream zipIn =
                    ZipSecurity.createHardenedInputStream(Files.newInputStream(zipFilePath))) {
                ZipEntry entry = zipIn.getNextEntry();
                while (entry != null) {
                    Path filePath =
                            tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
                    Path normalizedTargetDir =
                            tempUnzippedDir.getPath().toAbsolutePath().normalize();
                    Path normalizedFilePath = filePath.toAbsolutePath().normalize();
                    if (!normalizedFilePath.startsWith(normalizedTargetDir)) {
                        throw new IOException(
                                "Zip entry path escapes target directory: " + entry.getName());
                    }
                    if (!entry.isDirectory()) {
                        Files.createDirectories(filePath.getParent());
                        if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
                                || entry.getName().toLowerCase(Locale.ROOT).endsWith(".htm")) {
                            String content =
                                    new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
                            String sanitizedContent =
                                    sanitizeHtmlContent(content, customHtmlSanitizer);
                            Files.writeString(filePath, sanitizedContent);
                        } else {
                            Files.copy(zipIn, filePath);
                        }
                    }
                    zipIn.closeEntry();
                    entry = zipIn.getNextEntry();
                }
            }

            // Repack the sanitized files
            zipDirectory(tempUnzippedDir.getPath(), zipFilePath);
        } // tempUnzippedDir auto-cleaned
    }

    private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
        try (ZipOutputStream zos =
                new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
            try (Stream<Path> walk = Files.walk(sourceDir)) {
                walk.filter(path -> !Files.isDirectory(path))
                        .forEach(
                                path -> {
                                    ZipEntry zipEntry =
                                            new ZipEntry(sourceDir.relativize(path).toString());
                                    try {
                                        zos.putNextEntry(zipEntry);
                                        Files.copy(path, zos);
                                        zos.closeEntry();
                                    } catch (IOException e) {
                                        throw new UncheckedIOException(e);
                                    }
                                });
            }
        }
    }

    static String sanitizeZipFilename(String entryName) {
        if (entryName == null || entryName.trim().isEmpty()) {
            return "";
        }
        // Remove any drive letters (e.g., "C:\") and leading forward/backslashes
        entryName =
                RegexPatternUtils.getInstance()
                        .getDriveLetterPattern()
                        .matcher(entryName)
                        .replaceAll("");
        entryName =
                RegexPatternUtils.getInstance()
                        .getLeadingSlashesPattern()
                        .matcher(entryName)
                        .replaceAll("");

        // Recursively remove path traversal sequences
        while (entryName.contains("../") || entryName.contains("..\\")) {
            entryName = entryName.replace("../", "").replace("..\\", "");
        }
        // Normalize all backslashes to forward slashes
        entryName =
                RegexPatternUtils.getInstance()
                        .getBackslashPattern()
                        .matcher(entryName)
                        .replaceAll("/");
        return entryName;
    }
}
