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

import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
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.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.PersonalItemConstants;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemPageReqVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemRespVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.personal.vo.item.PersonalItemStatusActionReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.worklog.TaskWorklogSaveReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.personal.PersonalItemDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskWorklogDO;
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.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.personal.PersonalItemMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskWorklogMapper;
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.attachment.AttachmentFileIdResolver;
import com.njcn.rdms.module.project.framework.attachment.AttachmentValidator;
import com.njcn.rdms.module.project.service.project.ProjectService;
import com.njcn.rdms.module.project.service.status.StatusActionTextResolver;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Collections;
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.function.Function;

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 PersonalItemServiceImpl implements PersonalItemService {

    private static final BigDecimal DURATION_GRANULARITY_HOURS = new BigDecimal("0.5");
    private static final String WORKLOG_AUTO_START_ACTION = ObjectActivityConstants.TASK_ACTION_AUTO_START;
    private static final String WORKLOG_AUTO_START_REASON = "由工作日志自动触发";

    @Resource
    private PersonalItemMapper personalItemMapper;
    @Resource
    private ProjectExecutionMapper projectExecutionMapper;
    @Resource
    private ProjectTaskMapper projectTaskMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private TaskWorklogMapper taskWorklogMapper;
    @Resource
    private PersonalItemAccessService personalItemAccessService;
    @Resource
    private PersonalItemStatusViewService personalItemStatusViewService;
    @Resource
    private AttachmentFileIdResolver attachmentFileIdResolver;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private StatusActionTextResolver statusActionTextResolver;
    @Resource
    private ProjectService projectService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createItem(PersonalItemSaveReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        validateDateRange(reqVO.getPlannedStartDate(), reqVO.getPlannedEndDate(), "计划结束日期不能早于计划开始日期");
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        PersonalItemDO item = new PersonalItemDO();
        item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
        item.setType(normalizeRequiredType(reqVO.getType(), "个人事项类型不能为空"));
        item.setOwnerId(loginUserId);
        item.setStatusCode(getInitialStatusCode());
        item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
        item.setPlannedStartDate(reqVO.getPlannedStartDate());
        item.setPlannedEndDate(reqVO.getPlannedEndDate());
        item.setTaskDesc(normalizeNullableText(reqVO.getTaskDesc()));
        item.setAttachments(reqVO.getAttachments());
        personalItemMapper.insert(item);

        writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_CREATE, null,
                item.getStatusCode(), buildFieldChanges(null, item), null);
        return item.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItem(Long id, PersonalItemSaveReqVO reqVO) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);
        validateItemAllowEdit(item);
//        if (reqVO.getExecutionId() != null) {
//            personalItemAccessService.validateOwnerInExecution(reqVO.getExecutionId(), item.getOwnerId());
//        }
        validateDateRange(reqVO.getPlannedStartDate(), reqVO.getPlannedEndDate(), "计划结束日期不能早于计划开始日期");
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        PersonalItemDO before = cloneItem(item);
        item.setTaskTitle(normalizeRequiredTitle(reqVO.getTaskTitle()));
        item.setType(normalizeRequiredType(reqVO.getType(), "个人事项类型不能为空"));
        item.setProgressRate(normalizeProgress(reqVO.getProgressRate()));
        item.setPlannedStartDate(reqVO.getPlannedStartDate());
        item.setPlannedEndDate(reqVO.getPlannedEndDate());
        item.setTaskDesc(normalizeNullableText(reqVO.getTaskDesc()));
        item.setAttachments(reqVO.getAttachments());
        personalItemMapper.updateById(item);

        writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_UPDATE, before.getStatusCode(),
                item.getStatusCode(), buildFieldChanges(before, item), null);
    }

    @Override
    public PersonalItemRespVO getItemRespVO(Long id) {
        PersonalItemDO item = validateOwnedItemExists(id);
        PersonalItemRespVO respVO = toRespVO(item);
        respVO.setOwnerNickname(loadUserNickname(item.getOwnerId()));
        respVO.setTotalSpentHours(defaultDuration(taskWorklogMapper.sumDurationByTaskId(item.getId())));
        return respVO;
    }

    @Override
    public PageResult<PersonalItemRespVO> getItemRespVOPage(PersonalItemPageReqVO reqVO) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (reqVO.getOwnerId() != null && !Objects.equals(reqVO.getOwnerId(), loginUserId)) {
            return PageResult.empty();
        }
        reqVO.setOwnerId(loginUserId);
        PageResult<PersonalItemDO> doPage = personalItemMapper.selectPageByOwnerId(loginUserId, reqVO);
        List<PersonalItemRespVO> list = doPage.getList().stream().map(this::toRespVO).toList();
        fillOwnerNicknames(list);
        fillTotalSpentHours(list);
        return new PageResult<>(list, doPage.getTotal());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void changeStatus(Long id, PersonalItemStatusActionReqVO reqVO) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);

        String actionCode = normalizeRequiredActionCode(reqVO.getActionCode());
        String fromStatus = item.getStatusCode();
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus, actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_NOT_ALLOWED,
                    statusActionTextResolver.statusName(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus),
                    statusActionTextResolver.actionName(PersonalItemConstants.STATUS_OBJECT_TYPE, actionCode));
        }
        String reason = normalizeNullableText(reqVO.getReason());
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_ACTION_REASON_REQUIRED, transition.getActionName());
        }
        String toStatus = transition.getToStatusCode();
        int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus, reason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
        }

        PersonalItemDO before = cloneItem(item);
        item.setStatusCode(toStatus);
        item.setLastStatusReason(reason);
        applyLifecycleFields(item, fromStatus, toStatus);
        personalItemMapper.updateById(item);

        writeAuditLog(item, actionCode, fromStatus, toStatus, buildFieldChanges(before, item), reason);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createWorklog(Long id, TaskWorklogSaveReqVO reqVO) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);
        validateItemAllowEdit(item);

        Long userId = item.getOwnerId();
        validateDateRange(reqVO.getStartDate(), reqVO.getEndDate(), "段起始日期不能晚于段结束日期");
        validateDurationGranularity(reqVO.getDurationHours());
        validateNoOverlap(item.getId(), userId, reqVO.getStartDate(), reqVO.getEndDate(), null);
        validateProgressMonotonicity(item.getId(), userId, reqVO.getEndDate(), reqVO.getProgressRate(), null);
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        TaskWorklogDO worklog = buildWorklog(item.getId(), userId, reqVO);
        taskWorklogMapper.insert(worklog);

        PersonalItemDO current = cloneItem(item);
        maybeAutoStartByWorklog(current);
        syncOwnerProgressFromLatestWorklog(current);
        personalItemMapper.updateById(current);
        return worklog.getId();
    }

    @Override
    public PageResult<TaskWorklogRespVO> getWorklogPage(Long id, TaskWorklogPageReqVO reqVO) {
        PersonalItemDO item = validateOwnedItemExists(id);
        if (reqVO.getUserId() != null && !Objects.equals(reqVO.getUserId(), item.getOwnerId())) {
            return PageResult.empty();
        }
        reqVO.setUserId(item.getOwnerId());
        PageResult<TaskWorklogDO> page = taskWorklogMapper.selectPageByTaskId(item.getId(), reqVO);
        if (page.getList().isEmpty()) {
            return new PageResult<>(Collections.emptyList(), page.getTotal());
        }
        Map<Long, String> nicknameMap = loadUserNicknameMap(page.getList().stream()
                .map(TaskWorklogDO::getUserId)
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new)));
        List<TaskWorklogRespVO> list = page.getList().stream().map(worklog -> {
            TaskWorklogRespVO vo = BeanUtils.toBean(worklog, TaskWorklogRespVO.class);
            vo.setUserNickname(nicknameMap.get(worklog.getUserId()));
            return vo;
        }).toList();
        return new PageResult<>(list, page.getTotal());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateWorklog(Long id, Long worklogId, TaskWorklogSaveReqVO reqVO) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);
        validateItemAllowEdit(item);

        TaskWorklogDO worklog = loadWorklog(worklogId, item.getId());
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        if (!Objects.equals(worklog.getUserId(), loginUserId)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_EDIT_NOT_OWN);
        }
        validateDateRange(reqVO.getStartDate(), reqVO.getEndDate(), "段起始日期不能晚于段结束日期");
        validateDurationGranularity(reqVO.getDurationHours());
        validateNoOverlap(item.getId(), worklog.getUserId(), reqVO.getStartDate(), reqVO.getEndDate(), worklog.getId());
        validateProgressMonotonicity(item.getId(), worklog.getUserId(), reqVO.getEndDate(), reqVO.getProgressRate(),
                worklog.getId());
        AttachmentValidator.validate(reqVO.getAttachments());
        attachmentFileIdResolver.resolve(reqVO.getAttachments());

        TaskWorklogDO update = new TaskWorklogDO();
        update.setId(worklog.getId());
        update.setStartDate(reqVO.getStartDate());
        update.setEndDate(reqVO.getEndDate());
        update.setDurationHours(reqVO.getDurationHours());
        update.setProgressRate(reqVO.getProgressRate());
        update.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
        update.setAttachments(reqVO.getAttachments());
        update.setDifficulty(reqVO.getDifficulty());
        taskWorklogMapper.updateById(update);

        PersonalItemDO current = cloneItem(item);
        syncOwnerProgressFromLatestWorklog(current);
        personalItemMapper.updateById(current);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteWorklog(Long id, Long worklogId) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);
        validateItemAllowEdit(item);

        TaskWorklogDO worklog = loadWorklog(worklogId, item.getId());
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        boolean isFiler = Objects.equals(worklog.getUserId(), loginUserId);
        boolean isOwner = Objects.equals(item.getOwnerId(), loginUserId);
        if (!isFiler && !isOwner) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DELETE_FORBIDDEN);
        }
        taskWorklogMapper.deleteById(worklog.getId());

        PersonalItemDO current = cloneItem(item);
        syncOwnerProgressFromLatestWorklog(current);
        personalItemMapper.updateById(current);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteWorklogs(Long id, List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw invalidParamException("涓汉浜嬮」宸ヤ綔鏃ュ織缂栧彿鍒楄〃涓嶈兘涓虹┖");
        }
        for (Long worklogId : ids) {
            deleteWorklog(id, worklogId);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteItem(Long id) {
        PersonalItemDO item = validateItemExists(id);
        personalItemAccessService.checkCanWrite(item);
        String fromStatus = item.getStatusCode();
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
        if (statusModel == null || !Boolean.TRUE.equals(statusModel.getInitialFlag())) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_ALLOW_DELETE);
        }
        int deleteCount = personalItemMapper.deleteByIdAndStatus(id, fromStatus);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
        }
        writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE, fromStatus, null, null, null);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteItems(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw invalidParamException("个人事项编号列表不能为空");
        }
        for (Long id : ids) {
            deleteItem(id);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void relateExecution(List<Long> itemIds, Long executionId) {
        ProjectExecutionDO execution = projectExecutionMapper.selectById(executionId);
        if (execution == null) {
            throw exception(ErrorCodeConstants.PROJECT_EXECUTION_NOT_EXISTS);
        }
        Long projectId = execution.getProjectId();
        for (Long itemId : itemIds) {
            PersonalItemDO item = validateItemExists(itemId);
            personalItemAccessService.checkCanWrite(item);
            personalItemAccessService.validateOwnerInExecution(executionId, item.getOwnerId());

            ProjectTaskDO task = buildProjectTaskFromItem(item, projectId, executionId);
            projectTaskMapper.insert(task);
            personalItemMapper.deleteById(item.getId());
            writeAuditLog(item, ObjectActivityConstants.PERSONAL_ITEM_ACTION_DELETE,
                    item.getStatusCode(), null, null, "关联执行后转为项目任务");
        }
        // 个人事项转为项目根任务后，项目根任务均值已变，需下推一次项目进度重算（与任务侧增删改入口口径一致）
        projectService.recalcProgress(projectId);
    }

    private ProjectTaskDO buildProjectTaskFromItem(PersonalItemDO item, Long projectId, Long executionId) {
        ProjectTaskDO task = new ProjectTaskDO();
        task.setId(item.getId());
        task.setProjectId(projectId);
        task.setExecutionId(executionId);
        task.setParentTaskId(null);
        task.setTaskTitle(item.getTaskTitle());
        task.setType(item.getType());
        task.setOwnerId(item.getOwnerId());
        task.setStatusCode(item.getStatusCode());
        task.setProgressRate(item.getProgressRate());
        task.setPlannedStartDate(item.getPlannedStartDate());
        task.setPlannedEndDate(item.getPlannedEndDate());
        task.setActualStartDate(item.getActualStartDate());
        task.setActualEndDate(item.getActualEndDate());
        task.setTaskDesc(item.getTaskDesc());
        task.setLastStatusReason(item.getLastStatusReason());
        task.setAttachments(item.getAttachments());
        return task;
    }

    private PersonalItemDO validateItemExists(Long id) {
        PersonalItemDO item = personalItemMapper.selectById(id);
        if (item == null) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_EXISTS);
        }
        return item;
    }

    private PersonalItemDO validateOwnedItemExists(Long id) {
        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        PersonalItemDO item = personalItemMapper.selectByIdAndOwnerId(id, loginUserId);
        if (item == null) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_NOT_EXISTS);
        }
        return item;
    }

    private void validateItemAllowEdit(PersonalItemDO item) {
        ObjectStatusModelDO statusModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, item.getStatusCode());
        if (statusModel == null) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (!Boolean.TRUE.equals(statusModel.getAllowEdit())) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_NOT_ALLOW_EDIT);
        }
    }

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

    private PersonalItemRespVO toRespVO(PersonalItemDO item) {
        return BeanUtils.toBean(item, PersonalItemRespVO.class, this::applyLifecycle);
    }

    private void applyLifecycle(PersonalItemRespVO respVO) {
        PersonalItemStatusViewService.PersonalItemLifecycleView lifecycle =
                personalItemStatusViewService.getLifecycle(respVO.getStatusCode(), respVO.getProgressRate());
        respVO.setStatusName(lifecycle.statusName());
        respVO.setTerminal(lifecycle.terminal());
        respVO.setAllowEdit(lifecycle.allowEdit());
        respVO.setAvailableActions(lifecycle.availableActions());
    }

    private void fillOwnerNicknames(List<PersonalItemRespVO> list) {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        Collection<Long> ownerIds = list.stream()
                .map(PersonalItemRespVO::getOwnerId)
                .filter(Objects::nonNull)
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        Map<Long, String> nicknameMap = loadUserNicknameMap(ownerIds);
        list.forEach(item -> item.setOwnerNickname(nicknameMap.get(item.getOwnerId())));
    }

    private void fillTotalSpentHours(List<PersonalItemRespVO> list) {
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        Collection<Long> itemIds = list.stream()
                .map(PersonalItemRespVO::getId)
                .filter(Objects::nonNull)
                .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
        Map<Long, BigDecimal> spentHoursMap = loadTotalSpentHoursMap(itemIds);
        list.forEach(item -> item.setTotalSpentHours(
                spentHoursMap.getOrDefault(item.getId(), BigDecimal.ZERO)));
    }

    private Map<Long, BigDecimal> loadTotalSpentHoursMap(Collection<Long> itemIds) {
        if (itemIds == null || itemIds.isEmpty()) {
            return Collections.emptyMap();
        }
        List<Map<String, Object>> rows = taskWorklogMapper.sumDurationGroupByTaskIds(itemIds);
        if (rows == null || rows.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, BigDecimal> result = new HashMap<>(rows.size());
        for (Map<String, Object> row : rows) {
            Long itemId = toLong(row.getOrDefault("taskId", row.get("task_id")));
            BigDecimal total = toBigDecimal(row.get("total"));
            if (itemId != null && total != null) {
                result.put(itemId, total);
            }
        }
        return result;
    }

    private Long toLong(Object value) {
        if (value instanceof Number number) {
            return number.longValue();
        }
        if (value != null && StringUtils.hasText(value.toString())) {
            return Long.valueOf(value.toString());
        }
        return null;
    }

    private BigDecimal toBigDecimal(Object value) {
        if (value instanceof BigDecimal bigDecimal) {
            return bigDecimal;
        }
        if (value != null && StringUtils.hasText(value.toString())) {
            return new BigDecimal(value.toString());
        }
        return null;
    }

    private BigDecimal defaultDuration(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }

    private String loadUserNickname(Long userId) {
        if (userId == null) {
            return null;
        }
        return loadUserNicknameMap(List.of(userId)).get(userId);
    }

    private void applyLifecycleFields(PersonalItemDO item, String fromStatus, String toStatus) {
        ObjectStatusModelDO fromModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
        ObjectStatusModelDO toModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, toStatus);
        if (fromModel == null || toModel == null) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_MODEL_NOT_EXISTS_OR_DISABLED);
        }
        if (Boolean.TRUE.equals(fromModel.getInitialFlag()) && item.getActualStartDate() == null) {
            item.setActualStartDate(LocalDate.now());
        }
        if (Boolean.TRUE.equals(toModel.getTerminalFlag())) {
            item.setActualEndDate(LocalDate.now());
            if (PersonalItemConstants.STATUS_COMPLETED.equals(toStatus)) {
                item.setProgressRate(BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP));
            }
            return;
        }
        if (Boolean.TRUE.equals(fromModel.getTerminalFlag()) && !Boolean.TRUE.equals(toModel.getTerminalFlag())) {
            item.setActualEndDate(null);
        }
    }


    private void writeAuditLog(PersonalItemDO item,
                               String actionType,
                               String fromStatus,
                               String toStatus,
                               String fieldChanges,
                               String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(PersonalItemConstants.BIZ_TYPE);
        auditLog.setBizId(item.getId());
        auditLog.setActionType(actionType);
        auditLog.setFromStatus(fromStatus);
        auditLog.setToStatus(toStatus);
        auditLog.setFieldChanges(fieldChanges);
        auditLog.setReason(reason);
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private PersonalItemDO cloneItem(PersonalItemDO source) {
        PersonalItemDO target = new PersonalItemDO();
        target.setId(source.getId());
        target.setTaskTitle(source.getTaskTitle());
        target.setType(source.getType());
        target.setOwnerId(source.getOwnerId());
        target.setStatusCode(source.getStatusCode());
        target.setProgressRate(source.getProgressRate());
        target.setPlannedStartDate(source.getPlannedStartDate());
        target.setPlannedEndDate(source.getPlannedEndDate());
        target.setActualStartDate(source.getActualStartDate());
        target.setActualEndDate(source.getActualEndDate());
        target.setTaskDesc(source.getTaskDesc());
        target.setLastStatusReason(source.getLastStatusReason());
        target.setAttachments(source.getAttachments());
        return target;
    }

    private String buildFieldChanges(PersonalItemDO before, PersonalItemDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "taskTitle", valueOf(before, PersonalItemDO::getTaskTitle),
                valueOf(after, PersonalItemDO::getTaskTitle));
        appendFieldChange(fieldChanges, "type", valueOf(before, PersonalItemDO::getType),
                valueOf(after, PersonalItemDO::getType));
        appendFieldChange(fieldChanges, "ownerId", valueOf(before, PersonalItemDO::getOwnerId),
                valueOf(after, PersonalItemDO::getOwnerId));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, PersonalItemDO::getStatusCode),
                valueOf(after, PersonalItemDO::getStatusCode));
        appendFieldChange(fieldChanges, "progressRate", valueOf(before, PersonalItemDO::getProgressRate),
                valueOf(after, PersonalItemDO::getProgressRate));
        appendFieldChange(fieldChanges, "plannedStartDate", valueOf(before, PersonalItemDO::getPlannedStartDate),
                valueOf(after, PersonalItemDO::getPlannedStartDate));
        appendFieldChange(fieldChanges, "plannedEndDate", valueOf(before, PersonalItemDO::getPlannedEndDate),
                valueOf(after, PersonalItemDO::getPlannedEndDate));
        appendFieldChange(fieldChanges, "actualStartDate", valueOf(before, PersonalItemDO::getActualStartDate),
                valueOf(after, PersonalItemDO::getActualStartDate));
        appendFieldChange(fieldChanges, "actualEndDate", valueOf(before, PersonalItemDO::getActualEndDate),
                valueOf(after, PersonalItemDO::getActualEndDate));
        appendFieldChange(fieldChanges, "taskDesc", valueOf(before, PersonalItemDO::getTaskDesc),
                valueOf(after, PersonalItemDO::getTaskDesc));
        appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, PersonalItemDO::getLastStatusReason),
                valueOf(after, PersonalItemDO::getLastStatusReason));
        appendFieldChange(fieldChanges, "attachments", valueOf(before, PersonalItemDO::getAttachments),
                valueOf(after, PersonalItemDO::getAttachments));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private <T> T valueOf(PersonalItemDO item, Function<PersonalItemDO, T> getter) {
        return item == null ? null : getter.apply(item);
    }

    private void appendFieldChange(Map<String, Object> fieldChanges, String fieldName, Object before, Object after) {
        if (Objects.equals(before, after)) {
            return;
        }
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("before", before);
        value.put("after", after);
        fieldChanges.put(fieldName, value);
    }

    private void validateDateRange(LocalDate startDate, LocalDate endDate, String message) {
        if (startDate == null || endDate == null) {
            return;
        }
        if (endDate.isBefore(startDate)) {
            throw invalidParamException(message);
        }
    }

    private BigDecimal normalizeProgress(BigDecimal value) {
        BigDecimal progress = value == null ? BigDecimal.ZERO : value;
        return progress.setScale(2, RoundingMode.HALF_UP);
    }

    private void validateDurationGranularity(BigDecimal durationHours) {
        if (durationHours == null
                || durationHours.compareTo(BigDecimal.ZERO) <= 0
                || durationHours.remainder(DURATION_GRANULARITY_HOURS).compareTo(BigDecimal.ZERO) != 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DURATION_INVALID);
        }
    }

    private void validateNoOverlap(Long taskId, Long userId, LocalDate startDate, LocalDate endDate, Long excludeId) {
        if (taskWorklogMapper.existsOverlapping(taskId, userId, startDate, endDate, excludeId)) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_DATE_OVERLAP);
        }
    }

    private void validateProgressMonotonicity(Long taskId, Long userId, LocalDate endDate,
                                              BigDecimal progressRate, Long excludeId) {
        if (progressRate == null || endDate == null) {
            return;
        }
        TaskWorklogDO prev = taskWorklogMapper.selectPrevByEndDate(taskId, userId, endDate, excludeId);
        if (prev != null && prev.getProgressRate() != null
                && prev.getProgressRate().compareTo(progressRate) > 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
        }
        TaskWorklogDO next = taskWorklogMapper.selectNextByEndDate(taskId, userId, endDate, excludeId);
        if (next != null && next.getProgressRate() != null
                && next.getProgressRate().compareTo(progressRate) < 0) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_PROGRESS_NOT_MONOTONIC);
        }
    }

    private TaskWorklogDO buildWorklog(Long taskId, Long userId, TaskWorklogSaveReqVO reqVO) {
        TaskWorklogDO worklog = new TaskWorklogDO();
        worklog.setTaskId(taskId);
        worklog.setUserId(userId);
        worklog.setStartDate(reqVO.getStartDate());
        worklog.setEndDate(reqVO.getEndDate());
        worklog.setDurationHours(reqVO.getDurationHours());
        worklog.setProgressRate(reqVO.getProgressRate());
        worklog.setWorkContent(normalizeNullableText(reqVO.getWorkContent()));
        worklog.setAttachments(reqVO.getAttachments());
        worklog.setDifficulty(reqVO.getDifficulty());
        return worklog;
    }

    private TaskWorklogDO loadWorklog(Long worklogId, Long taskId) {
        if (worklogId == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
        }
        TaskWorklogDO worklog = taskWorklogMapper.selectByIdAndTaskId(worklogId, taskId);
        if (worklog == null) {
            throw exception(ErrorCodeConstants.PROJECT_TASK_WORKLOG_NOT_EXISTS);
        }
        return worklog;
    }

    private void maybeAutoStartByWorklog(PersonalItemDO item) {
        String fromStatus = item.getStatusCode();
        ObjectStatusModelDO fromModel = objectStatusModelMapper
                .selectByObjectTypeAndStatusCodeEnabled(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus);
        if (fromModel == null || !Boolean.TRUE.equals(fromModel.getInitialFlag())) {
            return;
        }
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(PersonalItemConstants.STATUS_OBJECT_TYPE, fromStatus,
                        WORKLOG_AUTO_START_ACTION);
        if (transition == null || !StringUtils.hasText(transition.getToStatusCode())) {
            return;
        }
        String toStatus = transition.getToStatusCode();
        int updateCount = personalItemMapper.updateStatusByIdAndStatus(item.getId(), fromStatus, toStatus,
                WORKLOG_AUTO_START_REASON);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PERSONAL_ITEM_STATUS_CONCURRENT_MODIFIED);
        }
        item.setStatusCode(toStatus);
        item.setLastStatusReason(WORKLOG_AUTO_START_REASON);
        applyLifecycleFields(item, fromStatus, toStatus);
    }

    private void syncOwnerProgressFromLatestWorklog(PersonalItemDO item) {
        if (item == null || item.getId() == null || item.getOwnerId() == null) {
            return;
        }
        TaskWorklogDO latest = taskWorklogMapper.selectLatestByTaskIdAndUserId(item.getId(), item.getOwnerId());
        if (latest == null || latest.getProgressRate() == null) {
            return;
        }
        item.setProgressRate(normalizeProgress(latest.getProgressRate()));
    }

    private Map<Long, String> loadUserNicknameMap(Collection<Long> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
        if (userMap == null || userMap.isEmpty()) {
            return Collections.emptyMap();
        }
        Map<Long, String> nicknameMap = new HashMap<>(userMap.size());
        userMap.forEach((userId, user) -> nicknameMap.put(userId, user == null ? null : user.getNickname()));
        return nicknameMap;
    }

    private String normalizeRequiredTitle(String value) {
        if (!StringUtils.hasText(value)) {
            throw invalidParamException("个人事项标题不能为空");
        }
        return value.trim();
    }

    private String normalizeRequiredActionCode(String value) {
        if (!StringUtils.hasText(value)) {
            throw invalidParamException("动作编码不能为空");
        }
        return value.trim();
    }

    private String normalizeRequiredType(String value, String message) {
        if (!StringUtils.hasText(value)) {
            throw invalidParamException(message);
        }
        return value.trim();
    }

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

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