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

import com.google.common.annotations.VisibleForTesting;
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.controller.admin.product.vo.product.*;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.product.ProductStatusLogDO;
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.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper;
import com.njcn.rdms.module.project.dal.mysql.product.ProductStatusLogMapper;
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.security.annotation.CheckObjectPermission;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRolePermissionRespDTO;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.enums.permission.MenuTypeEnum;
import com.njcn.rdms.module.system.enums.permission.PermissionScopeTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

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

/**
 * 产品 Service 实现类
 */
@Service
public class ProductServiceImpl implements ProductService {

    private static final String PRODUCT_OBJECT_TYPE = "product";
    private static final String ROLE_SCOPE_OBJECT = PermissionScopeTypeEnum.OBJECT.getScopeType();
    private static final String PRODUCT_MANAGER_ROLE_CODE = "product_manager";

    private static final String PRODUCT_ACTIVE_STATUS = "active";
    private static final String PRODUCT_PAUSED_STATUS = "paused";
    private static final String PRODUCT_ARCHIVED_STATUS = "archived";
    private static final String PRODUCT_ABANDONED_STATUS = "abandoned";

    private static final Integer MEMBER_STATUS_ACTIVE = 0;

    private static final String PRODUCT_CODE_PREFIX = "CNPD";
    private static final String PRODUCT_QUERY_PERMISSION = "project:product:query";
    private static final String PRODUCT_UPDATE_PERMISSION = "project:product:update";
    private static final String PRODUCT_STATUS_PERMISSION = "project:product:status";
    private static final String PRODUCT_DELETE_PERMISSION = "project:product:delete";
    private static final String PRODUCT_DELETE_CONFIRM_TEXT = "DELETE";

    @Resource
    private ProductMapper productMapper;
    @Resource
    private ProductStatusLogMapper productStatusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private ObjectPermissionApi objectPermissionApi;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private ProductRequirementModuleMapper requirementModuleMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createProduct(ProductSaveReqVO createReqVO) {
        validateCreateReqVO(createReqVO);
        validateManagerUser(createReqVO.getManagerUserId());

        ProductDO product = new ProductDO();
        product.setCode(generateProductCode(createReqVO.getCode()));
        product.setDirectionCode(createReqVO.getDirectionCode());
        product.setStatusCode(PRODUCT_ACTIVE_STATUS);
        product.setName(createReqVO.getName().trim());
        product.setManagerUserId(createReqVO.getManagerUserId());
        product.setDescription(normalizeNullableText(createReqVO.getDescription()));
        productMapper.insert(product);

        initManagerMemberRelation(product);
        initDefaultRequirementModule(product);
        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_CREATE, null, PRODUCT_ACTIVE_STATUS,
                buildProductFieldChanges(null, product), null);
        return product.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#updateReqVO.id",
            permission = PRODUCT_UPDATE_PERMISSION)
    public void updateProduct(ProductSaveReqVO updateReqVO) {
        if (updateReqVO.getId() == null) {
            throw invalidParamException("产品编号不能为空");
        }
        ProductDO product = validateProductExists(updateReqVO.getId());
        validateProductCodeUnchanged(product, updateReqVO.getCode());
        validateManagerUserUnchanged(product, updateReqVO.getManagerUserId());
        applyProductBaseInfoUpdate(product, updateReqVO.getDirectionCode(), updateReqVO.getName(), updateReqVO.getDescription());
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#productId",
            permission = PRODUCT_UPDATE_PERMISSION)
    public void updateProductBaseInfo(Long productId, ProductSettingBaseInfoUpdateReqVO reqVO) {
        ProductDO product = validateProductExists(productId);
        applyProductBaseInfoUpdate(product, reqVO.getDirectionCode(), reqVO.getName(), reqVO.getDescription());
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#id",
            permission = PRODUCT_QUERY_PERMISSION)
    public ProductDO getProduct(Long id) {
        return validateProductExists(id);
    }

    @Override
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#id", memberOnly = true)
    public ProductContextRespVO getProductContext(Long id) {
        ProductDO product = validateProductExists(id);

        Long loginUserId = SecurityFrameworkUtils.getLoginUserId();
        UserObjectRoleDO currentMember = userObjectRoleMapper
                .selectActiveByObjectAndUserId(PRODUCT_OBJECT_TYPE, id, loginUserId);
        ProductContextRespVO respVO = new ProductContextRespVO();
        respVO.setCurrentProduct(buildCurrentProduct(product));
        if (currentMember == null) {
            respVO.setNavs(Collections.emptyList());
            respVO.setButtons(Collections.emptyList());
            return respVO;
        }

        ObjectRolePermissionRespDTO permissionDetail = objectPermissionApi
                .getObjectRolePermissionDetail(currentMember.getRoleId(), ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
                .getCheckedData();
        ObjectRoleRespDTO currentRole = permissionDetail == null ? null : permissionDetail.getCurrentRole();
        List<ObjectMenuRespDTO> menus = permissionDetail == null || permissionDetail.getMenus() == null
                ? Collections.emptyList()
                : permissionDetail.getMenus();
        respVO.setCurrentRole(buildCurrentRole(currentMember, currentRole));
        respVO.setNavs(buildContextNavs(menus));
        respVO.setButtons(buildContextButtons(menus));
        return respVO;
    }

    @Override
    public PageResult<ProductDO> getProductPage(ProductPageReqVO pageReqVO) {
        return productMapper.selectPage(pageReqVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.id",
            permission = PRODUCT_STATUS_PERMISSION)
    public void changeProductStatus(ProductStatusActionReqVO reqVO) {
        ProductDO product = validateProductExists(reqVO.getId());
        String actionCode = reqVO.getActionCode().trim();
        ObjectStatusTransitionDO transition = validateProductTransition(product.getStatusCode(), actionCode);
        String reason = normalizeNullableText(reqVO.getReason());
        validateTransitionReason(transition, reason);

        String fromStatus = product.getStatusCode();
        String toStatus = transition.getToStatusCode();
        int updateCount = productMapper.updateStatusByIdAndStatus(product.getId(), fromStatus, toStatus, reason);
        if (updateCount != 1) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_CONCURRENT_MODIFIED);
        }
        product.setStatusCode(toStatus);
        product.setLastStatusReason(reason);

        writeProductStatusLog(product, actionCode, fromStatus, toStatus, reason);
        writeBizAuditLog(product, actionCode, fromStatus, toStatus, null, reason);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    @CheckObjectPermission(objectType = PRODUCT_OBJECT_TYPE, objectId = "#reqVO.id",
            permission = PRODUCT_DELETE_PERMISSION)
    public void deleteProduct(ProductDeleteReqVO reqVO) {
        ProductDO product = validateProductExists(reqVO.getId());
        validateDeleteConfirmText(reqVO.getConfirmText());
        if (!Objects.equals(product.getName(), reqVO.getProductName().trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_DELETE_NAME_MISMATCH);
        }

        String reason = reqVO.getReason().trim();
        String fromStatus = product.getStatusCode();
        int deleteCount = productMapper.deleteByIdAndStatus(reqVO.getId(), fromStatus);
        if (deleteCount != 1) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_CONCURRENT_MODIFIED);
        }

        writeProductStatusLog(product, ObjectActivityConstants.PRODUCT_ACTION_DELETE, fromStatus, null, reason);
        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_DELETE, fromStatus, null, null, reason);
    }

    @VisibleForTesting
    void validateCreateReqVO(ProductSaveReqVO createReqVO) {
        validateProductCodeUnique(null, createReqVO.getCode());
        validateProductNameUnique(null, createReqVO.getName());
    }

    @VisibleForTesting
    ProductDO validateProductExists(Long id) {
        if (id == null) {
            throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
        }
        ProductDO product = productMapper.selectById(id);
        if (product == null) {
            throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
        }
        return product;
    }

    @VisibleForTesting
    void validateProductCodeUnique(Long id, String code) {
        if (!StringUtils.hasText(code)) {
            return;
        }
        String normalizedCode = code.trim();
        ProductDO product = productMapper.selectByCode(normalizedCode);
        if (product == null) {
            return;
        }
        if (id == null || !product.getId().equals(id)) {
            throw exception(ErrorCodeConstants.PRODUCT_CODE_DUPLICATE, normalizedCode);
        }
    }

    @VisibleForTesting
    void validateProductNameUnique(Long id, String name) {
        String normalizedName = name.trim();
        ProductDO product = productMapper.selectByName(normalizedName);
        if (product == null) {
            return;
        }
        if (id == null || !product.getId().equals(id)) {
            throw exception(ErrorCodeConstants.PRODUCT_NAME_DUPLICATE, normalizedName);
        }
    }

    @VisibleForTesting
    void validateManagerUser(Long managerUserId) {
        adminUserApi.validateUser(managerUserId);
    }

    @VisibleForTesting
    void validateProductCodeUnchanged(ProductDO product, String code) {
        if (!StringUtils.hasText(code)) {
            return;
        }
        if (!Objects.equals(product.getCode(), code.trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_CODE_NOT_MODIFIABLE);
        }
    }

    @VisibleForTesting
    void validateManagerUserUnchanged(ProductDO product, Long managerUserId) {
        if (!Objects.equals(product.getManagerUserId(), managerUserId)) {
            throw exception(ErrorCodeConstants.PRODUCT_MANAGER_NOT_MODIFIABLE);
        }
    }

    @VisibleForTesting
    void validateProductEditable(ProductDO product, ProductSaveReqVO updateReqVO) {
        validateProductEditable(product, updateReqVO.getDirectionCode(), updateReqVO.getName());
    }

    @VisibleForTesting
    void validateProductEditable(ProductDO product, String directionCode, String name) {
        if (PRODUCT_ARCHIVED_STATUS.equals(product.getStatusCode())
                || PRODUCT_ABANDONED_STATUS.equals(product.getStatusCode())) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT);
        }
        if (!PRODUCT_PAUSED_STATUS.equals(product.getStatusCode())) {
            return;
        }
        if (!Objects.equals(product.getDirectionCode(), directionCode)
                || !Objects.equals(product.getName(), name.trim())) {
            throw exception(ErrorCodeConstants.PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE);
        }
    }

    @VisibleForTesting
    ObjectStatusTransitionDO validateProductTransition(String fromStatusCode, String actionCode) {
        ObjectStatusTransitionDO transition = objectStatusTransitionMapper
                .selectByObjectTypeAndFromStatusAndAction(PRODUCT_OBJECT_TYPE, fromStatusCode, actionCode);
        if (transition == null) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED, actionCode);
        }
        return transition;
    }

    @VisibleForTesting
    void validateTransitionReason(ObjectStatusTransitionDO transition, String reason) {
        if (Boolean.TRUE.equals(transition.getNeedReason()) && !StringUtils.hasText(reason)) {
            throw exception(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED, transition.getActionCode());
        }
    }

    @VisibleForTesting
    void validateDeleteConfirmText(String confirmText) {
        String normalizedConfirmText = normalizeNullableText(confirmText);
        if (!Objects.equals(PRODUCT_DELETE_CONFIRM_TEXT, normalizedConfirmText)) {
            throw exception(ErrorCodeConstants.PRODUCT_DELETE_CONFIRM_TEXT_INVALID);
        }
    }

    private String generateProductCode(String code) {
        String normalizedCode = normalizeNullableText(code);
        if (StringUtils.hasText(normalizedCode)) {
            validateProductCodeUnique(null, normalizedCode);
            return normalizedCode;
        }

        String year = String.valueOf(LocalDate.now().getYear());
        String codePrefix = PRODUCT_CODE_PREFIX + year;
        int nextSequence = 1;
        for (ProductDO product : productMapper.selectListByCodePrefix(codePrefix)) {
            String existedCode = product.getCode();
            if (!StringUtils.hasText(existedCode) || !existedCode.matches(codePrefix + "\\d{3}")) {
                continue;
            }
            nextSequence = Integer.parseInt(existedCode.substring(codePrefix.length())) + 1;
            break;
        }
        if (nextSequence > 999) {
            throw invalidParamException("{} 年产品自动编码序号已用尽", year);
        }
        String generatedCode = codePrefix + String.format("%03d", nextSequence);
        validateProductCodeUnique(null, generatedCode);
        return generatedCode;
    }

    private void initDefaultRequirementModule(ProductDO product) {
        com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO module =
                new com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO();
        module.setParentId(0L);
        module.setProductId(product.getId());
        module.setModuleName("全部需求");
        module.setRemark("自动创建的模块");
        module.setSort(0);
        requirementModuleMapper.insert(module);
    }

    private void initManagerMemberRelation(ProductDO product) {
        ObjectRoleRespDTO managerRole = objectPermissionApi
                .getObjectRoleByCode(PRODUCT_MANAGER_ROLE_CODE, ROLE_SCOPE_OBJECT, PRODUCT_OBJECT_TYPE)
                .getCheckedData();
        if (managerRole == null) {
            throw invalidParamException("未找到产品经理对象角色配置：{}", PRODUCT_MANAGER_ROLE_CODE);
        }

        UserObjectRoleDO member = new UserObjectRoleDO();
        member.setUserId(product.getManagerUserId());
        member.setObjectType(PRODUCT_OBJECT_TYPE);
        member.setObjectId(product.getId());
        member.setRoleId(managerRole.getId());
        member.setStatus(MEMBER_STATUS_ACTIVE);
        member.setJoinedTime(LocalDateTime.now());
        member.setLeftTime(null);
        userObjectRoleMapper.insert(member);

        writeMemberInitAuditLog(member);
        writeManagerInitAuditLog(product.getId(), product.getManagerUserId());
    }

    private ProductContextProductRespVO buildCurrentProduct(ProductDO product) {
        return BeanUtils.toBean(product, ProductContextProductRespVO.class);
    }

    private ProductContextRoleRespVO buildCurrentRole(UserObjectRoleDO currentMember, ObjectRoleRespDTO currentRole) {
        ProductContextRoleRespVO roleRespVO = new ProductContextRoleRespVO();
        roleRespVO.setRoleId(currentMember.getRoleId());
        if (currentRole != null) {
            roleRespVO.setRoleCode(currentRole.getCode());
            roleRespVO.setRoleName(currentRole.getName());
        }
        return roleRespVO;
    }

    private List<ProductContextNavRespVO> buildContextNavs(List<ObjectMenuRespDTO> menus) {
        if (menus.isEmpty()) {
            return Collections.emptyList();
        }

        List<ProductContextNavRespVO> navs = menus.stream()
                .filter(menu -> !MenuTypeEnum.BUTTON.getType().equals(menu.getType()))
                .filter(menu -> !Boolean.FALSE.equals(menu.getVisible()))
                .map(menu -> {
                    ProductContextNavRespVO nav = new ProductContextNavRespVO();
                    nav.setId(menu.getId());
                    nav.setName(menu.getName());
                    nav.setPath(menu.getPath());
                    nav.setIcon(menu.getIcon());
                    nav.setSort(menu.getSort());
                    return nav;
                })
                .collect(Collectors.toCollection(ArrayList::new));

        navs.sort(Comparator.comparing(ProductContextNavRespVO::getSort, Comparator.nullsLast(Integer::compareTo))
                .thenComparing(ProductContextNavRespVO::getId, Comparator.nullsLast(Long::compareTo)));
        return navs;
    }

    private List<String> buildContextButtons(List<ObjectMenuRespDTO> menus) {
        if (menus.isEmpty()) {
            return Collections.emptyList();
        }
        return menus.stream()
                .filter(menu -> MenuTypeEnum.BUTTON.getType().equals(menu.getType()))
                .map(ObjectMenuRespDTO::getPermission)
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .sorted()
                .collect(Collectors.toList());
    }

    private void writeProductStatusLog(ProductDO product, String actionType, String fromStatus,
                                       String toStatus, String reason) {
        ProductStatusLogDO statusLog = new ProductStatusLogDO();
        statusLog.setProductId(product.getId());
        statusLog.setActionType(actionType);
        statusLog.setFromStatus(fromStatus);
        statusLog.setToStatus(toStatus);
        statusLog.setReason(defaultText(reason));
        statusLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        statusLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        statusLog.setProductCodeSnapshot(product.getCode());
        statusLog.setProductNameSnapshot(product.getName());
        productStatusLogMapper.insert(statusLog);
    }

    private void writeBizAuditLog(ProductDO product, String actionType, String fromStatus, String toStatus,
                                  String fieldChanges, String reason) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(PRODUCT_OBJECT_TYPE);
        auditLog.setBizId(product.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 void writeMemberInitAuditLog(UserObjectRoleDO member) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(ObjectActivityConstants.MEMBER_BIZ_TYPE);
        auditLog.setBizId(member.getId());
        auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_ADD);
        auditLog.setFieldChanges(buildMemberFieldChanges(member));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private void writeManagerInitAuditLog(Long productId, Long managerUserId) {
        BizAuditLogDO auditLog = new BizAuditLogDO();
        auditLog.setBizType(PRODUCT_OBJECT_TYPE);
        auditLog.setBizId(productId);
        auditLog.setActionType(ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER);
        auditLog.setFieldChanges(buildManagerFieldChanges(null, managerUserId));
        auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
        auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
        bizAuditLogMapper.insert(auditLog);
    }

    private ProductDO cloneProduct(ProductDO source) {
        ProductDO target = new ProductDO();
        target.setId(source.getId());
        target.setCode(source.getCode());
        target.setDirectionCode(source.getDirectionCode());
        target.setStatusCode(source.getStatusCode());
        target.setName(source.getName());
        target.setManagerUserId(source.getManagerUserId());
        target.setDescription(source.getDescription());
        target.setLastStatusReason(source.getLastStatusReason());
        return target;
    }

    private void applyProductBaseInfoUpdate(ProductDO product, String directionCode, String name, String description) {
        validateProductEditable(product, directionCode, name);
        validateProductNameUnique(product.getId(), name);

        ProductDO before = cloneProduct(product);
        product.setDirectionCode(directionCode);
        product.setName(name.trim());
        product.setDescription(normalizeNullableText(description));
        productMapper.updateById(product);

        writeBizAuditLog(product, ObjectActivityConstants.PRODUCT_ACTION_UPDATE, before.getStatusCode(), product.getStatusCode(),
                buildProductFieldChanges(before, product), null);
    }

    private String buildProductFieldChanges(ProductDO before, ProductDO after) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "code", valueOf(before, ProductDO::getCode), valueOf(after, ProductDO::getCode));
        appendFieldChange(fieldChanges, "directionCode", valueOf(before, ProductDO::getDirectionCode),
                valueOf(after, ProductDO::getDirectionCode));
        appendFieldChange(fieldChanges, "statusCode", valueOf(before, ProductDO::getStatusCode),
                valueOf(after, ProductDO::getStatusCode));
        appendFieldChange(fieldChanges, "name", valueOf(before, ProductDO::getName), valueOf(after, ProductDO::getName));
        appendFieldChange(fieldChanges, "managerUserId", valueOf(before, ProductDO::getManagerUserId),
                valueOf(after, ProductDO::getManagerUserId));
        appendFieldChange(fieldChanges, "description", valueOf(before, ProductDO::getDescription),
                valueOf(after, ProductDO::getDescription));
        appendFieldChange(fieldChanges, "lastStatusReason", valueOf(before, ProductDO::getLastStatusReason),
                valueOf(after, ProductDO::getLastStatusReason));
        return fieldChanges.isEmpty() ? null : JsonUtils.toJsonString(fieldChanges);
    }

    private String buildMemberFieldChanges(UserObjectRoleDO member) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "userId", null, member.getUserId());
        appendFieldChange(fieldChanges, "roleId", null, member.getRoleId());
        appendFieldChange(fieldChanges, "status", null, member.getStatus());
        appendFieldChange(fieldChanges, "joinedTime", null, member.getJoinedTime());
        appendFieldChange(fieldChanges, "leftTime", null, member.getLeftTime());
        appendFieldChange(fieldChanges, "remark", null, member.getRemark());
        return JsonUtils.toJsonString(fieldChanges);
    }

    private String buildManagerFieldChanges(Long beforeManagerUserId, Long afterManagerUserId) {
        Map<String, Object> fieldChanges = new LinkedHashMap<>();
        appendFieldChange(fieldChanges, "managerUserId", beforeManagerUserId, afterManagerUserId);
        return JsonUtils.toJsonString(fieldChanges);
    }

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

    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 String normalizeNullableText(String value) {
        if (!StringUtils.hasText(value)) {
            return null;
        }
        return value.trim();
    }

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

}
