package com.nanri.aiimage.modules.convert.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.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.common.exception.BusinessException;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.convert.model.dto.ConvertRunRequest;
import com.nanri.aiimage.modules.convert.model.dto.UploadedSourceFileDto;
import com.nanri.aiimage.modules.convert.model.entity.ConvertTemplateEntity;
import com.nanri.aiimage.modules.convert.model.vo.ConvertResultItemVo;
import com.nanri.aiimage.modules.convert.model.vo.ConvertRunVo;
import com.nanri.aiimage.modules.file.service.oss.OssStorageService;
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.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.springframework.stereotype.Service;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Service
@RequiredArgsConstructor
public class ConvertRunService {

    private static final String MODULE_TYPE = "CONVERT";
    private static final String TEMPLATE_CODE_FIVE_COUNTRIES = "uk_offer";
    private static final List<String> FIVE_COUNTRY_OUTPUT_FILES = List.of(
            "\u82f1\u56fd.txt",
            "\u6cd5\u56fd.txt",
            "\u5fb7\u56fd.txt",
            "\u897f\u73ed\u7259.txt",
            "\u610f\u5927\u5229.txt"
    );

    private final FileTaskMapper fileTaskMapper;
    private final FileResultMapper fileResultMapper;
    private final StorageProperties storageProperties;
    private final ConvertTemplateService convertTemplateService;
    private final ObjectMapper objectMapper;
    private final OssStorageService ossStorageService;

    public ConvertRunVo run(ConvertRunRequest request) {
        ConvertTemplateEntity template = convertTemplateService.getByCode(request.getTemplateId());

        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<ConvertResultItemVo> items = new ArrayList<>();
        int successCount = 0;
        int failedCount = 0;
        List<ConvertArchiveEntry> archiveEntries = new ArrayList<>();

        for (UploadedSourceFileDto 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<GeneratedConvertFile> generatedFiles = generateOutputFiles(inputFile, template);
                if (folderMode) {
                    for (GeneratedConvertFile generatedFile : generatedFiles) {
                        archiveEntries.add(new ConvertArchiveEntry(sourceFile.getRelativePath(), inputName, generatedFile));
                    }
                    successCount++;
                    continue;
                }

                boolean useZipPackage = request.getFiles().size() == 1 && generatedFiles.size() > 1;
                File packagedResultFile = useZipPackage
                        ? packageGeneratedFilesAsZip(inputName, generatedFiles)
                        : generatedFiles.getFirst().file();
                String packagedFilename = useZipPackage
                        ? FileUtil.mainName(inputName) + ".zip"
                        : generatedFiles.getFirst().filename();
                String contentType = useZipPackage ? "application/zip" : "text/plain";

                String ossObjectKey = ossStorageService.uploadResultFile(packagedResultFile, MODULE_TYPE);
                // 只存 objectKey
                ConvertResultItemVo item = new ConvertResultItemVo();
                item.setSourceFilename(inputName);
                item.setSuccess(true);
                item.setOutputFilename(packagedFilename);
                item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));

                FileResultEntity resultEntity = new FileResultEntity();
                resultEntity.setTaskId(task.getId());
                resultEntity.setModuleType(MODULE_TYPE);
                resultEntity.setSourceFilename(inputName);
                resultEntity.setResultFilename(packagedFilename);
                resultEntity.setResultFileUrl(ossObjectKey);  // 存 objectKey
                resultEntity.setResultFileSize(packagedResultFile.length());
                resultEntity.setResultContentType(contentType);
                resultEntity.setSuccess(1);
                resultEntity.setUserId(request.getUserId());
                resultEntity.setCreatedAt(LocalDateTime.now());
                fileResultMapper.insert(resultEntity);
                item.setResultId(resultEntity.getId());

                items.add(item);
                successCount++;
            } catch (Exception ex) {
                ConvertResultItemVo item = new ConvertResultItemVo();
                item.setSourceFilename(sourceFile.getOriginalFilename());
                item.setSuccess(false);
                item.setError(ex.getMessage());
                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 (folderMode && !archiveEntries.isEmpty()) {
            File zipFile = packageFolderConvertResultsAsZip(request.getArchiveName(), archiveEntries);
            String ossObjectKey = ossStorageService.uploadResultFile(zipFile, MODULE_TYPE);
            // 只存 objectKey
            ConvertResultItemVo item = new ConvertResultItemVo();
            item.setSourceFilename(request.getArchiveName());
            item.setOutputFilename(zipFile.getName());
            item.setSuccess(true);
            item.setDownloadUrl(ossStorageService.generateFreshDownloadUrl(ossObjectKey));

            FileResultEntity resultEntity = new FileResultEntity();
            resultEntity.setTaskId(task.getId());
            resultEntity.setModuleType(MODULE_TYPE);
            resultEntity.setSourceFilename(request.getArchiveName());
            resultEntity.setResultFilename(zipFile.getName());
            resultEntity.setResultFileUrl(ossObjectKey);  // 存 objectKey
            resultEntity.setResultFileSize(zipFile.length());
            resultEntity.setResultContentType("application/zip");
            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);

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

    public List<ConvertResultItemVo> listHistory(Long userId) {
        return fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
                        .eq(FileResultEntity::getModuleType, MODULE_TYPE)
                        .eq(FileResultEntity::getUserId, userId)
                        .orderByDesc(FileResultEntity::getCreatedAt)
                        .last("limit 200"))
                .stream()
                .map(entity -> {
                    ConvertResultItemVo vo = new ConvertResultItemVo();
                    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.setSuccess(entity.getSuccess() != null && entity.getSuccess() == 1);
                    vo.setError(entity.getErrorMessage());
                    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("Convert result record does not exist.");
        }
        fileResultMapper.deleteById(resultId);
    }

    public File 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("Convert result file does not exist.");
        }

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

    private List<GeneratedConvertFile> generateOutputFiles(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
        List<String> outputFilenames = resolveOutputFilenames(templateEntity);
        List<String> rows = buildTxtRows(inputFile, templateEntity);
        File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));

        List<GeneratedConvertFile> generatedFiles = new ArrayList<>();
        for (String outputFilename : outputFilenames) {
            File outputFile = buildNamedOutputFile(outputDir, outputFilename);
            FileUtil.writeLines(rows, outputFile, StandardCharsets.UTF_8);
            generatedFiles.add(new GeneratedConvertFile(outputFilename, outputFile));
        }
        return generatedFiles;
    }

    private List<String> resolveOutputFilenames(ConvertTemplateEntity templateEntity) {
        if (TEMPLATE_CODE_FIVE_COUNTRIES.equals(templateEntity.getTemplateCode())) {
            return FIVE_COUNTRY_OUTPUT_FILES;
        }

        String outputFilename = templateEntity.getOutputFilename();
        if (outputFilename == null || outputFilename.isBlank()) {
            return List.of("result.txt");
        }
        return List.of(outputFilename);
    }

    private List<String> buildTxtRows(File inputFile, ConvertTemplateEntity templateEntity) throws IOException {
        List<String> requiredColumns = readStringList(templateEntity.getRequiredSourceColumnsJson());
        List<String> headerColumns = readStringList(templateEntity.getHeaderColumnsJson());
        Map<String, String> fieldMapping = readStringMap(templateEntity.getFieldMappingJson());
        Map<String, String> defaults = readStringMap(templateEntity.getDefaultsJson());
        List<String> preambleLines = readStringList(templateEntity.getPreambleLinesJson());
        int blankHeaderRowsAfterSchema = templateEntity.getBlankHeaderRowsAfterSchema() == null
                ? 0
                : templateEntity.getBlankHeaderRowsAfterSchema();

        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 = new LinkedHashMap<>();
            for (int i = 0; i < headerRow.getLastCellNum(); i++) {
                String value = normalizeCellText(formatter.formatCellValue(headerRow.getCell(i)));
                if (!value.isBlank() && !headerMap.containsKey(value)) {
                    headerMap.put(value, i);
                }
            }

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

            List<String> rows = new ArrayList<>();
            rows.addAll(preambleLines);
            rows.add(String.join("\t", headerColumns));
            for (int i = 0; i < blankHeaderRowsAfterSchema; i++) {
                rows.add("\t".repeat(Math.max(0, headerColumns.size() - 1)));
            }

            long batchId = System.currentTimeMillis();
            int rowIndex = 1;
            int asinIndex = headerMap.getOrDefault("ASIN", -1);
            for (int rowNum = 1; rowNum <= sheet.getLastRowNum(); rowNum++) {
                Row row = sheet.getRow(rowNum);
                if (row == null) {
                    continue;
                }

                String asin = asinIndex >= 0 ? normalizeCellText(formatter.formatCellValue(row.getCell(asinIndex))) : "";
                if (asin.isBlank()) {
                    continue;
                }

                List<String> lineValues = new ArrayList<>();
                for (String column : headerColumns) {
                    if ("sku".equals(column)) {
                        lineValues.add(batchId + "-" + rowIndex);
                        continue;
                    }
                    if (fieldMapping.containsKey(column)) {
                        String sourceColumn = fieldMapping.get(column);
                        Integer sourceIndex = headerMap.get(sourceColumn);
                        String value = sourceIndex == null
                                ? ""
                                : normalizeCellText(formatter.formatCellValue(row.getCell(sourceIndex)));
                        lineValues.add(value);
                        continue;
                    }
                    if (defaults.containsKey(column)) {
                        lineValues.add(defaults.get(column));
                        continue;
                    }
                    lineValues.add("");
                }

                rows.add(String.join("\t", lineValues));
                rowIndex++;
            }
            return rows;
        }
    }

    private List<String> readStringList(String json) {
        try {
            if (json == null || json.isBlank()) {
                return new ArrayList<>();
            }
            return objectMapper.readValue(json, new TypeReference<List<String>>() {
            });
        } catch (Exception ex) {
            throw new BusinessException("Template config parsing failed.");
        }
    }

    private File packageGeneratedFilesAsZip(String inputName, List<GeneratedConvertFile> generatedFiles) {
        File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
        File zipFile = buildNamedOutputFile(outputDir, FileUtil.mainName(inputName) + ".zip");
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
            for (GeneratedConvertFile generatedFile : generatedFiles) {
                ZipEntry entry = new ZipEntry(generatedFile.filename());
                zos.putNextEntry(entry);
                Files.copy(generatedFile.file().toPath(), zos);
                zos.closeEntry();
            }
        } catch (Exception ex) {
            throw new BusinessException("Convert result packaging failed: " + ex.getMessage());
        }
        return zipFile;
    }

    private File packageFolderConvertResultsAsZip(String archiveName, List<ConvertArchiveEntry> archiveEntries) {
        File outputDir = FileUtil.mkdir(FileUtil.file(storageProperties.getLocalTempDir(), "convert-result"));
        File zipFile = buildNamedOutputFile(outputDir, archiveName + ".zip");
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) {
            for (ConvertArchiveEntry entry : archiveEntries) {
                String zipEntryName = buildFolderZipEntry(entry.relativePath(), entry.inputName(), entry.generatedFile().filename(), true);
                zos.putNextEntry(new ZipEntry(zipEntryName));
                Files.copy(entry.generatedFile().file().toPath(), zos);
                zos.closeEntry();
            }
        } catch (Exception ex) {
            throw new BusinessException("Convert folder result packaging failed: " + 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 Map<String, String> readStringMap(String json) {
        try {
            if (json == null || json.isBlank()) {
                return new HashMap<>();
            }
            return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {
            });
        } catch (Exception ex) {
            throw new BusinessException("Template config parsing failed.");
        }
    }

    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 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 GeneratedConvertFile(String filename, File file) {
    }

    private record ConvertArchiveEntry(String relativePath, String inputName, GeneratedConvertFile generatedFile) {
    }
}
