/**
 * BladeX Commercial License Agreement
 * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
 * <p>
 * Use of this software is governed by the Commercial License Agreement
 * obtained after purchasing a license from BladeX.
 * <p>
 * 1. This software is for development use only under a valid license
 * from BladeX.
 * <p>
 * 2. Redistribution of this software's source code to any third party
 * without a commercial license is strictly prohibited.
 * <p>
 * 3. Licensees may copyright their own code but cannot use segments
 * from this software for such purposes. Copyright of this software
 * remains with BladeX.
 * <p>
 * Using this software signifies agreement to this License, and the software
 * must not be used for illegal purposes.
 * <p>
 * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
 * not liable for any claims arising from secondary or illegal development.
 * <p>
 * Author: Chill Zhuang (bladejava@qq.com)
 */
package org.springblade.common.excel;

import cn.idev.excel.FastExcel;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.write.handler.CellWriteHandler;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.metadata.holder.WriteTableHolder;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Workbook;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 导入失败明细 Excel 导出工具类
 *
 * @author Chill
 */
public class ImportFailureExcelUtil {

	private static final String FAILURE_REASON = "导入失败原因";
	private static final String ERROR_MESSAGE_FIELD = "errorMessage";
	private static final String FAILURE_REASON_FIELD = "failureReason";

	private ImportFailureExcelUtil() {
	}

	/**
	 * 导出导入失败明细
	 *
	 * @param response 响应
	 * @param fileName 文件名
	 * @param sheetName 工作表名
	 * @param data 失败数据
	 * @param excelClass 原导入 Excel 类型
	 */
	public static void export(HttpServletResponse response, String fileName, String sheetName, List<?> data, Class<?> excelClass) {
		response.setContentType("application/vnd.ms-excel");
		response.setCharacterEncoding(StandardCharsets.UTF_8.name());
		String encodeFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8);
		response.setHeader("Content-disposition", "attachment;filename=" + encodeFileName + ".xlsx");
		List<Field> excelFields = excelFields(excelClass);
		List<List<String>> head = buildHead(excelFields);
		List<List<Object>> rows = buildRows(data, excelFields);
		try {
			FastExcel.write(response.getOutputStream())
				.registerWriteHandler(new ImportFailureCellStyleHandler(excelFields, rows))
				.head(head)
				.sheet(sheetName)
				.doWrite(rows);
		} catch (IOException exception) {
			throw new IllegalStateException("导出导入失败明细失败", exception);
		}
	}

	private static List<Field> excelFields(Class<?> excelClass) {
		return Arrays.stream(excelClass.getDeclaredFields())
			.filter(field -> field.getAnnotation(ExcelProperty.class) != null)
			.filter(field -> field.getAnnotation(ExcelIgnore.class) == null)
			.filter(field -> !Objects.equals(field.getName(), ERROR_MESSAGE_FIELD))
			.filter(field -> !Objects.equals(field.getName(), FAILURE_REASON_FIELD))
			.toList();
	}

	private static List<List<String>> buildHead(List<Field> excelFields) {
		List<List<String>> head = new ArrayList<>();
		for (Field field : excelFields) {
			ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
			String[] value = excelProperty.value();
			head.add(List.of(value.length == 0 ? field.getName() : value[0]));
		}
		head.add(List.of(FAILURE_REASON));
		return head;
	}

	private static List<List<Object>> buildRows(List<?> data, List<Field> excelFields) {
		List<List<Object>> rows = new ArrayList<>();
		for (Object item : data) {
			List<Object> row = new ArrayList<>();
			for (Field field : excelFields) {
				row.add(fieldValue(item, field));
			}
			row.add(errorMessage(item));
			rows.add(row);
		}
		return rows;
	}

	private static Object fieldValue(Object item, Field field) {
		try {
			Field targetField = targetField(item.getClass(), field.getName());
			targetField.setAccessible(true);
			return targetField.get(item);
		} catch (NoSuchFieldException | IllegalAccessException exception) {
			return null;
		}
	}

	private static Object errorMessage(Object item) {
		try {
			Field field = targetField(item.getClass(), ERROR_MESSAGE_FIELD, FAILURE_REASON_FIELD);
			field.setAccessible(true);
			Object value = field.get(item);
			return value == null ? "" : String.valueOf(value);
		} catch (NoSuchFieldException | IllegalAccessException exception) {
			return "";
		}
	}

	private static Field targetField(Class<?> itemClass, String... fieldNames) throws NoSuchFieldException {
		Class<?> currentClass = itemClass;
		while (currentClass != null) {
			for (String fieldName : fieldNames) {
				try {
					return currentClass.getDeclaredField(fieldName);
				} catch (NoSuchFieldException ignored) {
					// 继续查找其他字段名或父类字段。
				}
			}
			currentClass = currentClass.getSuperclass();
		}
		throw new NoSuchFieldException(String.join(",", fieldNames));
	}

	private static String columnName(Field field) {
		ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
		String[] value = excelProperty.value();
		return value.length == 0 ? field.getName() : value[0];
	}

	private static String normalize(String value) {
		return value == null ? "" : value.replaceAll("[\\s*_：:，,。；;（）()\\[\\]【】<>《》-]", "").toLowerCase();
	}

	private static List<String> columnKeywords(Field field) {
		String columnName = columnName(field);
		List<String> keywords = new ArrayList<>();
		keywords.add(columnName);
		keywords.add(field.getName());
		keywords.addAll(Arrays.asList(columnName.replace("*", "").split("[/／、（）()\\s]+")));
		return keywords.stream()
			.map(ImportFailureExcelUtil::normalize)
			.filter(keyword -> keyword.length() >= 2)
			.distinct()
			.toList();
	}

	private static class ImportFailureCellStyleHandler implements CellWriteHandler {

		private final List<List<String>> columnKeywords;
		private final List<List<Object>> rows;
		private final int failureReasonColumnIndex;
		private final Map<Short, CellStyle> redStyleCache = new HashMap<>();
		private final Map<Short, CellStyle> noWrapStyleCache = new HashMap<>();
		private final Map<Integer, Integer> columnWidthCache = new HashMap<>();

		private ImportFailureCellStyleHandler(List<Field> excelFields, List<List<Object>> rows) {
			this.columnKeywords = excelFields.stream()
				.map(ImportFailureExcelUtil::columnKeywords)
				.toList();
			this.rows = rows;
			this.failureReasonColumnIndex = excelFields.size();
		}

		@Override
		public void afterCellDispose(
			WriteSheetHolder writeSheetHolder,
			WriteTableHolder writeTableHolder,
			List<WriteCellData<?>> cellDataList,
			Cell cell,
			cn.idev.excel.metadata.Head head,
			Integer relativeRowIndex,
			Boolean isHead) {
			if (Boolean.TRUE.equals(isHead)) {
				adjustColumnWidth(cell);
				markNoWrap(cell);
				return;
			}
			adjustColumnWidth(cell);
			if (relativeRowIndex == null || relativeRowIndex < 0 || relativeRowIndex >= rows.size()) {
				return;
			}
			if (shouldMarkRed(relativeRowIndex, cell.getColumnIndex())) {
				markRed(cell);
			}
		}

		private boolean shouldMarkRed(int rowIndex, int columnIndex) {
			if (columnIndex == failureReasonColumnIndex) {
				return true;
			}
			if (columnIndex < 0 || columnIndex >= columnKeywords.size()) {
				return false;
			}
			String failureReason = normalize(String.valueOf(rows.get(rowIndex).get(failureReasonColumnIndex)));
			return columnKeywords.get(columnIndex).stream().anyMatch(failureReason::contains);
		}

		private void markRed(Cell cell) {
			CellStyle currentStyle = cell.getCellStyle();
			CellStyle redStyle = redStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
				Workbook workbook = cell.getSheet().getWorkbook();
				CellStyle newStyle = workbook.createCellStyle();
				newStyle.cloneStyleFrom(currentStyle);
				Font font = workbook.createFont();
				font.setColor(IndexedColors.RED.getIndex());
				newStyle.setFont(font);
				return newStyle;
			});
			cell.setCellStyle(redStyle);
		}

		private void markNoWrap(Cell cell) {
			CellStyle currentStyle = cell.getCellStyle();
			CellStyle noWrapStyle = noWrapStyleCache.computeIfAbsent(currentStyle.getIndex(), styleIndex -> {
				CellStyle newStyle = cell.getSheet().getWorkbook().createCellStyle();
				newStyle.cloneStyleFrom(currentStyle);
				newStyle.setWrapText(false);
				return newStyle;
			});
			cell.setCellStyle(noWrapStyle);
		}

		private void adjustColumnWidth(Cell cell) {
			int columnIndex = cell.getColumnIndex();
			int columnWidth = Math.min(Math.max(displayWidth(cell.toString()) + 4, 12), 80) * 256;
			Integer currentWidth = columnWidthCache.get(columnIndex);
			if (currentWidth == null || columnWidth > currentWidth) {
				columnWidthCache.put(columnIndex, columnWidth);
				cell.getSheet().setColumnWidth(columnIndex, columnWidth);
			}
		}

		private int displayWidth(String value) {
			int width = 0;
			for (int index = 0; index < value.length(); index++) {
				width += value.charAt(index) > 255 ? 2 : 1;
			}
			return width;
		}
	}

}
