package org.springblade.file.util;

import cn.hutool.core.io.CharsetDetector;
import com.github.junrar.Archive;
import com.github.junrar.rarfile.FileHeader;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry;
import org.apache.commons.compress.archivers.sevenz.SevenZFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.io.IOUtils;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.tool.utils.StringPool;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipInputStream;

/**
 *  解压工具类
 * @author bfhuange
 * @since 2025/2/28
 */
@Slf4j
public class UnzipUtil {
    /**
     *  rar格式
     */
    public static final String RAR = "rar";

    /**
     *  macos下垃圾目录，不处理
     */
    private static final String MACOSX_DIR = "__MACOSX";

    /**
     *  解压文件
     * @param zipFilePath 压缩文件路径
     * @param unzipDir 解压目录
     */
    public static List<File> unzipOrUnrar(String zipFilePath, File unzipDir) {
        if (zipFilePath.toLowerCase().endsWith(RAR)) {
            // 解压rar
            return unrar(zipFilePath, unzipDir);
        } else if (zipFilePath.toLowerCase().endsWith(ArchiveStreamFactory.SEVEN_Z)) {
            // 解压7z
            return un7z(zipFilePath, unzipDir);
        }
        // 解压其余格式
        return unzip(zipFilePath, unzipDir);
    }

    /**
     *  解压zip格式
     * @param downloadFile
     * @param destDir
     */
    public static List<File> un7z(String downloadFile, File destDir) {
        List<File> files = new ArrayList<>();
        try(
                SevenZFile sevenZFile = new SevenZFile.Builder()
                        .setFile(new File(downloadFile))
                        .get()
        ) {
            SevenZArchiveEntry entry = sevenZFile.getNextEntry();
            while (entry != null) {
                log.debug("压缩文件条目：{}", entry.getName());
                File file = new File(destDir, entry.getName());
                if (!entry.isDirectory()) {
                    // 不是目录才处理
                    files.add(file);
                    File parentFile = file.getParentFile();
                    if (!parentFile.exists()) {
                        parentFile.mkdirs();
                    }
                    try(
                            OutputStream os = new FileOutputStream(file);
                            InputStream is = sevenZFile.getInputStream(entry);
                    ) {
                        IOUtils.copy(is, os);
                    }
                }
                entry = sevenZFile.getNextEntry();
            }
        } catch (IOException e) {
            log.error("解压文件异常", e);
            throw new RuntimeException(e);
        }
        return files;
    }

    /**
     *  解压rar格式
     * @param zipFilePath 压缩文件路径
     * @param unzipDir 解压文件目录
     */
    public static List<File> unrar(String zipFilePath, File unzipDir) {
        List<File> files = new ArrayList<>();
        try (
                // 加载rar文件
                InputStream inputStream = Files.newInputStream(Paths.get(zipFilePath));
                Archive archive = new Archive(inputStream);
        ) {
            List<FileHeader> fileHeaders = archive.getFileHeaders();
            if (fileHeaders == null) {
                throw new ServiceException("压缩文件为空");
            }
            int total = fileHeaders.size();
            for (int i = 0; i < fileHeaders.size(); i++) {
                FileHeader fileHeader = fileHeaders.get(i);
                log.debug("解压进度：{}/{} 文件：{}", i + 1, total, fileHeader.getFileName());
                // 解压文件
                File file = new File(unzipDir, fileHeader.getFileName());
                if (!fileHeader.isDirectory()) {
                    // 不是目录才处理
                    files.add(file);
                    File parentFile = file.getParentFile();
                    if (!parentFile.exists()) {
                        parentFile.mkdirs();
                    }
                    InputStream is = archive.getInputStream(fileHeader);
                    try (OutputStream os = new FileOutputStream(file)) {
                        IOUtils.copy(is, os);
                    }
                }
            }
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            log.error("解压文件异常", e);
            throw new ServiceException("解压文件异常");
        }
        return files;
    }

    /**
     *  解压zip格式
     * @param zipFilePath 压缩文件路径
     * @param unzipDir 解压目录
     */
    public static List<File> unzip(String zipFilePath, File unzipDir) {
        List<File> files = new ArrayList<>();
        ArchiveStreamFactory archiveStreamFactory = new ArchiveStreamFactory();
        try(
                FileInputStream is = new FileInputStream(zipFilePath);
                BufferedInputStream bis = new BufferedInputStream(is);
                ArchiveInputStream<? extends ArchiveEntry> ais = archiveStreamFactory.createArchiveInputStream(bis);
        ) {
            ArchiveEntry entry;
            while ((entry = ais.getNextEntry()) != null) {
                // 获取文件名称
                String entryName = UnzipUtil.getEntryName(entry);
                log.debug("压缩文件条目：{}", entryName);
                if (entryName.contains(MACOSX_DIR)) {
                    // macos下垃圾目录，跳过
                    log.info("跳过macos下__MACOSX的文件：{}", entryName);
                    continue;
                }
                // 压缩文件内文件夹分隔符是\，linux下不会识别为文件夹，会识别成文件名，处理下
                if (entryName.contains(StringPool.BACK_SLASH)) {
                    entryName = entryName.replace(StringPool.BACK_SLASH, StringPool.SLASH);
                }
                if (!isDirectory(entryName)) {
                    File file = new File(unzipDir, entryName);
                    if (file.getName().startsWith(StringPool.DOT)) {
                        // .开头的文件，是隐藏文件，跳过
                        log.info("跳过.开头的文件：{}", entryName);
                        continue;
                    }
                    // 不是目录才处理
                    files.add(file);
                    File parentFile = file.getParentFile();
                    if (!parentFile.exists()) {
                        parentFile.mkdirs();
                    }
                    try(OutputStream os = new FileOutputStream(file)) {
                        IOUtils.copy(ais, os);
                    }
                }
            }
        } catch (IOException e) {
            log.error("解压文件异常", e);
            throw new RuntimeException(e);
        } catch (ArchiveException e) {
            throw new RuntimeException(e);
        }
        return files;
    }

    /**
     *  获取文件名称
     * @param entry
     * @return
     */
    private static String getEntryName(ArchiveEntry entry) {
        if (entry instanceof ZipArchiveEntry zipArchiveEntry) {
            byte[] rawName = zipArchiveEntry.getRawName();
            // 检测文件名编码
            Charset charset = CharsetDetector.detect(new ByteArrayInputStream(rawName));
            return new String(rawName, charset);
        }
        return entry.getName();
    }

    /**
     *  判断压缩文件是否utf8编码
     * @param zipFilePath 压缩文件路径
     * @return
     */
    public static boolean isUtf8(String zipFilePath) {
        try (
                FileInputStream fileInputStream = new FileInputStream(zipFilePath);
                ZipInputStream zipInputStream = new ZipInputStream(fileInputStream, StandardCharsets.UTF_8);
        ) {
            while (zipInputStream.getNextEntry() != null) {

            }
            return true;
        } catch (Exception ignore) {
            // gbk编码的用utf8编码读取会抛异常
            return false;
        }
    }

    /**
     *  判断压缩文件中条目是否文件夹
     * @param entryName
     * @return
     */
    private static boolean isDirectory(String entryName) {
        if (entryName == null) {
            return false;
        }
        return entryName.endsWith(StringPool.SLASH) || entryName.endsWith(StringPool.BACK_SLASH);
    }

}
