package com.ruoyi.info.collection.utils;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.common.annotation.DictDropdown;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.info.collection.handler.DictDropdownWriteHandler;
import com.ruoyi.info.collection.handler.RequiredFieldWriteHandler;
import com.ruoyi.info.collection.handler.TextFormatWriteHandler;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
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.ss.util.CellRangeAddress;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * EasyExcel工具类
 *
 * @author ruoyi
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class EasyExcelUtil {

    /**
     * 导出Excel
     *
     * @param response     响应对象
     * @param list         数据列表
     * @param clazz        实体类
     * @param sheetName    工作表名称
     * @param <T>          泛型
     */
    public static <T> void exportExcel(HttpServletResponse response, List<T> list, Class<T> clazz, String sheetName) {
        try {
            setResponseHeader(response, sheetName);
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .doWrite(list);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel失败", e);
        }
    }

    /**
     * 导出Excel（指定数据）
     *
     * @param response     响应对象
     * @param list         数据列表
     * @param clazz        实体类
     * @param sheetName    工作表名称
     * @param fileName     文件名称
     * @param <T>          泛型
     */
    public static <T> void exportExcel(HttpServletResponse response, List<T> list, Class<T> clazz, String sheetName, String fileName) {
        try {
            setResponseHeader(response, fileName);
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .doWrite(list);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel失败", e);
        }
    }

    /**
     * 导入Excel
     *
     * @param fileName 文件名
     * @param clazz    实体类
     * @param <T>      泛型
     * @return 数据列表
     */
    public static <T> List<T> importExcel(String fileName, Class<T> clazz) {
        try (InputStream inputStream = java.nio.file.Files.newInputStream(java.nio.file.Path.of(fileName))) {
            return importExcel(inputStream, clazz);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("导入Excel失败", e);
        }
    }

    /**
     * 导入Excel（通过输入流）
     *
     * @param inputStream 输入流
     * @param clazz       实体类
     * @param <T>         泛型
     * @return 数据列表
     */
    public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz) {
        try {
            byte[] bytes = inputStream.readAllBytes();
            validateDictDropdownTemplate(bytes, clazz, null);
            return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet().doReadSync();
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("导入Excel失败", e);
        }
    }

    /**
     * 导入Excel（按指定Sheet名称）
     *
     * @param inputStream 输入流
     * @param clazz 实体类
     * @param sheetName 工作表名称
     * @param <T> 泛型
     * @return 数据列表
     */
    public static <T> List<T> importExcel(java.io.InputStream inputStream, Class<T> clazz, String sheetName) {
        try {
            byte[] bytes = inputStream.readAllBytes();
            validateDictDropdownTemplate(bytes, clazz, sheetName);
            return EasyExcel.read(new ByteArrayInputStream(bytes)).head(clazz).sheet(sheetName).doReadSync();
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("导入Excel失败", e);
        }
    }

    /**
     * 下载导入模板
     *
     * @param response  响应对象
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param <T>       泛型
     */
    public static <T> void importTemplateExcel(HttpServletResponse response, Class<T> clazz, String sheetName) {
        try {
            setResponseHeader(response, sheetName + "模板");
            templateWriter(response, clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new TextFormatWriteHandler(clazz))
                    .doWrite(List.of());
        } catch (IOException e) {
            throw new RuntimeException("下载导入模板失败", e);
        }
    }

    /**
     * 下载导入模板（带自定义处理器）
     *
     * @param response  响应对象
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param handlers  自定义写入处理器
     * @param <T>       泛型
     */
    @SafeVarargs
    public static <T> void importTemplateExcel(HttpServletResponse response, Class<T> clazz, String sheetName,
                                                  WriteHandler... handlers) {
        try {
            setResponseHeader(response, sheetName + "模板");
            var writerBuilder = templateWriter(response, clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new TextFormatWriteHandler(clazz));
            // 注册所有自定义处理器
            for (WriteHandler handler : handlers) {
                writerBuilder.registerWriteHandler(handler);
            }
            writerBuilder.doWrite(List.of());
        } catch (IOException e) {
            throw new RuntimeException("下载导入模板失败", e);
        }
    }

    /**
     * 设置响应头
     *
     * @param response 响应对象
     * @param fileName 文件名
     */
    private static void setResponseHeader(HttpServletResponse response, String fileName) {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
    }

    /**
     * 下载带字典下拉框的导入模板
     * 自动解析实体类中的@DictDropdown注解，为对应字段添加下拉框
     * 自动解析实体类中的@Required注解，为必填字段表头添加红色星号(*)标记
     *
     * @param response  响应对象
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param <T>       泛型
     */
    public static <T> void importTemplateWithDictDropdown(HttpServletResponse response, Class<T> clazz, String sheetName) {
        try {
            setResponseHeader(response, sheetName + "模板");
            templateWriter(response, clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new DictDropdownWriteHandler(clazz))
                    .registerWriteHandler(new TextFormatWriteHandler(clazz))
                    .registerWriteHandler(new RequiredFieldWriteHandler(clazz))
                    .doWrite(List.of());
        } catch (IOException e) {
            throw new RuntimeException("下载带字典下拉框的导入模板失败", e);
        }
    }

    /**
     * 下载带字典下拉框的导入模板（指定文件名）
     * 自动解析实体类中的@DictDropdown注解，为对应字段添加下拉框
     * 自动解析实体类中的@Required注解，为必填字段表头添加红色星号(*)标记
     *
     * @param response  响应对象
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param fileName  文件名称
     * @param <T>       泛型
     */
    public static <T> void importTemplateWithDictDropdown(HttpServletResponse response, Class<T> clazz,
                                                           String sheetName, String fileName) {
        try {
            setResponseHeader(response, fileName);
            templateWriter(response, clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new DictDropdownWriteHandler(clazz))
                    .registerWriteHandler(new TextFormatWriteHandler(clazz))
                    .registerWriteHandler(new RequiredFieldWriteHandler(clazz))
                    .doWrite(List.of());
        } catch (IOException e) {
            throw new RuntimeException("下载带字典下拉框的导入模板失败", e);
        }
    }

    /**
     * 下载双Sheet导入模板（带字典下拉框）
     *
     * @param response 响应对象
     * @param firstClazz 第一张Sheet实体类
     * @param firstSheetName 第一张Sheet名称
     * @param secondClazz 第二张Sheet实体类
     * @param secondSheetName 第二张Sheet名称
     * @param fileName 文件名称
     * @param <T1> 第一张Sheet泛型
     * @param <T2> 第二张Sheet泛型
     */
    public static <T1, T2> void importTemplateWithDictDropdown(
        HttpServletResponse response,
        Class<T1> firstClazz,
        String firstSheetName,
        Class<T2> secondClazz,
        String secondSheetName,
        String fileName
    ) {
        setResponseHeader(response, fileName);
        try (ExcelWriter writer = templateWriter(response).build()) {
            writer.write(List.of(), buildTemplateSheet(0, firstClazz, firstSheetName));
            writer.write(List.of(), buildTemplateSheet(1, secondClazz, secondSheetName));
        } catch (IOException e) {
            throw new RuntimeException("下载双Sheet导入模板失败", e);
        }
    }

    private static <T> WriteSheet buildTemplateSheet(int sheetNo, Class<T> clazz, String sheetName) {
        return EasyExcel.writerSheet(sheetNo, sheetName)
            .head(clazz)
            .registerWriteHandler(new DictDropdownWriteHandler(clazz))
            .registerWriteHandler(new TextFormatWriteHandler(clazz))
            .registerWriteHandler(new RequiredFieldWriteHandler(clazz))
            .build();
    }

    /**
     * 导出Excel（带字典下拉框）
     * 导出的数据包含实际值，但模板中有下拉框供后续编辑使用
     * 自动解析实体类中的@Required注解，为必填字段表头添加红色星号(*)标记
     *
     * @param response  响应对象
     * @param list      数据列表
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param <T>       泛型
     */
    public static <T> void exportExcelWithDictDropdown(HttpServletResponse response, List<T> list,
                                                        Class<T> clazz, String sheetName) {
        try {
            setResponseHeader(response, sheetName);
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new DictDropdownWriteHandler(clazz))
                    .registerWriteHandler(new TextFormatWriteHandler(clazz))
                    .registerWriteHandler(new RequiredFieldWriteHandler(clazz))
                    .doWrite(list);
        } catch (IOException e) {
            throw new RuntimeException("导出带字典下拉框的Excel失败", e);
        }
    }

    /**
     * 导出Excel（带字典下拉框，指定文件名）
     * 导出的数据包含实际值，但模板中有下拉框供后续编辑使用
     * 自动解析实体类中的@Required注解，为必填字段表头添加红色星号(*)标记
     *
     * @param response  响应对象
     * @param list      数据列表
     * @param clazz     实体类
     * @param sheetName 工作表名称
     * @param fileName  文件名称
     * @param <T>       泛型
     */
    public static <T> void exportExcelWithDictDropdown(HttpServletResponse response, List<T> list,
                                                        Class<T> clazz, String sheetName, String fileName) {
        try {
            setResponseHeader(response, fileName);
            EasyExcel.write(response.getOutputStream(), clazz)
                    .sheet(sheetName)
                    .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
                    .registerWriteHandler(new DictDropdownWriteHandler(clazz))
                    .registerWriteHandler(new TextFormatWriteHandler(clazz))
                    .registerWriteHandler(new RequiredFieldWriteHandler(clazz))
                    .doWrite(list);
        } catch (IOException e) {
            throw new RuntimeException("导出带字典下拉框的Excel失败", e);
        }
    }

    private static void validateDictDropdownTemplate(byte[] bytes, Class<?> clazz, String sheetName) {
        List<DropdownColumn> dropdownColumns = resolveDropdownColumns(clazz);
        if (dropdownColumns.isEmpty()) {
            return;
        }

        try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) {
            Sheet sheet = sheetName == null ? workbook.getSheetAt(0) : workbook.getSheet(sheetName);
            if (sheet == null) {
                return;
            }

            int lastDataRowIndex = findLastDataRowIndex(sheet);
            if (lastDataRowIndex < 1) {
                return;
            }

            List<String> missingColumnTitles = new ArrayList<>();
            for (DropdownColumn column : dropdownColumns) {
                if (!isListValidationCovered(sheet, column.index(), lastDataRowIndex)) {
                    missingColumnTitles.add(column.title());
                }
            }
            if (!missingColumnTitles.isEmpty()) {
                throw new ServiceException(sheet.getSheetName() + " Sheet 的 "
                        + String.join("、", missingColumnTitles)
                        + " 列缺少下拉框，请下载最新导入模板填写后重新导入");
            }
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("导入Excel失败", e);
        }
    }

    private static List<DropdownColumn> resolveDropdownColumns(Class<?> clazz) {
        List<DropdownColumn> columns = new ArrayList<>();
        Class<?> current = clazz;
        while (current != null && current != Object.class) {
            for (Field field : current.getDeclaredFields()) {
                if (field.getAnnotation(DictDropdown.class) == null) {
                    continue;
                }

                ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
                if (excelProperty == null || excelProperty.index() < 0) {
                    continue;
                }

                columns.add(new DropdownColumn(excelProperty.index(), resolveColumnTitle(field, excelProperty)));
            }
            current = current.getSuperclass();
        }
        columns.sort(Comparator.comparingInt(DropdownColumn::index));
        return columns;
    }

    private static String resolveColumnTitle(Field field, ExcelProperty excelProperty) {
        if (excelProperty.value().length > 0 && excelProperty.value()[0] != null
                && !excelProperty.value()[0].isBlank()) {
            return excelProperty.value()[0].replace("*", "");
        }
        return field.getName();
    }

    private static int findLastDataRowIndex(Sheet sheet) {
        int lastDataRowIndex = -1;
        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
            Row row = sheet.getRow(rowIndex);
            if (hasData(row)) {
                lastDataRowIndex = rowIndex;
            }
        }
        return lastDataRowIndex;
    }

    private static boolean hasData(Row row) {
        if (row == null || row.getLastCellNum() < 0) {
            return false;
        }
        for (int cellIndex = row.getFirstCellNum(); cellIndex < row.getLastCellNum(); cellIndex++) {
            if (cellIndex < 0) {
                continue;
            }
            Cell cell = row.getCell(cellIndex);
            if (cell != null && cell.toString() != null && !cell.toString().isBlank()) {
                return true;
            }
        }
        return false;
    }

    private static boolean isListValidationCovered(Sheet sheet, int columnIndex, int lastDataRowIndex) {
        boolean[] coveredRows = new boolean[lastDataRowIndex + 1];
        for (DataValidation validation : sheet.getDataValidations()) {
            DataValidationConstraint constraint = validation.getValidationConstraint();
            if (constraint == null || constraint.getValidationType() != DataValidationConstraint.ValidationType.LIST) {
                continue;
            }

            for (CellRangeAddress address : validation.getRegions().getCellRangeAddresses()) {
                if (address.getFirstColumn() > columnIndex || address.getLastColumn() < columnIndex) {
                    continue;
                }

                int firstRow = Math.max(1, address.getFirstRow());
                int lastRow = Math.min(lastDataRowIndex, address.getLastRow());
                for (int rowIndex = firstRow; rowIndex <= lastRow; rowIndex++) {
                    coveredRows[rowIndex] = true;
                }
            }
        }

        for (int rowIndex = 1; rowIndex <= lastDataRowIndex; rowIndex++) {
            if (hasData(sheet.getRow(rowIndex)) && !coveredRows[rowIndex]) {
                return false;
            }
        }
        return true;
    }

    private record DropdownColumn(int index, String title) {}

    private static <T> ExcelWriterBuilder templateWriter(HttpServletResponse response, Class<T> clazz)
            throws IOException {
        // 模板为空且体量小，使用内存工作簿避免 SXSSF 在无字体环境初始化 Fontconfig。
        return EasyExcel.write(response.getOutputStream(), clazz).inMemory(Boolean.TRUE);
    }

    private static ExcelWriterBuilder templateWriter(HttpServletResponse response) throws IOException {
        return EasyExcel.write(response.getOutputStream()).inMemory(Boolean.TRUE);
    }
}
