package com.njcn.gather.aireport.testreport.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.pojo.constant.TestReportConst;
import lombok.Data;
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.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Component
public class TestReportLedgerExcelParser {

    private static final DataFormatter FORMATTER = new DataFormatter();
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final List<String> REQUIRED_SHEETS = Arrays.asList(
            TestReportConst.LEDGER_POINT_SHEET_NAME);
    private static final List<String> OPTIONAL_SHEETS = Arrays.asList(
            TestReportConst.LEDGER_GUIDE_SHEET_NAME,
            TestReportConst.LEDGER_DEVICE_SHEET_NAME,
            TestReportConst.LEDGER_CLIENT_SHEET_NAME);
    private static final List<String> POINT_HEADERS = Arrays.asList(
            "分组号", "变电站", "监测点名称", "检测设备编号", "委托单位名称", "Excel附件名称", "Word附件名称");
    private static final List<String> DEVICE_HEADERS = Arrays.asList(
            "设备型号", "设备编号", "标准有效期", "设备状态", "标准报告文件名");
    private static final List<String> CLIENT_HEADERS = Arrays.asList(
            "单位名称", "联系人", "联系方式", "地址");

    public ParsedLedger parse(MultipartFile summaryFile, Map<String, MultipartFile> attachments) {
        String fileName = summaryFile == null ? null : StrUtil.trimToNull(summaryFile.getOriginalFilename());
        Set<String> attachmentNames = attachments == null ? Collections.<String>emptySet() : attachments.keySet();
        try (InputStream inputStream = summaryFile.getInputStream()) {
            return parse(fileName, inputStream, attachmentNames);
        } catch (IOException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：读取台账汇总excel失败");
        }
    }

    public ParsedLedger parse(String fileName, InputStream inputStream, Set<String> attachmentNames) {
        ParsedLedger ledger = parseForValidate(fileName, inputStream);
        ensureRequiredSheetsPresent(ledger);
        validateReferencedAttachments(ledger.getPoints(), attachmentNames);
        return ledger;
    }

    public ParsedLedger parseForValidate(String fileName, InputStream inputStream) {
        String normalizedFileName = StrUtil.trimToNull(fileName);
        if (!TestReportConst.LEDGER_SUMMARY_FILE_NAME.equals(normalizedFileName)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：台账汇总文件名必须为台账信息汇总.xlsx");
        }
        try (Workbook workbook = WorkbookFactory.create(inputStream)) {
            ParsedLedger ledger = new ParsedLedger();
            ledger.setSummaryFileName(TestReportConst.LEDGER_SUMMARY_FILE_NAME);
            ledger.getRequiredSheets().addAll(REQUIRED_SHEETS);
            collectSheets(ledger, workbook);

            Sheet pointSheet = workbook.getSheet(TestReportConst.LEDGER_POINT_SHEET_NAME);
            if (pointSheet != null) {
                ledger.setPoints(parsePointSheet(pointSheet));
            }
            // 检测设备/委托单位 sheet 按需填写，仅在实际存在时才做结构校验和解析。
            Sheet deviceSheet = workbook.getSheet(TestReportConst.LEDGER_DEVICE_SHEET_NAME);
            if (deviceSheet != null) {
                ledger.setDevices(parseDeviceSheet(deviceSheet));
            }
            Sheet clientSheet = workbook.getSheet(TestReportConst.LEDGER_CLIENT_SHEET_NAME);
            if (clientSheet != null) {
                ledger.setClients(parseClientSheet(clientSheet));
            }
            return ledger;
        } catch (BusinessException exception) {
            throw exception;
        } catch (IOException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：读取台账汇总excel失败");
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：解析台账汇总excel失败");
        }
    }

    private void collectSheets(ParsedLedger ledger, Workbook workbook) {
        for (String sheetName : REQUIRED_SHEETS) {
            Sheet sheet = workbook.getSheet(sheetName);
            if (sheet == null) {
                ledger.getMissingSheets().add(sheetName);
                continue;
            }
            ledger.getExistingSheets().add(sheetName);
        }
        for (String sheetName : OPTIONAL_SHEETS) {
            if (workbook.getSheet(sheetName) != null) {
                ledger.getExistingSheets().add(sheetName);
            }
        }
    }

    private void ensureRequiredSheetsPresent(ParsedLedger ledger) {
        if (!ledger.getMissingSheets().isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL,
                    "校验文件阶段：缺少" + String.join("、", ledger.getMissingSheets()) + "sheet");
        }
    }

    private List<ParsedPointRow> parsePointSheet(Sheet sheet) {
        validateHeaders(sheet, POINT_HEADERS, "监测点信息");
        List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
        Set<String> pointKeys = new HashSet<String>();
        Set<String> excelNames = new HashSet<String>();
        Set<String> wordNames = new HashSet<String>();
        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
            Row row = sheet.getRow(rowIndex);
            if (isEmptyRow(row, POINT_HEADERS.size())) {
                continue;
            }
            ParsedPointRow point = new ParsedPointRow();
            point.setRowNo(rowIndex + 1);
            point.setGroupNo(parsePositiveInteger(optionalCell(row, 0), "第" + (rowIndex + 1) + "行分组号必须为正整数"));
            point.setSubstationName(requireCell(row, 1, rowIndex, "变电站"));
            point.setPointName(requireCell(row, 2, rowIndex, "监测点名称"));
            point.setTestDeviceNo(optionalCell(row, 3));
            point.setClientUnitName(optionalCell(row, 4));
            point.setExcelAttachmentName(requireCell(row, 5, rowIndex, "Excel附件名称"));
            point.setWordAttachmentName(requireCell(row, 6, rowIndex, "Word附件名称"));
            validateExtension(point.getExcelAttachmentName(), Arrays.asList(".xls", ".xlsx"),
                    "第" + (rowIndex + 1) + "行Excel附件扩展名仅允许xls/xlsx");
            validateExtension(point.getWordAttachmentName(), Arrays.asList(".doc", ".docx"),
                    "第" + (rowIndex + 1) + "行Word附件扩展名仅允许doc/docx");
            String pointKey = point.getSubstationName() + "|" + point.getPointName();
            if (!pointKeys.add(pointKey)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：监测点信息中存在重复点位");
            }
            if (!excelNames.add(point.getExcelAttachmentName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：监测点信息中存在重复Excel附件名称");
            }
            if (!wordNames.add(point.getWordAttachmentName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：监测点信息中存在重复Word附件名称");
            }
            points.add(point);
        }
        if (points.isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：监测点信息sheet不能为空");
        }
        return points;
    }

    private List<ParsedDeviceRow> parseDeviceSheet(Sheet sheet) {
        validateHeaders(sheet, DEVICE_HEADERS, "检测设备");
        List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
        Set<String> deviceNos = new HashSet<String>();
        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
            Row row = sheet.getRow(rowIndex);
            if (isEmptyRow(row, DEVICE_HEADERS.size())) {
                continue;
            }
            ParsedDeviceRow device = new ParsedDeviceRow();
            device.setRowNo(rowIndex + 1);
            device.setTypeName(optionalCell(row, 0));
            device.setNo(optionalCell(row, 1));
            device.setValidityPeriod(parseOptionalDate(optionalCell(row, 2), rowIndex));
            device.setStateName(optionalCell(row, 3));
            device.setValidityReportName(optionalCell(row, 4));
            if (StrUtil.isBlank(device.getNo())) {
                continue;
            }
            if (!deviceNos.add(device.getNo())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：检测设备sheet中存在重复设备编号");
            }
            devices.add(device);
        }
        return devices;
    }

    private List<ParsedClientRow> parseClientSheet(Sheet sheet) {
        validateHeaders(sheet, CLIENT_HEADERS, "委托单位");
        List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();
        Set<String> clientNames = new HashSet<String>();
        for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
            Row row = sheet.getRow(rowIndex);
            if (isEmptyRow(row, CLIENT_HEADERS.size())) {
                continue;
            }
            ParsedClientRow client = new ParsedClientRow();
            client.setRowNo(rowIndex + 1);
            client.setName(optionalCell(row, 0));
            client.setContactName(optionalCell(row, 1));
            client.setPhonenumber(optionalCell(row, 2));
            client.setAddress(optionalCell(row, 3));
            if (StrUtil.isBlank(client.getName())) {
                continue;
            }
            if (!clientNames.add(client.getName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：委托单位sheet中存在重复单位名称");
            }
            clients.add(client);
        }
        return clients;
    }

    private void validateReferencedAttachments(List<ParsedPointRow> points, Set<String> attachmentNames) {
        Set<String> referencedNames = new HashSet<String>();
        for (ParsedPointRow point : points) {
            if (StrUtil.isNotBlank(point.getExcelAttachmentName())) {
                referencedNames.add(point.getExcelAttachmentName());
            }
            if (StrUtil.isNotBlank(point.getWordAttachmentName())) {
                referencedNames.add(point.getWordAttachmentName());
            }
        }
        for (ParsedPointRow point : points) {
            if (!attachmentNames.contains(point.getExcelAttachmentName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：第" + point.getRowNo() + "行缺少Excel附件");
            }
            if (!attachmentNames.contains(point.getWordAttachmentName())) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：第" + point.getRowNo() + "行缺少Word附件");
            }
        }
        for (String attachmentName : attachmentNames) {
            if (!referencedNames.contains(attachmentName)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：存在未在监测点信息sheet中引用的附件 " + attachmentName);
            }
        }
    }

    private void validateHeaders(Sheet sheet, List<String> expectedHeaders, String sheetName) {
        Row headerRow = sheet.getRow(0);
        if (headerRow == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：" + sheetName + "sheet缺少表头");
        }
        for (int i = 0; i < expectedHeaders.size(); i++) {
            String actualHeader = optionalCell(headerRow, i);
            if (!expectedHeaders.get(i).equals(actualHeader)) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：" + sheetName + "sheet表头不符合模板要求");
            }
        }
    }

    private boolean isEmptyRow(Row row, int cellCount) {
        if (row == null) {
            return true;
        }
        for (int i = 0; i < cellCount; i++) {
            if (StrUtil.isNotBlank(optionalCell(row, i))) {
                return false;
            }
        }
        return true;
    }

    private String requireCell(Row row, int cellIndex, int rowIndex, String fieldName) {
        String value = optionalCell(row, cellIndex);
        if (StrUtil.isBlank(value)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：第" + (rowIndex + 1) + "行缺少" + fieldName);
        }
        return value;
    }

    private String optionalCell(Row row, int cellIndex) {
        if (row == null) {
            return null;
        }
        Cell cell = row.getCell(cellIndex);
        return cell == null ? null : StrUtil.trimToNull(FORMATTER.formatCellValue(cell));
    }

    private Integer parsePositiveInteger(String value, String message) {
        if (StrUtil.isBlank(value)) {
            return null;
        }
        try {
            int parsed = Integer.parseInt(value);
            if (parsed <= 0) {
                throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：" + message);
            }
            return parsed;
        } catch (NumberFormatException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：" + message);
        }
    }

    private void validateExtension(String fileName, List<String> validExtensions, String message) {
        String lowerFileName = fileName.toLowerCase();
        for (String validExtension : validExtensions) {
            if (lowerFileName.endsWith(validExtension)) {
                return;
            }
        }
        throw new BusinessException(CommonResponseEnum.FAIL, "校验文件阶段：" + message);
    }

    private LocalDate parseDate(String value, int rowIndex) {
        try {
            return LocalDate.parse(value, DATE_FORMATTER);
        } catch (DateTimeParseException exception) {
            throw new BusinessException(CommonResponseEnum.FAIL,
                    "校验文件阶段：检测设备sheet第" + (rowIndex + 1) + "行标准有效期格式必须为yyyy-MM-dd");
        }
    }

    private LocalDate parseOptionalDate(String value, int rowIndex) {
        if (StrUtil.isBlank(value)) {
            return null;
        }
        return parseDate(value, rowIndex);
    }

    @Data
    public static class ParsedLedger {
        private String summaryFileName;
        private List<String> requiredSheets = new ArrayList<String>();
        private List<String> existingSheets = new ArrayList<String>();
        private List<String> missingSheets = new ArrayList<String>();
        private List<ParsedPointRow> points = new ArrayList<ParsedPointRow>();
        private List<ParsedDeviceRow> devices = new ArrayList<ParsedDeviceRow>();
        private List<ParsedClientRow> clients = new ArrayList<ParsedClientRow>();

        public Map<String, ParsedDeviceRow> toDeviceMap() {
            Map<String, ParsedDeviceRow> result = new HashMap<String, ParsedDeviceRow>();
            for (ParsedDeviceRow device : devices) {
                result.put(device.getNo(), device);
            }
            return result;
        }

        public Map<String, ParsedClientRow> toClientMap() {
            Map<String, ParsedClientRow> result = new HashMap<String, ParsedClientRow>();
            for (ParsedClientRow client : clients) {
                result.put(client.getName(), client);
            }
            return result;
        }
    }

    @Data
    public static class ParsedPointRow {
        private Integer rowNo;
        private Integer groupNo;
        private String substationName;
        private String pointName;
        private String testDeviceNo;
        private String clientUnitName;
        private String excelAttachmentName;
        private String wordAttachmentName;
    }

    @Data
    public static class ParsedDeviceRow {
        private Integer rowNo;
        private String typeName;
        private String no;
        private LocalDate validityPeriod;
        private String stateName;
        private String validityReportName;
    }

    @Data
    public static class ParsedClientRow {
        private Integer rowNo;
        private String name;
        private String contactName;
        private String phonenumber;
        private String address;
    }
}
