package com.nanri.aiimage.modules.split.service;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
import com.nanri.aiimage.modules.split.model.dto.SplitRunRequest;
import com.nanri.aiimage.modules.split.model.dto.SplitSourceFileDto;
import com.nanri.aiimage.modules.split.model.vo.SplitArchiveEntryVo;
import com.nanri.aiimage.modules.split.model.vo.SplitResultItemVo;
import com.nanri.aiimage.modules.split.model.vo.SplitRunVo;
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
@RequiredArgsConstructor
public class SplitRunService {

    private static final String MODULE_TYPE = "SPLIT";
    private static final String ZIP_CONTENT_TYPE = "application/zip";
    private static final String SPLIT_MODE_ROWS_PER_FILE = "rows_per_file";
    private static final String SPLIT_MODE_PARTS = "parts";
    private static final String HEADER_SUFFIX_MARKER = "idASIN\u56fd\u5bb6\u72b6\u6001\u4ef7\u683c\u53d8\u4f53\u6570\u91cf";
    private static final String HEADER_STOP_COLUMN = "\u7f29\u7565\u56fe\u5730\u57408";
    private static final DateTimeFormatter ZIP_NAME_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");

    private final FileTaskMapper fileTaskMapper;
    private final FileResultMapper fileResultMapper;
    private final StorageProperties storageProperties;
    private final OssStorageService ossStorageService;

    public SplitRunVo run(SplitRunRequest request) {
        validateRequest(request);

        FileTaskEntity task = new FileTaskEntity();
        task.setTaskNo(MODULE_TYPE + "-" + IdUtil.getSnowflakeNextIdStr());
        task.setModuleType(MODULE_TYPE);
        task.setTaskMode("IMMEDIATE");
        task.setStatus("SUCCESS");
        task.setSourceFileCount(request.getFiles().size());
        task.setCreatedBy("user:" + request.getUserId());
        task.setUserId(request.getUserId());
        task.setRequestJson(JSONUtil.toJsonStr(request));
        task.setCreatedAt(LocalDateTime.now());
        task.setUpdatedAt(LocalDateTime.now());
        task.setFinishedAt(LocalDateTime.now());
        fileTaskMapper.insert(task);

        boolean folderMode = request.getArchiveName() != null && !request.getArchiveName().isBlank();
        List<SplitResultItemVo> items = new ArrayList<>();
        int successCount = 0;
        int failedCount = 0;
        List<TaskSplitChunkResult> allChunkResults = new ArrayList<>();
        List<String> successSourceNames = new ArrayList<>();

        for (SplitSourceFileDto sourceFile : request.getFiles()) {
            try {
                File inputFile = findLocalSourceFile(sourceFile.getFileKey());
                if (inputFile == null || !inputFile.exists()) {
                    throw new BusinessException("Uploaded source file does not exist, please upload again.");
                }

                String inputName = sourceFile.getOriginalFilename() == null
                        ? inputFile.getName()
                        : sourceFile.getOriginalFilename();
                List<SplitChunkResult> chunkResults = splitExcelByLegacyRules(inputFile, inputName, request);
                for (SplitChunkResult chunkResult : chunkResults) {
                    allChunkResults.add(new TaskSplitChunkResult(sourceFile.getRelativePath(), inputName, chunkResult.outputFile(), chunkResult.rowCount()));
                }
                successSourceNames.add(inputName);
                successCount++;
            } catch (Exception ex) {
                SplitResultItemVo item = new SplitResultItemVo();
                item.setSourceFilename(sourceFile.getOriginalFilename());
                item.setSuccess(false);
                item.setError(ex.getMessage());
                item.setEntries(new ArrayList<>());
                item.setEntryCount(0);
                items.add(item);
                failedCount++;

                FileResultEntity resultEntity = new FileResultEntity();
                resultEntity.setTaskId(task.getId());
                resultEntity.setModuleType(MODULE_TYPE);
                resultEntity.setSourceFilename(sourceFile.getOriginalFilename());
                resultEntity.setSuccess(0);
                resultEntity.setErrorMessage(ex.getMessage());
                resultEntity.setUserId(request.getUserId());
                resultEntity.setCreatedAt(LocalDateTime.now());
                fileResultMapper.insert(resultEntity);
            }
        }

        if (!allChunkResults.isEmpty()) {
            File zipFile = packageTaskSplitResultsAsZip(request.getArchiveName(), successSourceNames, allChunkResults);
            String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
            // 只存 objectKey
            SplitResultItemVo item = new SplitResultItemVo();
            item.setSourceFilename(folderMode && request.getArchiveName() != null && !request.getArchiveName().isBlank()
                    ? request.getArchiveName()
                    : successSourceNames.size() == 1 ? successSourceNames.get(0) : "Current split task");
            item.setOutputFilename(zipFile.getName());
            item.setSuccess(true);
            item.setRowCount(allChunkResults.stream().mapToInt(TaskSplitChunkResult::rowCount).sum());
            item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));
            item.setEntryCount(allChunkResults.size());
            item.setEntries(allChunkResults.stream().map(this::toArchiveEntry).toList());

            FileResultEntity resultEntity = new FileResultEntity();
            resultEntity.setTaskId(task.getId());
            resultEntity.setModuleType(MODULE_TYPE);
            resultEntity.setSourceFilename(item.getSourceFilename());
            resultEntity.setResultFilename(zipFile.getName());
            resultEntity.setResultFileUrl(ossObjectKey);  // 存 objectKey
            resultEntity.setResultFileSize(zipFile.length());
            resultEntity.setResultContentType(ZIP_CONTENT_TYPE);
            resultEntity.setRowCount(item.getRowCount());
            resultEntity.setSuccess(1);
            resultEntity.setUserId(request.getUserId());
            resultEntity.setCreatedAt(LocalDateTime.now());
            fileResultMapper.insert(resultEntity);
            item.setResultId(resultEntity.getId());
            items.add(0, item);
        }

        task.setSuccessFileCount(successCount);
        task.setFailedFileCount(failedCount);
        task.setResultJson(JSONUtil.toJsonStr(items));
        task.setUpdatedAt(LocalDateTime.now());
        fileTaskMapper.updateById(task);

        SplitRunVo vo = new SplitRunVo();
        vo.setTotal(request.getFiles().size());
        vo.setSuccessCount(successCount);
        vo.setFailedCount(failedCount);
        vo.setItems(items);
        return vo;
    }

    public List<SplitResultItemVo> listHistory(Long userId) {
        return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                        .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                        .eq(FileResultEntity::getUserId, userId)
                        .orderByDesc(FileResultEntity::getCreatedAt)
                        .last("limit 100"))
                .stream()
                .map(entity -> {
                    SplitResultItemVo vo = new SplitResultItemVo();
                    vo.setResultId(entity.getId());
                    vo.setSourceFilename(entity.getSourceFilename());
                    vo.setOutputFilename(entity.getResultFilename());
                    vo.setDownloadUrl(entity.getSuccess() != null && entity.getSuccess() == 1
                            ? ossStorageService.generateFreshDownloadUrl(entity.getResultFileUrl())
                            : null);
                    vo.setRowCount(entity.getRowCount());
                    vo.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
                    vo.setError(entity.getErrorMessage());
                    vo.setEntryCount(0);
                    vo.setEntries(new ArrayList<>());
                    return vo;
                })
                .toList();
    }

    public void deleteHistory(Long resultId, Long userId) {
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("Split result record does not exist.");
        }
        fileResultMapper.deleteById(resultId);
    }

    public Resource getResultFile(Long resultId, Long userId) {
        FileResultEntity entity = fileResultMapper.selectById(resultId);
        if (entity == null || entity.getResultFileUrl() == null || !MODULE_TYPE.equals(entity.getModuleType()) || !userId.equals(entity.getUserId())) {
            throw new BusinessException("Split result file does not exist.");
        }

        File file = new File(entity.getResultFileUrl());
        if (!file.exists()) {
            throw new BusinessException("Split result file does not exist.");
        }
        return new FileSystemResource(file);
    }

    private List<SplitChunkResult> splitExcelByLegacyRules(
            File inputFile,
            String originalFilename,
            SplitRunRequest request
    ) throws Exception {
        DataFormatter formatter = new DataFormatter();
        try (FileInputStream fis = new FileInputStream(inputFile);
             Workbook workbook = WorkbookFactory.create(fis)) {
            Sheet sheet = workbook.getSheetAt(0);
            Row headerRow = sheet.getRow(0);
            if (headerRow == null) {
                throw new BusinessException("Excel header row is empty.");
            }

            Map<String, Integer> headerMap = buildHeaderMap(headerRow, formatter);
            List<String> missing = request.getSelectedColumns().stream()
                    .filter(column -> !headerMap.containsKey(column))
                    .toList();
            if (!missing.isEmpty()) {
                throw new BusinessException("Missing required columns: " + String.join(", ", missing));
            }

            List<List<String>> rowsData = new ArrayList<>();
            for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    continue;
                }
                List<String> projected = new ArrayList<>();
                for (String column : request.getSelectedColumns()) {
                    Integer sourceIndex = headerMap.get(column);
                    String value = sourceIndex == null
                            ? ""
                            : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
                    projected.add(value);
                }
                rowsData.add(projected);
            }

            if (rowsData.isEmpty()) {
                throw new BusinessException("No data rows available for splitting.");
            }

            List<List<List<String>>> chunks = buildChunks(rowsData, request);
            File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result"));
            String sourceStem = FileUtil.mainName(originalFilename);
            List<SplitChunkResult> results = new ArrayList<>();

            for (int i = 0; i < chunks.size(); i++) {
                File outputFile = buildNamedOutputFile(outputDir, sourceStem + "_split_" + (i + 1) + ".xlsx");
                try (XSSFWorkbook outputWorkbook = new XSSFWorkbook()) {
                    Sheet outputSheet = outputWorkbook.createSheet(sheet.getSheetName());
                    Row outputHeaderRow = outputSheet.createRow(0);
                    for (int colIndex = 0; colIndex < request.getSelectedColumns().size(); colIndex++) {
                        outputHeaderRow.createCell(colIndex).setCellValue(request.getSelectedColumns().get(colIndex));
                    }
                    int outputRowIndex = 1;
                    for (List<String> rowValues : chunks.get(i)) {
                        Row outputRow = outputSheet.createRow(outputRowIndex++);
                        for (int colIndex = 0; colIndex < rowValues.size(); colIndex++) {
                            outputRow.createCell(colIndex).setCellValue(rowValues.get(colIndex));
                        }
                    }
                    try (FileOutputStream fos = new FileOutputStream(outputFile)) {
                        outputWorkbook.write(fos);
                    }
                }
                results.add(new SplitChunkResult(outputFile, chunks.get(i).size()));
            }

            return results;
        }
    }

    private Map<String, Integer> buildHeaderMap(Row headerRow, DataFormatter formatter) {
        Map<String, Integer> headerMap = new HashMap<>();
        for (int i = 0; i < headerRow.getLastCellNum(); i++) {
            Cell cell = headerRow.getCell(i);
            String value = normalizeHeaderName(cell == null ? null : formatter.formatCellValue(cell));
            if (value.isBlank() || headerMap.containsKey(value)) {
                continue;
            }
            headerMap.put(value, i);
            if (HEADER_STOP_COLUMN.equals(value)) {
                break;
            }
        }
        return headerMap;
    }

    private List<List<List<String>>> buildChunks(List<List<String>> rowsData, SplitRunRequest request) {
        int totalRows = rowsData.size();
        List<List<List<String>>> chunks = new ArrayList<>();

        if (SPLIT_MODE_ROWS_PER_FILE.equals(request.getSplitMode())) {
            int chunkSize = request.getRowsPerFile();
            for (int i = 0; i < totalRows; i += chunkSize) {
                chunks.add(rowsData.subList(i, Math.min(i + chunkSize, totalRows)));
            }
            return chunks;
        }

        int actualParts = Math.min(request.getParts(), totalRows);
        int base = totalRows / actualParts;
        int remainder = totalRows % actualParts;
        int start = 0;
        for (int i = 0; i < actualParts; i++) {
            int size = base + (i < remainder ? 1 : 0);
            int end = start + size;
            chunks.add(rowsData.subList(start, end));
            start = end;
        }
        return chunks;
    }

    private void validateRequest(SplitRunRequest request) {
        if (!List.of(SPLIT_MODE_ROWS_PER_FILE, SPLIT_MODE_PARTS).contains(request.getSplitMode())) {
            throw new BusinessException("Invalid split mode.");
        }
        if (SPLIT_MODE_ROWS_PER_FILE.equals(request.getSplitMode())
                && (request.getRowsPerFile() == null || request.getRowsPerFile() <= 0)) {
            throw new BusinessException("Invalid rowsPerFile value.");
        }
        if (SPLIT_MODE_PARTS.equals(request.getSplitMode())
                && (request.getParts() == null || request.getParts() <= 0)) {
            throw new BusinessException("Invalid parts value.");
        }
    }

    private File findLocalSourceFile(String fileKey) {
        File baseDir = FileUtil.file(storageProperties.getLocalTempDir());
        if (!baseDir.exists()) {
            return null;
        }
        List<File> matchedFiles = FileUtil.loopFiles(
                baseDir,
                pathname -> pathname.isFile() && pathname.getName().startsWith(fileKey)
        );
        return matchedFiles.isEmpty() ? null : matchedFiles.get(0);
    }

    private File buildNamedOutputFile(File outputDir, String filename) {
        File candidate = FileUtil.file(outputDir, filename);
        if (!candidate.exists()) {
            return candidate;
        }

        String mainName = FileUtil.mainName(filename);
        String extName = FileUtil.extName(filename);
        int index = 2;
        while (true) {
            String nextFilename = extName == null || extName.isBlank()
                    ? mainName + "_" + index
                    : mainName + "_" + index + "." + extName;
            File nextCandidate = FileUtil.file(outputDir, nextFilename);
            if (!nextCandidate.exists()) {
                return nextCandidate;
            }
            index++;
        }
    }

    private File packageTaskSplitResultsAsZip(String archiveName, List<String> successSourceNames, List<TaskSplitChunkResult> chunkResults) {
        File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "split-result"));
        String zipFilename = archiveName != null && !archiveName.isBlank()
                ? archiveName + ".zip"
                : successSourceNames.size() == 1
                ? FileUtil.mainName(successSourceNames.getFirst()) + ".zip"
                : LocalDateTime.now().format(ZIP_NAME_DATE_FORMATTER) + ".zip";
        File zipFile = buildNamedOutputFile(outputDir, zipFilename);
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
            for (TaskSplitChunkResult chunkResult : chunkResults) {
                String zipEntryName = buildFolderZipEntry(chunkResult.relativePath(), chunkResult.sourceFilename(), chunkResult.outputFile().getName(), true);
                ZipEntry entry = new ZipEntry(zipEntryName);
                zos.putNextEntry(entry);
                Files.copy(chunkResult.outputFile().toPath(), zos);
                zos.closeEntry();
            }
        } catch (Exception ex) {
            throw new BusinessException("拆分结果打包失败：" + ex.getMessage());
        }
        return zipFile;
    }

    private String buildFolderZipEntry(String relativePath, String inputName, String outputFilename, boolean includeSourceStemFolder) {
        String normalizedRelativePath = relativePath == null ? "" : relativePath.replace('\\', '/');
        String relativeDir = normalizedRelativePath.contains("/")
                ? normalizedRelativePath.substring(0, normalizedRelativePath.lastIndexOf('/'))
                : "";
        List<String> parts = new ArrayList<>();
        if (!relativeDir.isBlank()) {
            parts.add(relativeDir);
        }
        if (includeSourceStemFolder) {
            parts.add(FileUtil.mainName(inputName));
        }
        parts.add(outputFilename);
        return String.join("/", parts);
    }

    private SplitArchiveEntryVo toArchiveEntry(TaskSplitChunkResult chunkResult) {
        SplitArchiveEntryVo vo = new SplitArchiveEntryVo();
        vo.setFilename(chunkResult.outputFile().getName());
        vo.setRowCount(chunkResult.rowCount());
        return vo;
    }

    private String normalizeHeaderName(String value) {
        String normalized = normalizeCellText(value);
        if (normalized.isBlank()) {
            return normalized;
        }

        String prefix = normalized.split(HEADER_SUFFIX_MARKER, 2)[0].trim();
        return prefix.isEmpty() ? normalized : prefix;
    }

    private String normalizeCellText(String value) {
        if (value == null) {
            return "";
        }
        return value.replace("\ufeff", "")
                .replace("\u3000", " ")
                .replace("\r\n", " ")
                .replace("\r", " ")
                .replace("\n", " ")
                .replace("\t", " ")
                .trim()
                .replaceAll("\\s+", " ");
    }

    private record SplitChunkResult(File outputFile, int rowCount) {
    }

    private record TaskSplitChunkResult(String relativePath, String sourceFilename, File outputFile, int rowCount) {
    }
}
