package com.njcn.gather.aireport.testreportmulti.component;

import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.aireport.testreport.component.TestReportLedgerExcelParser;
import com.njcn.gather.aireport.testreport.pojo.constant.TestReportConst;
import com.njcn.gather.aireport.testreportmulti.pojo.vo.TestReportMultiTemplateDirVO;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

@Component
@RequiredArgsConstructor
public class TestReportMultiZipParser {

    private static final Set<String> SUPPORTED_EXTENSIONS = new LinkedHashSet<String>();

    static {
        SUPPORTED_EXTENSIONS.add("xls");
        SUPPORTED_EXTENSIONS.add("xlsx");
        SUPPORTED_EXTENSIONS.add("doc");
        SUPPORTED_EXTENSIONS.add("docx");
    }

    private final TestReportLedgerExcelParser testReportLedgerExcelParser;

    public ParsedArchive parse(MultipartFile zipFile) {
        if (zipFile == null || zipFile.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包不能为空");
        }
        String zipFileName = requireZipFileName(zipFile.getOriginalFilename());
        Map<String, byte[]> rawEntries = readZipEntries(zipFile);
        Map<String, byte[]> normalizedEntries = stripSingleWrapperDir(rawEntries);
        String summaryPath = resolveSummaryPath(normalizedEntries.keySet());
        if (summaryPath == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包根目录缺少台账信息汇总.xls或台账信息汇总.xlsx");
        }

        byte[] summaryBytes = normalizedEntries.get(summaryPath);
        TestReportLedgerExcelParser.ParsedLedger ledger = testReportLedgerExcelParser.parseForValidate(
                resolveFileName(summaryPath), new ByteArrayInputStream(summaryBytes));

        ParsedArchive archive = new ParsedArchive();
        archive.setZipFileName(zipFileName);
        archive.setSummaryFileName(resolveFileName(summaryPath));
        archive.setSummaryRelativePath(summaryPath);
        archive.setSummaryFileBytes(summaryBytes);
        archive.setLedger(ledger);
        archive.setFileContentMap(normalizedEntries);
        archive.setPointCount(ledger.getPoints().size());
        archive.setExcelAttachmentCount(ledger.getPoints().size());

        Map<String, TemplateDirSummary> dirSummaryMap = new LinkedHashMap<String, TemplateDirSummary>();
        Set<String> referencedPaths = new HashSet<String>();
        int wordAttachmentCount = 0;

        for (TestReportLedgerExcelParser.ParsedPointRow pointRow : ledger.getPoints()) {
            String excelRelativePath = normalizeRelativePath(pointRow.getExcelAttachmentName());
            String templateDirPath = resolveTopLevelDir(excelRelativePath);
            if (templateDirPath == null) {
                archive.getFailReasons().add("第" + pointRow.getRowNo() + "行Excel附件名称必须包含一级模板目录");
                continue;
            }
            if (!normalizedEntries.containsKey(excelRelativePath)) {
                archive.getFailReasons().add("第" + pointRow.getRowNo() + "行缺少Excel附件：" + excelRelativePath);
            }
            referencedPaths.add(excelRelativePath);

            String wordRelativePath = null;
            if (StrUtil.isNotBlank(pointRow.getWordAttachmentName())) {
                wordAttachmentCount++;
                wordRelativePath = normalizeRelativePath(pointRow.getWordAttachmentName());
                if (!normalizedEntries.containsKey(wordRelativePath)) {
                    archive.getFailReasons().add("第" + pointRow.getRowNo() + "行缺少Word附件：" + wordRelativePath);
                }
                if (!templateDirPath.equals(resolveTopLevelDir(wordRelativePath))) {
                    archive.getFailReasons().add("第" + pointRow.getRowNo() + "行Word附件与Excel附件不在同一一级目录");
                }
                referencedPaths.add(wordRelativePath);
            }

            TemplateDirSummary dirSummary = dirSummaryMap.computeIfAbsent(templateDirPath, this::newTemplateDirSummary);
            dirSummary.setPointCount(dirSummary.getPointCount() + 1);
            dirSummary.setExcelAttachmentCount(dirSummary.getExcelAttachmentCount() + 1);
            if (StrUtil.isNotBlank(wordRelativePath)) {
                dirSummary.setWordAttachmentCount(dirSummary.getWordAttachmentCount() + 1);
            }
            if (pointRow.getGroupNo() != null) {
                dirSummary.getGroupNos().add(pointRow.getGroupNo());
            }

            ParsedPointBinding pointBinding = new ParsedPointBinding();
            pointBinding.setRowNo(pointRow.getRowNo());
            pointBinding.setGroupNo(pointRow.getGroupNo());
            pointBinding.setSubstationName(pointRow.getSubstationName());
            pointBinding.setPointName(pointRow.getPointName());
            pointBinding.setTemplateDirName(dirSummary.getDirName());
            pointBinding.setTemplateDirPath(dirSummary.getDirPath());
            pointBinding.setExcelAttachmentName(resolveFileName(excelRelativePath));
            pointBinding.setExcelAttachmentRelativePath(excelRelativePath);
            pointBinding.setWordAttachmentName(StrUtil.isBlank(wordRelativePath) ? null : resolveFileName(wordRelativePath));
            pointBinding.setWordAttachmentRelativePath(wordRelativePath);
            archive.getPoints().add(pointBinding);
        }

        archive.setWordAttachmentCount(wordAttachmentCount);
        for (TemplateDirSummary dirSummary : dirSummaryMap.values()) {
            TestReportMultiTemplateDirVO dirVO = new TestReportMultiTemplateDirVO();
            dirVO.setDirName(dirSummary.getDirName());
            dirVO.setDirPath(dirSummary.getDirPath());
            dirVO.setPointCount(dirSummary.getPointCount());
            dirVO.setGroupCount(dirSummary.getGroupNos().size());
            dirVO.setExcelAttachmentCount(dirSummary.getExcelAttachmentCount());
            dirVO.setWordAttachmentCount(dirSummary.getWordAttachmentCount());
            archive.getTemplateDirs().add(dirVO);
        }

        for (String relativePath : normalizedEntries.keySet()) {
            if (summaryPath.equals(relativePath)) {
                continue;
            }
            if (!referencedPaths.contains(relativePath)) {
                archive.getOrphanAttachmentPaths().add(relativePath);
            }
        }
        if (!archive.getOrphanAttachmentPaths().isEmpty()) {
            archive.getFailReasons().add("压缩包存在未被台账引用的附件");
        }
        archive.setDirCount(archive.getTemplateDirs().size());
        archive.setChildReportCount(archive.getTemplateDirs().size());
        archive.setSuccess(archive.getFailReasons().isEmpty());
        return archive;
    }

    private Map<String, byte[]> readZipEntries(MultipartFile zipFile) {
        Map<String, byte[]> result = new LinkedHashMap<String, byte[]>();
        Set<String> duplicateCheck = new HashSet<String>();
        try (ZipInputStream zipInputStream = new ZipInputStream(zipFile.getInputStream())) {
            ZipEntry entry;
            while ((entry = zipInputStream.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    continue;
                }
                String normalizedPath = normalizeRelativePath(entry.getName());
                if (isIgnoredEntry(normalizedPath)) {
                    continue;
                }
                validateEntryExtension(normalizedPath);
                String duplicateKey = normalizedPath.toLowerCase(Locale.ENGLISH);
                if (!duplicateCheck.add(duplicateKey)) {
                    throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中存在重复文件路径：" + normalizedPath);
                }
                result.put(normalizedPath, readEntryBytes(zipInputStream));
            }
        } catch (BusinessException exception) {
            throw exception;
        } catch (IOException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "读取压缩包失败");
        }
        if (result.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中没有可用文件");
        }
        return result;
    }

    private Map<String, byte[]> stripSingleWrapperDir(Map<String, byte[]> rawEntries) {
        if (rawEntries.isEmpty()) {
            return Collections.emptyMap();
        }
        String wrapper = null;
        for (String path : rawEntries.keySet()) {
            int slashIndex = path.indexOf('/');
            if (slashIndex <= 0) {
                return rawEntries;
            }
            String currentWrapper = path.substring(0, slashIndex);
            if (wrapper == null) {
                wrapper = currentWrapper;
                continue;
            }
            if (!wrapper.equals(currentWrapper)) {
                return rawEntries;
            }
        }
        Map<String, byte[]> result = new LinkedHashMap<String, byte[]>();
        for (Map.Entry<String, byte[]> entry : rawEntries.entrySet()) {
            String strippedPath = entry.getKey().substring(wrapper.length() + 1);
            if (StrUtil.isBlank(strippedPath)) {
                continue;
            }
            result.put(strippedPath, entry.getValue());
        }
        return result.isEmpty() ? rawEntries : result;
    }

    private String resolveSummaryPath(Set<String> relativePaths) {
        String matchedPath = null;
        for (String relativePath : relativePaths) {
            if (!TestReportConst.isLedgerSummaryFileName(resolveFileName(relativePath))) {
                continue;
            }
            if (relativePath.contains("/")) {
                throw new BusinessException(CommonResponseEnum.FAIL, "台账信息汇总文件必须位于压缩包根目录");
            }
            if (matchedPath != null) {
                throw new BusinessException(CommonResponseEnum.FAIL, "压缩包根目录仅允许存在一个台账信息汇总文件");
            }
            matchedPath = relativePath;
        }
        return matchedPath;
    }

    private TemplateDirSummary newTemplateDirSummary(String dirPath) {
        TemplateDirSummary summary = new TemplateDirSummary();
        summary.setDirPath(dirPath);
        summary.setDirName(resolveFileName(dirPath));
        return summary;
    }

    private void validateEntryExtension(String path) {
        String extension = resolveExtension(path);
        if (!SUPPORTED_EXTENSIONS.contains(extension)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包中存在不支持的文件类型：" + path);
        }
    }

    private boolean isIgnoredEntry(String path) {
        String lowerPath = path.toLowerCase(Locale.ENGLISH);
        return lowerPath.startsWith("__macosx/") || lowerPath.endsWith("/.ds_store") || lowerPath.equals(".ds_store");
    }

    private byte[] readEntryBytes(ZipInputStream zipInputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[8192];
        int length;
        while ((length = zipInputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, length);
        }
        return outputStream.toByteArray();
    }

    private String requireZipFileName(String fileName) {
        String normalized = StrUtil.trimToNull(fileName);
        if (normalized == null || !normalized.toLowerCase(Locale.ENGLISH).endsWith(".zip")) {
            throw new BusinessException(CommonResponseEnum.FAIL, "仅支持上传zip压缩包");
        }
        return normalized;
    }

    private String resolveTopLevelDir(String relativePath) {
        int slashIndex = relativePath.indexOf('/');
        return slashIndex <= 0 ? null : relativePath.substring(0, slashIndex);
    }

    private String resolveFileName(String relativePath) {
        int slashIndex = relativePath.lastIndexOf('/');
        return slashIndex < 0 ? relativePath : relativePath.substring(slashIndex + 1);
    }

    private String resolveExtension(String filePath) {
        int dotIndex = filePath.lastIndexOf('.');
        return dotIndex < 0 || dotIndex == filePath.length() - 1 ? "" : filePath.substring(dotIndex + 1).toLowerCase(Locale.ENGLISH);
    }

    private String normalizeRelativePath(String relativePath) {
        String normalized = StrUtil.trimToEmpty(relativePath).replace("\\", "/");
        while (normalized.startsWith("/")) {
            normalized = normalized.substring(1);
        }
        while (normalized.endsWith("/")) {
            normalized = normalized.substring(0, normalized.length() - 1);
        }
        if (StrUtil.isBlank(normalized)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "压缩包内文件路径不能为空");
        }
        for (String segment : normalized.split("/")) {
            if (StrUtil.isBlank(segment) || ".".equals(segment) || "..".equals(segment)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "压缩包内文件路径不合法：" + relativePath);
            }
        }
        return normalized;
    }

    @Data
    public static class ParsedArchive {
        private Boolean success = Boolean.FALSE;
        private String zipFileName;
        private String summaryFileName;
        private String summaryRelativePath;
        private byte[] summaryFileBytes;
        private Integer dirCount = 0;
        private Integer childReportCount = 0;
        private Integer pointCount = 0;
        private Integer excelAttachmentCount = 0;
        private Integer wordAttachmentCount = 0;
        private List<String> orphanAttachmentPaths = new ArrayList<String>();
        private List<String> failReasons = new ArrayList<String>();
        private List<TestReportMultiTemplateDirVO> templateDirs = new ArrayList<TestReportMultiTemplateDirVO>();
        private List<ParsedPointBinding> points = new ArrayList<ParsedPointBinding>();
        private Map<String, byte[]> fileContentMap = new LinkedHashMap<String, byte[]>();
        private TestReportLedgerExcelParser.ParsedLedger ledger;
    }

    @Data
    public static class ParsedPointBinding {
        private Integer rowNo;
        private Integer groupNo;
        private String substationName;
        private String pointName;
        private String templateDirName;
        private String templateDirPath;
        private String excelAttachmentName;
        private String excelAttachmentRelativePath;
        private String wordAttachmentName;
        private String wordAttachmentRelativePath;
    }

    @Data
    private static class TemplateDirSummary {
        private String dirName;
        private String dirPath;
        private int pointCount;
        private int excelAttachmentCount;
        private int wordAttachmentCount;
        private Set<Integer> groupNos = new LinkedHashSet<Integer>();
    }
}
