import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Slf4j
public final class ZipUtils {

    static final int MAX_FILES_NUM = 1024;      // Max number of files
    private static final long THRESHOLD_SIZE = 100L * 1024L * 1024L; // 100MB
    private static final double THRESHOLD_RATIO = 10.0;

    private ZipUtils() {
    }

    public static void unzip(Path inputZipFile, Path outputDirectory) throws IOException {

        File destDir = new File(outputDirectory.toString());

        try {
            // 先試試 utf8
            unzip(inputZipFile, destDir, StandardCharsets.UTF_8);
        } catch (RuntimeException e) {
            log.error("UTF_8解壓縮失敗");
            // 再試試看 big5
            unzip(inputZipFile, destDir, Charset.forName("Big5"));
        }
    }

    private static void unzip(Path inputZipFile, File destDir, Charset charset) throws IOException {
        log.info("開始使用 Charset: {} 進行解壓縮", charset);

        long totalUncompressedSize = 0;
        int totalEntries = 0;

        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(inputZipFile.toFile()), charset)) {
            ZipEntry zipEntry;
            while ((zipEntry = zis.getNextEntry()) != null) {
                byte[] buffer = new byte[1024];

                File newFile = newFile(destDir, zipEntry);

                long uncompressedEntrySize = 0;

                if (zipEntry.isDirectory()) {
                    if (!newFile.isDirectory() && !newFile.mkdirs()) {
                        throw new IOException("Failed to create directory " + newFile);
                    }
                } else {
                    File parent = newFile.getParentFile();
                    if (!parent.isDirectory() && !parent.mkdirs()) {
                        throw new IOException("Failed to create directory " + parent);
                    }

                    try (FileOutputStream fos = new FileOutputStream(newFile);
                         BufferedOutputStream bos = new BufferedOutputStream(fos)) {
                        int len;
                        while ((len = zis.read(buffer)) > 0) {
                            bos.write(buffer, 0, len);
                            uncompressedEntrySize += len;
                            totalUncompressedSize += len;

                            checkSizeAndRatio(totalUncompressedSize, uncompressedEntrySize, zipEntry);
                        }
                    }
                }

                totalEntries++;
                checkNumberOfFiles(totalEntries);
            }
        }
    }

    private static File newFile(File destinationDir, ZipEntry zipEntry) throws IOException {
        File destFile = new File(destinationDir, zipEntry.getName());

        String destDirPath = destinationDir.getCanonicalPath();
        String destFilePath = destFile.getCanonicalPath();

        if (!destFilePath.startsWith(destDirPath + File.separator)) {
            throw new IOException("Entry is outside of the target dir: " + zipEntry.getName());
        }

        return destFile;
    }

    private static void checkSizeAndRatio(long totalUncompressedSize, long uncompressedEntrySize, ZipEntry zipEntry) {
        if (totalUncompressedSize > THRESHOLD_SIZE) {
            throw new IllegalStateException("Uncompressed size exceeds threshold.");
        }

        double compressionRatio = uncompressedEntrySize / (double) zipEntry.getCompressedSize();
        if (compressionRatio > THRESHOLD_RATIO) {
            throw new IllegalStateException("Compression ratio exceeds threshold, possible zip bomb.");
        }
    }

    private static void checkNumberOfFiles(int totalEntries) {
        if (totalEntries > MAX_FILES_NUM) {
            throw new IllegalStateException("Too many files to unzip.");
        }
    }
}