package com.njcn.rdms.module.project.service.performance;

import com.njcn.rdms.framework.common.pojo.CommonResult;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.object.BeanUtils;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.module.project.constant.PerformanceConstants;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceMonthlyResultRespVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetCreateReqVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetExcelUpdateReqVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetPageReqVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetRespVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetResponseRecordRespVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusDictRespVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusLogRespVO;
import com.njcn.rdms.module.project.controller.admin.performance.vo.PerformanceSheetStatusTransitionRespVO;
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetDO;
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetResponseRecordDO;
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceSheetStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.performance.PerformanceTemplateDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetMapper;
import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetResponseRecordMapper;
import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceSheetStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.performance.PerformanceTemplateMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.framework.notify.NotifySendEvent;
import com.njcn.rdms.module.project.framework.notify.NotifyTemplateCodeConstants;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
import com.njcn.rdms.module.project.service.team.TeamDashboardAccessService;
import com.njcn.rdms.module.system.api.dept.DeptApi;
import com.njcn.rdms.module.system.api.dept.dto.DeptRespDTO;
import com.njcn.rdms.module.system.api.file.FileApi;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.UserManagementRelationApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import com.njcn.rdms.module.system.enums.notify.NotifyMessageLevelConstants;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.exception;
import static com.njcn.rdms.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;

@Service
public class PerformanceSheetServiceImpl implements PerformanceSheetService {

    private static final String EXCEL_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
    private static final String ZIP_CONTENT_TYPE = "application/zip";

    @Resource
    private PerformanceSheetMapper performanceSheetMapper;
    @Resource
    private PerformanceTemplateMapper performanceTemplateMapper;
    @Resource
    private PerformanceSheetStatusLogMapper performanceSheetStatusLogMapper;
    @Resource
    private PerformanceSheetResponseRecordMapper performanceSheetResponseRecordMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private StatusActionTextResolver statusActionTextResolver;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private DeptApi deptApi;
    @Resource
    private UserManagementRelationApi userManagementRelationApi;
    @Resource
    private FileApi fileApi;
    @Resource
    private TeamDashboardAccessService teamDashboardAccessService;
    @Resource
    private ApplicationEventPublisher applicationEventPublisher;

    @Override
    public PageResult<PerformanceSheetRespVO> getSheetPage(PerformanceSheetPageReqVO reqVO) {
        PageResult<PerformanceSheetDO> pageResult;
        if (reqVO.getEmployeeIds() != null) {
            List<Long> employeeIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getEmployeeIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
            pageResult = performanceSheetMapper.selectEmployeePage(employeeIds, reqVO);
        } else {
            pageResult = performanceSheetMapper.selectEmployeePage(SecurityFrameworkUtils.getLoginUserId(), reqVO);
        }
        return new PageResult<>(pageResult.getList().stream().map(this::toRespVO).toList(), pageResult.getTotal());
    }

    @Override
    public PerformanceSheetRespVO getSheet(Long id) {
        PerformanceSheetDO sheet = validateReadableSheet(id);
        return toRespVO(sheet);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createSheet(PerformanceSheetCreateReqVO reqVO) {
        String periodMonth = normalizePeriodMonth(reqVO.getPeriodMonth());
        PerformanceTemplateDO template = performanceTemplateMapper.selectCurrent();
        if (template == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_TEMPLATE_CURRENT_NOT_EXISTS);
        }
        if (performanceSheetMapper.selectByEmployeeIdAndPeriodMonth(reqVO.getEmployeeId(), periodMonth) != null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_DUPLICATE);
        }

        AdminUserRespDTO employee = validateActiveUser(reqVO.getEmployeeId());
        AdminUserRespDTO manager = loadDirectManager(employee.getId());
        validateManagerOnly(manager.getId());
        DeptRespDTO dept = validateEmployeeDept(employee.getDeptId());

        PerformanceSheetDO sheet = new PerformanceSheetDO();
        sheet.setPeriodMonth(periodMonth);
        sheet.setEmployeeId(employee.getId());
        sheet.setEmployeeName(defaultText(employee.getNickname()));
        sheet.setEmployeeDeptId(employee.getDeptId());
        sheet.setEmployeeDeptName(dept.getName().trim());
        sheet.setDeptOrgType(dept.getOrgType().trim());
        sheet.setManagerId(manager.getId());
        sheet.setManagerName(defaultText(manager.getNickname()));
        sheet.setTemplateId(template.getId());
        sheet.setFileName(buildPerformanceFileName(periodMonth, employee.getNickname()));
        sheet.setFileVersion(0);
        sheet.setStatusCode(getInitialStatusCode());
        performanceSheetMapper.insert(sheet);
        return sheet.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateExcel(Long id, PerformanceSheetExcelUpdateReqVO reqVO) {
        PerformanceSheetDO current = validateSheetExists(id);
        validateManagerOnly(current.getManagerId());
        validateEditable(current);

        PerformanceSheetDO update = new PerformanceSheetDO();
        update.setFileId(reqVO.getFileId());
        update.setFileName(reqVO.getFileName().trim());
        update.setFileVersion(reqVO.getFileVersion() + 1);
        update.setActualScoreTotal(normalizeScore(reqVO.getActualScoreTotal()));
        update.setBaseScoreTotal(normalizeScore(reqVO.getBaseScoreTotal()));
        update.setExtraScoreTotal(normalizeScore(reqVO.getExtraScoreTotal()));
        int updateCount = performanceSheetMapper.updateExcelByIdAndVersion(update, id, reqVO.getFileVersion());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteSheet(Long id) {
        PerformanceSheetDO sheet = validateSheetExists(id);
        validateManagerOnly(sheet.getManagerId());
        if (!PerformanceConstants.STATUS_DRAFT.equals(sheet.getStatusCode())) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_NOT_ALLOW_DELETE);
        }
        performanceSheetMapper.deleteById(id);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void send(Long id) {
        processManagerStatusAction(id, PerformanceConstants.ACTION_SEND);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void resend(Long id) {
        processManagerStatusAction(id, PerformanceConstants.ACTION_RESEND);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void confirm(Long id, PerformanceSheetStatusActionReqVO reqVO) {
        processEmployeeResponse(id, PerformanceConstants.ACTION_CONFIRM,
                normalizeNullableText(reqVO == null ? null : reqVO.getReason()));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void reject(Long id, PerformanceSheetStatusActionReqVO reqVO) {
        String reason = normalizeNullableText(reqVO == null ? null : reqVO.getReason());
        if (!StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_REJECT_REASON_REQUIRED);
        }
        PerformanceSheetDO sheet = processEmployeeResponse(id, PerformanceConstants.ACTION_REJECT, reason);
        publishPerformanceNotice(List.of(sheet.getManagerId()), NotifyTemplateCodeConstants.PERFORMANCE_REJECTED, sheet);
    }

    @Override
    public PerformanceDownloadFile download(Long id) {
        PerformanceSheetDO sheet = validateReadableSheet(id);
        return new PerformanceDownloadFile(defaultDownloadName(sheet), EXCEL_CONTENT_TYPE, readFileContent(sheet));
    }

    @Override
    public PerformanceDownloadFile batchDownload(Collection<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            throw invalidParamException("绩效表 ID 不能为空");
        }
        List<PerformanceSheetDO> sheets = new ArrayList<>();
        for (Long id : ids) {
            sheets.add(validateReadableSheet(id));
        }
        return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets);
    }

    @Override
    public PerformanceDownloadFile exportByCondition(PerformanceSheetPageReqVO reqVO) {
        List<Long> employeeIds;
        if (reqVO.getEmployeeIds() != null) {
            employeeIds = teamDashboardAccessService.resolveRequestedSubordinateUserIds(
                    reqVO.getEmployeeIds(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD);
        } else {
            employeeIds = List.of(SecurityFrameworkUtils.getLoginUserId());
        }
        List<PerformanceSheetDO> sheets = performanceSheetMapper.selectExportList(reqVO, employeeIds);
        return zipSheets("绩效表_" + LocalDate.now() + ".zip", sheets);
    }

    @Override
    public List<PerformanceSheetStatusLogRespVO> getStatusLogs(Long id) {
        validateReadableSheet(id);
        return BeanUtils.toBean(performanceSheetStatusLogMapper.selectListBySheetId(id),
                PerformanceSheetStatusLogRespVO.class);
    }

    @Override
    public List<PerformanceSheetResponseRecordRespVO> getResponseRecords(Long id) {
        validateReadableSheet(id);
        return BeanUtils.toBean(performanceSheetResponseRecordMapper.selectListBySheetId(id),
                PerformanceSheetResponseRecordRespVO.class);
    }

    @Override
    public PerformanceMonthlyResultRespVO getMonthlyResult(Long employeeId, String periodMonth) {
        String normalizedMonth = normalizePeriodMonth(periodMonth);
        PerformanceSheetDO sheet = performanceSheetMapper.selectByEmployeeIdAndPeriodMonth(employeeId, normalizedMonth);
        PerformanceMonthlyResultRespVO respVO = new PerformanceMonthlyResultRespVO();
        respVO.setEmployeeId(employeeId);
        respVO.setPeriodMonth(normalizedMonth);
        if (sheet == null) {
            return respVO;
        }
        validateReadable(sheet);
        respVO.setSheetId(sheet.getId());
        respVO.setActualScoreTotal(sheet.getActualScoreTotal());
        respVO.setBaseScoreTotal(sheet.getBaseScoreTotal());
        respVO.setExtraScoreTotal(sheet.getExtraScoreTotal());
        respVO.setStatusCode(sheet.getStatusCode());
        return respVO;
    }

    @Override
    public List<PerformanceSheetStatusDictRespVO> getStatusDict() {
        return objectStatusModelMapper.selectListByObjectTypeEnabled(PerformanceConstants.STATUS_OBJECT_TYPE)
                .stream()
                .map(this::buildStatusDictRespVO)
                .toList();
    }

    @Override
    public List<PerformanceSheetStatusTransitionRespVO> getStatusTransitions() {
        List<String> statusCodes = objectStatusModelMapper
                .selectListByObjectTypeEnabled(PerformanceConstants.STATUS_OBJECT_TYPE)
                .stream()
                .map(ObjectStatusModelDO::getStatusCode)
                .toList();
        if (statusCodes.isEmpty()) {
            return List.of();
        }
        return objectStatusTransitionMapper
                .selectListByObjectTypeAndFromStatuses(PerformanceConstants.STATUS_OBJECT_TYPE, statusCodes)
                .stream()
                .map(this::buildStatusTransitionRespVO)
                .toList();
    }

    private void processManagerStatusAction(Long id, String actionType) {
        PerformanceSheetDO current = validateSheetExists(id);
        validateManagerOnly(current.getManagerId());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionType, null);
        if (current.getFileId() == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_FILE_REQUIRED);
        }
        PerformanceSheetDO update = new PerformanceSheetDO();
        update.setStatusCode(transition.getToStatusCode());
        update.setSentTime(LocalDateTime.now());
        int updateCount = performanceSheetMapper.updateStatusByIdAndStatus(update, id, current.getStatusCode());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED);
        }
        PerformanceSheetDO after = merge(current, update);
        writeStatusLog(after, actionType, current.getStatusCode(), transition.getToStatusCode(), null);
        publishPerformanceNotice(List.of(after.getEmployeeId()), NotifyTemplateCodeConstants.PERFORMANCE_SENT, after);
    }

    private PerformanceSheetDO processEmployeeResponse(Long id, String actionType, String reason) {
        PerformanceSheetDO current = validateSheetExists(id);
        validateEmployeeOnly(current.getEmployeeId());
        ObjectStatusTransitionDO transition = validateTransition(current.getStatusCode(), actionType, reason);
        PerformanceSheetDO update = new PerformanceSheetDO();
        update.setStatusCode(transition.getToStatusCode());
        if (PerformanceConstants.STATUS_CONFIRMED.equals(transition.getToStatusCode())) {
            update.setConfirmedTime(LocalDateTime.now());
        } else if (PerformanceConstants.STATUS_REJECTED.equals(transition.getToStatusCode())) {
            update.setRejectedTime(LocalDateTime.now());
            update.setLastStatusReason(reason);
        }
        int updateCount = performanceSheetMapper.updateStatusByIdAndStatus(update, id, current.getStatusCode());
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_CONCURRENT_MODIFIED);
        }
        PerformanceSheetDO after = merge(current, update);
        PerformanceSheetStatusLogDO statusLog = writeStatusLog(after, actionType, current.getStatusCode(),
                transition.getToStatusCode(), reason);
        writeResponseRecord(after, statusLog, actionType, current.getStatusCode(), transition.getToStatusCode(), reason);
        return after;
    }

    private PerformanceSheetStatusLogDO writeStatusLog(PerformanceSheetDO sheet, String actionType,
                                                       String fromStatus, String toStatus, String reason) {
        PerformanceSheetStatusLogDO statusLog = new PerformanceSheetStatusLogDO();
        statusLog.setSheetId(sheet.getId());
        statusLog.setActionType(actionType);
        statusLog.setFromStatus(fromStatus);
        statusLog.setToStatus(toStatus);
        statusLog.setReason(defaultText(reason));
        statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        statusLog.setPeriodMonthSnapshot(sheet.getPeriodMonth());
        statusLog.setEmployeeNameSnapshot(sheet.getEmployeeName());
        statusLog.setRemark(buildRemark(sheet));
        performanceSheetStatusLogMapper.insert(statusLog);
        return statusLog;
    }

    private void writeResponseRecord(PerformanceSheetDO sheet, PerformanceSheetStatusLogDO statusLog,
                                     String actionType, String fromStatus, String toStatus, String opinion) {
        PerformanceSheetResponseRecordDO record = new PerformanceSheetResponseRecordDO();
        record.setSheetId(sheet.getId());
        record.setStatusLogId(statusLog.getId());
        record.setRoundNo(performanceSheetResponseRecordMapper.countBySheetId(sheet.getId()) + 1);
        record.setActionType(actionType);
        record.setFromStatus(fromStatus);
        record.setToStatus(toStatus);
        record.setOpinion(defaultText(opinion));
        record.setResponderUserId(SecurityFrameworkUtils.getLoginUserId());
        record.setResponderName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        performanceSheetResponseRecordMapper.insert(record);
    }

    private PerformanceSheetDO validateSheetExists(Long id) {
        PerformanceSheetDO sheet = id == null ? null : performanceSheetMapper.selectById(id);
        if (sheet == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_NOT_EXISTS);
        }
        return sheet;
    }

    private PerformanceSheetDO validateReadableSheet(Long id) {
        PerformanceSheetDO sheet = validateSheetExists(id);
        validateReadable(sheet);
        return sheet;
    }

    private void validateReadable(PerformanceSheetDO sheet) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(loginUserId, sheet.getEmployeeId())
                && !Objects.equals(loginUserId, sheet.getManagerId())
                && !teamDashboardAccessService.canReadSubordinateUser(
                        sheet.getEmployeeId(), PerformanceConstants.PERMISSION_TEAM_DASHBOARD)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_READ_FORBIDDEN);
        }
    }

    private void validateEditable(PerformanceSheetDO sheet) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                PerformanceConstants.STATUS_OBJECT_TYPE, sheet.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_NOT_ALLOW_EDIT);
        }
    }

    private void validateManagerOnly(Long managerId) {
        if (!Objects.equals(SecurityFrameworkUtils.getLoginUserId(), managerId)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_MANAGER_ONLY);
        }
    }

    private void validateEmployeeOnly(Long employeeId) {
        if (!Objects.equals(SecurityFrameworkUtils.getLoginUserId(), employeeId)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_EMPLOYEE_ONLY);
        }
    }

    private AdminUserRespDTO validateActiveUser(Long userId) {
        CommonResult<AdminUserRespDTO> result = adminUserApi.getUser(userId);
        AdminUserRespDTO user = result == null ? null : result.getCheckedData();
        if (user == null || user.getId() == null || !Objects.equals(user.getStatus(), 0)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_INVALID);
        }
        return user;
    }

    private AdminUserRespDTO loadDirectManager(Long employeeId) {
        CommonResult<AdminUserRespDTO> result = userManagementRelationApi.getDirectManager(employeeId);
        AdminUserRespDTO manager = result == null ? null : result.getCheckedData();
        if (manager == null || manager.getId() == null || !Objects.equals(manager.getStatus(), 0)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_DIRECT_MANAGER_NOT_EXISTS);
        }
        return manager;
    }

    private DeptRespDTO validateEmployeeDept(Long deptId) {
        if (deptId == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_DEPT_INVALID);
        }
        CommonResult<DeptRespDTO> result = deptApi.getDept(deptId);
        DeptRespDTO dept = result == null ? null : result.getCheckedData();
        if (dept == null || dept.getId() == null
                || !StringUtils.hasText(dept.getName())
                || !StringUtils.hasText(dept.getOrgType())) {
            throw exception(ErrorCodeConstants.PERFORMANCE_EMPLOYEE_DEPT_INVALID);
        }
        return dept;
    }

    private PerformanceSheetRespVO toRespVO(PerformanceSheetDO sheet) {
        PerformanceSheetRespVO respVO = BeanUtils.toBean(sheet, PerformanceSheetRespVO.class);
        respVO.setStatusName(statusName(sheet.getStatusCode()));
        return respVO;
    }

    private PerformanceDownloadFile zipSheets(String filename, List<PerformanceSheetDO> sheets) {
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             ZipOutputStream zipOutputStream = new ZipOutputStream(outputStream)) {
            Map<String, Integer> filenameCounts = new HashMap<>();
            for (PerformanceSheetDO sheet : sheets) {
                byte[] content = readFileContent(sheet);
                String entryName = uniqueFilename(defaultDownloadName(sheet), filenameCounts);
                zipOutputStream.putNextEntry(new ZipEntry(entryName));
                zipOutputStream.write(content);
                zipOutputStream.closeEntry();
            }
            zipOutputStream.finish();
            return new PerformanceDownloadFile(filename, ZIP_CONTENT_TYPE, outputStream.toByteArray());
        } catch (Exception ex) {
            throw new IllegalStateException("打包绩效表失败", ex);
        }
    }

    private byte[] readFileContent(PerformanceSheetDO sheet) {
        if (sheet.getFileId() == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_FILE_REQUIRED);
        }
        CommonResult<byte[]> result = fileApi.getFileContent(sheet.getFileId());
        byte[] content = result == null ? null : result.getCheckedData();
        if (content == null || content.length == 0) {
            throw exception(ErrorCodeConstants.PERFORMANCE_FILE_NOT_EXISTS);
        }
        return content;
    }

    private String uniqueFilename(String filename, Map<String, Integer> filenameCounts) {
        Integer count = filenameCounts.merge(filename, 1, Integer::sum);
        if (count == 1) {
            return filename;
        }
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex <= 0) {
            return filename + "_" + count;
        }
        return filename.substring(0, dotIndex) + "_" + count + filename.substring(dotIndex);
    }

    private PerformanceSheetDO merge(PerformanceSheetDO source, PerformanceSheetDO update) {
        PerformanceSheetDO merged = BeanUtils.toBean(source, PerformanceSheetDO.class);
        if (update.getStatusCode() != null) {
            merged.setStatusCode(update.getStatusCode());
        }
        if (update.getSentTime() != null) {
            merged.setSentTime(update.getSentTime());
        }
        if (update.getConfirmedTime() != null) {
            merged.setConfirmedTime(update.getConfirmedTime());
        }
        if (update.getRejectedTime() != null) {
            merged.setRejectedTime(update.getRejectedTime());
        }
        if (update.getLastStatusReason() != null) {
            merged.setLastStatusReason(update.getLastStatusReason());
        }
        return merged;
    }

    private void publishPerformanceNotice(Collection<Long> recipients, String templateCode, PerformanceSheetDO sheet) {
        if (recipients == null || recipients.isEmpty()) {
            return;
        }
        Map<String, Object> params = new LinkedHashMap<>();
        params.put("periodMonth", sheet.getPeriodMonth());
        params.put("employeeName", sheet.getEmployeeName());
        params.put("managerName", sheet.getManagerName());
        params.put("sheetId", sheet.getId());
        params.put("statusName", statusName(sheet.getStatusCode()));
        params.put("reason", sheet.getLastStatusReason());
        applicationEventPublisher.publishEvent(NotifySendEvent.of(new ArrayList<>(new LinkedHashSet<>(recipients)),
                templateCode, params, NotifyMessageLevelConstants.REMIND));
    }

    private ObjectStatusTransitionDO validateTransition(String fromStatus, String actionCode, String reason) {
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction(
                PerformanceConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(PerformanceConstants.STATUS_OBJECT_TYPE, fromStatus),
                    statusActionTextResolver.actionName(PerformanceConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_ACTION_REASON_REQUIRED,
                    statusActionTextResolver.actionName(PerformanceConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        ObjectStatusModelDO toModel = objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled(
                PerformanceConstants.STATUS_OBJECT_TYPE, transition.getToStatusCode());
        if (toModel == null) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return transition;
    }

    private String getInitialStatusCode() {
        ObjectStatusModelDO statusModel = objectStatusModelMapper.selectInitialByObjectTypeEnabled(
                PerformanceConstants.STATUS_OBJECT_TYPE);
        if (statusModel == null || !StringUtils.hasText(statusModel.getStatusCode())) {
            throw exception(ErrorCodeConstants.PERFORMANCE_SHEET_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        return statusModel.getStatusCode();
    }

    private String normalizePeriodMonth(String periodMonth) {
        if (!StringUtils.hasText(periodMonth)) {
            throw invalidParamException("绩效月份不能为空");
        }
        try {
            return YearMonth.parse(periodMonth.trim()).toString();
        } catch (DateTimeParseException ex) {
            throw invalidParamException("绩效月份格式必须为 yyyy-MM");
        }
    }

    private BigDecimal normalizeScore(BigDecimal value) {
        return value.setScale(2, RoundingMode.HALF_UP);
    }

    private String buildPerformanceFileName(String periodMonth, String employeeName) {
        return periodMonth + "月-绩效表_" + defaultText(employeeName) + ".xlsx";
    }

    private String defaultDownloadName(PerformanceSheetDO sheet) {
        return StringUtils.hasText(sheet.getFileName())
                ? sheet.getFileName()
                : buildPerformanceFileName(sheet.getPeriodMonth(), sheet.getEmployeeName());
    }

    private String buildRemark(PerformanceSheetDO sheet) {
        return sheet.getPeriodMonth() + " " + defaultText(sheet.getEmployeeName()) + " 绩效表";
    }

    private String statusName(String statusCode) {
        return statusActionTextResolver.statusName(PerformanceConstants.STATUS_OBJECT_TYPE, statusCode);
    }

    private PerformanceSheetStatusDictRespVO buildStatusDictRespVO(ObjectStatusModelDO statusModel) {
        PerformanceSheetStatusDictRespVO respVO = new PerformanceSheetStatusDictRespVO();
        respVO.setStatusCode(statusModel.getStatusCode());
        respVO.setStatusName(statusModel.getStatusName());
        respVO.setSort(statusModel.getSort());
        respVO.setInitialFlag(statusModel.getInitialFlag());
        respVO.setTerminalFlag(statusModel.getTerminalFlag());
        respVO.setAllowEdit(statusModel.getAllowEdit());
        return respVO;
    }

    private PerformanceSheetStatusTransitionRespVO buildStatusTransitionRespVO(ObjectStatusTransitionDO transition) {
        PerformanceSheetStatusTransitionRespVO respVO = new PerformanceSheetStatusTransitionRespVO();
        respVO.setActionCode(transition.getActionCode());
        respVO.setActionName(transition.getActionName());
        respVO.setFromStatusCode(transition.getFromStatusCode());
        respVO.setToStatusCode(transition.getToStatusCode());
        respVO.setNeedReason(transition.getNeedReason());
        return respVO;
    }

    private String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

    private String defaultText(String value) {
        return StringUtils.hasText(value) ? value.trim() : "";
    }
}
