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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.PageUtils;
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO;
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.ProductStatusLogDO;
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.ProductStatusLogMapper;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
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.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

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

@Service
public class ProductActivityTimelineQueryService {

    /**
     * 成员名称在读取时间线时再通过缓存转换，避免把昵称快照写进动态记录。
     */
    private static final String TIMELINE_USER_NICKNAME_CACHE = "project_timeline_user_nickname#10m";
    private static final String TIMELINE_ROLE_NAME_CACHE = "project_timeline_role_name#10m";

    @Resource
    private ProductStatusLogMapper productStatusLogMapper;
    @Resource
    private BizAuditLogMapper bizAuditLogMapper;
    @Resource
    private UserObjectRoleMapper userObjectRoleMapper;
    @Resource
    private CacheManager cacheManager;
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private ObjectPermissionApi objectPermissionApi;

    public PageResult<ProductActivityTimelineRespVO> getProductActivityTimelinePage(
            Long productId, ProductActivityTimelinePageReqVO reqVO) {
        LocalDateTime[] timeRange = buildTimeRange(reqVO);
        List<String> actionTypes = normalizeActionTypes(reqVO.getActionTypes());
        List<ActivityItem> items = new ArrayList<>();

        appendStatusActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);
        appendProductActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);
        appendMemberActivities(productId, reqVO.getActivityType(), actionTypes, timeRange, items);

        items.sort(Comparator.comparing(ActivityItem::occurredAt, Comparator.nullsLast(LocalDateTime::compareTo))
                .thenComparing(ActivityItem::sourceId, Comparator.nullsLast(Long::compareTo))
                .reversed());
        PageResult<ProductActivityTimelineRespVO> pageResult =
                buildPageResult(items.stream().map(ActivityItem::respVO).toList(), reqVO);
        fillTargetUserNames(pageResult.getList());
        fillMemberRoleNames(pageResult.getList());
        return pageResult;
    }

    private void appendStatusActivities(Long productId, String activityType, List<String> actionTypes,
                                        LocalDateTime[] timeRange, List<ActivityItem> items) {
        if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_STATUS)) {
            return;
        }
        List<String> statusActions = actionTypes == null || actionTypes.isEmpty() ? null : actionTypes;
        productStatusLogMapper.selectListByProductIdAndActions(productId, statusActions, timeRange[0], timeRange[1])
                .forEach(log -> items.add(new ActivityItem(log.getId(), log.getCreateTime(), toStatusTimeline(log))));
    }

    private void appendProductActivities(Long productId, String activityType, List<String> actionTypes,
                                         LocalDateTime[] timeRange, List<ActivityItem> items) {
        if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT)) {
            return;
        }
        List<String> productActions = limitActions(actionTypes, ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES);
        if (shouldSkipByIntersection(actionTypes, productActions)) {
            return;
        }
        List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
                ProductObjectConstants.OBJECT_TYPE, productId, productActions, timeRange[0], timeRange[1]);
        Set<CreateSignature> createSignatures = buildCreateSignatures(productLogs);
        for (BizAuditLogDO log : productLogs) {
            if (ObjectActivityConstants.isStatusAction(log.getActionType())) {
                continue;
            }
            if (isCreateInitManagerNoise(log, createSignatures)) {
                continue;
            }
            if (!ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES.contains(trim(log.getActionType()))) {
                continue;
            }
            items.add(new ActivityItem(log.getId(), log.getCreateTime(), toProductTimeline(log)));
        }
    }

    private void appendMemberActivities(Long productId, String activityType, List<String> actionTypes,
                                        LocalDateTime[] timeRange, List<ActivityItem> items) {
        if (!includeType(activityType, ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
            return;
        }
        List<String> memberActions = limitActions(actionTypes, ObjectActivityConstants.MEMBER_TIMELINE_ACTION_TYPES);
        if (shouldSkipByIntersection(actionTypes, memberActions)) {
            return;
        }
        List<BizAuditLogDO> memberLogs = bizAuditLogMapper.selectListByBizTypeAndActions(
                ObjectActivityConstants.MEMBER_BIZ_TYPE, memberActions, timeRange[0], timeRange[1]);
        if (memberLogs.isEmpty()) {
            return;
        }
        Map<Long, UserObjectRoleDO> memberMap = loadMemberMap(productId, memberLogs);
        Set<CreateSignature> createSignatures = loadCreateSignatures(productId, timeRange);
        for (BizAuditLogDO log : memberLogs) {
            UserObjectRoleDO member = memberMap.get(log.getBizId());
            if (member == null) {
                continue;
            }
            if (!ObjectActivityConstants.MEMBER_TIMELINE_ACTION_TYPES.contains(trim(log.getActionType()))) {
                continue;
            }
            if (isCreateInitMemberNoise(log, createSignatures)) {
                continue;
            }
            items.add(new ActivityItem(log.getId(), log.getCreateTime(), toMemberTimeline(log, member)));
        }
    }

    private LocalDateTime[] buildTimeRange(ProductActivityTimelinePageReqVO reqVO) {
        if ((reqVO.getStartTime() == null) != (reqVO.getEndTime() == null)) {
            throw invalidParamException("开始时间和结束时间必须同时传入");
        }
        if (reqVO.getStartTime() == null) {
            LocalDateTime endTime = LocalDateTime.now();
            return new LocalDateTime[]{endTime.minusDays(30), endTime};
        }
        if (reqVO.getStartTime().isAfter(reqVO.getEndTime())) {
            throw invalidParamException("开始时间不能晚于结束时间");
        }
        return new LocalDateTime[]{reqVO.getStartTime(), reqVO.getEndTime()};
    }

    private List<String> normalizeActionTypes(List<String> actionTypes) {
        if (actionTypes == null || actionTypes.isEmpty()) {
            return null;
        }
        List<String> normalized = actionTypes.stream()
                .filter(StringUtils::hasText)
                .map(String::trim)
                .distinct()
                .toList();
        return normalized.isEmpty() ? null : normalized;
    }

    private List<String> limitActions(List<String> actionTypes, List<String> allowedActions) {
        if (actionTypes == null || actionTypes.isEmpty()) {
            return allowedActions;
        }
        return actionTypes.stream()
                .filter(allowedActions::contains)
                .distinct()
                .toList();
    }

    private boolean shouldSkipByIntersection(List<String> actualActions, List<String> limitedActions) {
        return actualActions != null && actualActions.size() > 0 && limitedActions.isEmpty();
    }

    private boolean includeType(String actual, String expected) {
        return !StringUtils.hasText(actual) || Objects.equals(actual.trim(), expected);
    }

    private Map<Long, UserObjectRoleDO> loadMemberMap(Long productId, List<BizAuditLogDO> memberLogs) {
        List<Long> memberIds = memberLogs.stream()
                .map(BizAuditLogDO::getBizId)
                .filter(Objects::nonNull)
                .distinct()
                .toList();
        if (memberIds.isEmpty()) {
            return Map.of();
        }
        Map<Long, UserObjectRoleDO> memberMap = new LinkedHashMap<>();
        for (UserObjectRoleDO member : userObjectRoleMapper.selectListByIdsAndObject(
                memberIds, ProductObjectConstants.OBJECT_TYPE, productId)) {
            memberMap.put(member.getId(), member);
        }
        return memberMap;
    }

    private Set<CreateSignature> loadCreateSignatures(Long productId, LocalDateTime[] timeRange) {
        List<BizAuditLogDO> productLogs = bizAuditLogMapper.selectListByBizAndActions(
                ProductObjectConstants.OBJECT_TYPE, productId,
                ObjectActivityConstants.PRODUCT_TIMELINE_ACTION_TYPES, timeRange[0], timeRange[1]);
        return buildCreateSignatures(productLogs);
    }

    private Set<CreateSignature> buildCreateSignatures(List<BizAuditLogDO> productLogs) {
        Set<CreateSignature> signatures = new LinkedHashSet<>();
        for (BizAuditLogDO log : productLogs) {
            if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.PRODUCT_ACTION_CREATE)) {
                continue;
            }
            Long managerUserId = getFieldChangeLong(log.getFieldChanges(), "managerUserId", "after");
            if (managerUserId == null || log.getCreateTime() == null) {
                continue;
            }
            signatures.add(new CreateSignature(log.getCreateTime(), managerUserId));
        }
        return signatures;
    }

    private boolean isCreateInitManagerNoise(BizAuditLogDO log, Set<CreateSignature> createSignatures) {
        if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.PRODUCT_ACTION_CHANGE_MANAGER)) {
            return false;
        }
        if (getFieldChangeLong(log.getFieldChanges(), "managerUserId", "before") != null) {
            return false;
        }
        Long managerUserId = getFieldChangeLong(log.getFieldChanges(), "managerUserId", "after");
        return managerUserId != null && log.getCreateTime() != null
                && createSignatures.contains(new CreateSignature(log.getCreateTime(), managerUserId));
    }

    private boolean isCreateInitMemberNoise(BizAuditLogDO log, Set<CreateSignature> createSignatures) {
        if (!Objects.equals(trim(log.getActionType()), ObjectActivityConstants.MEMBER_ACTION_ADD)) {
            return false;
        }
        if (getFieldChangeLong(log.getFieldChanges(), "userId", "before") != null) {
            return false;
        }
        Long userId = getFieldChangeLong(log.getFieldChanges(), "userId", "after");
        return userId != null && log.getCreateTime() != null
                && createSignatures.contains(new CreateSignature(log.getCreateTime(), userId));
    }

    private Long getFieldChangeLong(String fieldChanges, String fieldName, String valueField) {
        JsonNode valueNode = getFieldChangeNode(fieldChanges, fieldName, valueField);
        if (valueNode == null || valueNode.isNull()) {
            return null;
        }
        if (valueNode.isNumber()) {
            return valueNode.longValue();
        }
        if (valueNode.isTextual() && StringUtils.hasText(valueNode.textValue())) {
            return Long.valueOf(valueNode.textValue().trim());
        }
        return null;
    }

    private JsonNode getFieldChangeNode(String fieldChanges, String fieldName, String valueField) {
        if (!StringUtils.hasText(fieldChanges) || !JsonUtils.isJsonObject(fieldChanges)) {
            return null;
        }
        JsonNode fieldNode = JsonUtils.parseTree(fieldChanges).path(fieldName);
        if (fieldNode.isMissingNode()) {
            return null;
        }
        JsonNode valueNode = fieldNode.path(valueField);
        return valueNode.isMissingNode() ? null : valueNode;
    }

    private PageResult<ProductActivityTimelineRespVO> buildPageResult(List<ProductActivityTimelineRespVO> activities,
                                                                      ProductActivityTimelinePageReqVO reqVO) {
        if (activities.isEmpty()) {
            return PageResult.empty();
        }
        int start = PageUtils.getStart(reqVO);
        if (start >= activities.size()) {
            return PageResult.empty((long) activities.size());
        }
        int end = Math.min(start + reqVO.getPageSize(), activities.size());
        return new PageResult<>(activities.subList(start, end), (long) activities.size());
    }

    private void fillTargetUserNames(List<ProductActivityTimelineRespVO> activities) {
        if (activities == null || activities.isEmpty()) {
            return;
        }
        Set<Long> userIds = new LinkedHashSet<>();
        for (ProductActivityTimelineRespVO activity : activities) {
            if (activity != null && activity.getTargetUserId() != null) {
                userIds.add(activity.getTargetUserId());
            }
        }
        if (userIds.isEmpty()) {
            return;
        }
        Map<Long, String> nicknameMap = loadUserNicknameMap(userIds);
        for (ProductActivityTimelineRespVO activity : activities) {
            if (activity == null || activity.getTargetUserId() == null) {
                continue;
            }
            activity.setTargetUserName(nicknameMap.get(activity.getTargetUserId()));
        }
    }

    private void fillMemberRoleNames(List<ProductActivityTimelineRespVO> activities) {
        if (activities == null || activities.isEmpty()) {
            return;
        }
        Set<Long> roleIds = new LinkedHashSet<>();
        for (ProductActivityTimelineRespVO activity : activities) {
            if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
                continue;
            }
            Long beforeRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "before");
            Long afterRoleId = getFieldChangeLong(activity.getDetails(), "roleId", "after");
            if (beforeRoleId != null) {
                roleIds.add(beforeRoleId);
            }
            if (afterRoleId != null) {
                roleIds.add(afterRoleId);
            }
        }
        if (roleIds.isEmpty()) {
            return;
        }
        Map<Long, String> roleNameMap = loadRoleNameMap(roleIds);
        for (ProductActivityTimelineRespVO activity : activities) {
            if (!Objects.equals(activity.getType(), ObjectActivityConstants.ACTIVITY_TYPE_MEMBER)) {
                continue;
            }
            activity.setDetails(appendRoleNames(activity.getDetails(), roleNameMap));
        }
    }

    private Map<Long, String> loadUserNicknameMap(Set<Long> userIds) {
        Map<Long, String> nicknameMap = new LinkedHashMap<>();
        if (userIds == null || userIds.isEmpty()) {
            return nicknameMap;
        }
        Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_USER_NICKNAME_CACHE);
        Set<Long> missIds = new LinkedHashSet<>();
        for (Long userId : userIds) {
            String nickname = cache == null ? null : cache.get(userId, String.class);
            if (nickname != null) {
                nicknameMap.put(userId, nickname);
            } else {
                missIds.add(userId);
            }
        }
        if (missIds.isEmpty()) {
            return nicknameMap;
        }
        Map<Long, AdminUserRespDTO> userMap = adminUserApi == null ? Map.of() : adminUserApi.getUserMap(missIds);
        if (userMap == null || userMap.isEmpty()) {
            return nicknameMap;
        }
        for (Long userId : missIds) {
            AdminUserRespDTO user = userMap.get(userId);
            if (user == null) {
                continue;
            }
            nicknameMap.put(userId, user.getNickname());
            if (cache != null && user.getNickname() != null) {
                cache.put(userId, user.getNickname());
            }
        }
        return nicknameMap;
    }

    private Map<Long, String> loadRoleNameMap(Set<Long> roleIds) {
        Map<Long, String> roleNameMap = new LinkedHashMap<>();
        if (roleIds == null || roleIds.isEmpty()) {
            return roleNameMap;
        }
        Cache cache = cacheManager == null ? null : cacheManager.getCache(TIMELINE_ROLE_NAME_CACHE);
        Set<Long> missIds = new LinkedHashSet<>();
        for (Long roleId : roleIds) {
            String roleName = cache == null ? null : cache.get(roleId, String.class);
            if (roleName != null) {
                roleNameMap.put(roleId, roleName);
            } else {
                missIds.add(roleId);
            }
        }
        if (missIds.isEmpty()) {
            return roleNameMap;
        }
        Map<Long, ObjectRoleRespDTO> roleMap = objectPermissionApi == null ? Map.of()
                : objectPermissionApi.getObjectRoleMap(missIds, ObjectRoleConstants.ROLE_SCOPE_OBJECT,
                        ProductObjectConstants.OBJECT_TYPE);
        if (roleMap == null || roleMap.isEmpty()) {
            return roleNameMap;
        }
        for (Long roleId : missIds) {
            ObjectRoleRespDTO role = roleMap.get(roleId);
            if (role == null || !StringUtils.hasText(role.getName())) {
                continue;
            }
            roleNameMap.put(roleId, role.getName());
            if (cache != null) {
                cache.put(roleId, role.getName());
            }
        }
        return roleNameMap;
    }

    private String appendRoleNames(String details, Map<Long, String> roleNameMap) {
        if (!StringUtils.hasText(details) || !JsonUtils.isJsonObject(details)) {
            return details;
        }
        Long beforeRoleId = getFieldChangeLong(details, "roleId", "before");
        Long afterRoleId = getFieldChangeLong(details, "roleId", "after");
        if (beforeRoleId == null && afterRoleId == null) {
            return details;
        }
        JsonNode detailsNode = JsonUtils.parseTree(details);
        if (!(detailsNode instanceof ObjectNode)) {
            return details;
        }
        ObjectNode objectNode = ((ObjectNode) detailsNode).deepCopy();
        ObjectNode roleNameNode = objectNode.putObject("roleName");
        appendRoleName(roleNameNode, "before", beforeRoleId, roleNameMap);
        appendRoleName(roleNameNode, "after", afterRoleId, roleNameMap);
        return JsonUtils.toJsonString(objectNode);
    }

    private void appendRoleName(ObjectNode roleNameNode, String fieldName, Long roleId, Map<Long, String> roleNameMap) {
        if (roleId == null) {
            roleNameNode.putNull(fieldName);
            return;
        }
        String roleName = roleNameMap.get(roleId);
        if (StringUtils.hasText(roleName)) {
            roleNameNode.put(fieldName, roleName);
            return;
        }
        roleNameNode.putNull(fieldName);
    }

    private ProductActivityTimelineRespVO toStatusTimeline(ProductStatusLogDO log) {
        ProductActivityTimelineRespVO respVO = new ProductActivityTimelineRespVO();
        respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_STATUS + ":" + log.getId());
        respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_STATUS);
        respVO.setActionType(log.getActionType());
        respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
        respVO.setOperatorUserId(log.getOperatorUserId());
        respVO.setOperatorName(log.getOperatorName());
        respVO.setOccurredAt(log.getCreateTime());
        respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
        respVO.setReason(log.getReason());
        respVO.setFromStatus(log.getFromStatus());
        respVO.setToStatus(log.getToStatus());
        respVO.setDetails(JsonUtils.toJsonString(buildStatusDetails(log)));
        return respVO;
    }

    private ProductActivityTimelineRespVO toProductTimeline(BizAuditLogDO log) {
        ProductActivityTimelineRespVO respVO = new ProductActivityTimelineRespVO();
        respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT + ":" + log.getId());
        respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_PRODUCT);
        respVO.setActionType(log.getActionType());
        respVO.setActionName(ObjectActivityConstants.resolveActionName(log.getActionType()));
        respVO.setOperatorUserId(log.getOperatorUserId());
        respVO.setOperatorName(log.getOperatorName());
        respVO.setOccurredAt(log.getCreateTime());
        respVO.setSummary(buildSummary(log.getOperatorName(), respVO.getActionName(), log.getReason()));
        respVO.setReason(log.getReason());
        respVO.setFromStatus(log.getFromStatus());
        respVO.setToStatus(log.getToStatus());
        respVO.setDetails(log.getFieldChanges());
        return respVO;
    }

    private ProductActivityTimelineRespVO toMemberTimeline(BizAuditLogDO log, UserObjectRoleDO member) {
        ProductActivityTimelineRespVO respVO = toProductTimeline(log);
        respVO.setId(ObjectActivityConstants.ACTIVITY_TYPE_MEMBER + ":" + log.getId());
        respVO.setType(ObjectActivityConstants.ACTIVITY_TYPE_MEMBER);
        respVO.setTargetUserId(member == null ? null : member.getUserId());
        return respVO;
    }

    private Map<String, Object> buildStatusDetails(ProductStatusLogDO log) {
        Map<String, Object> details = new LinkedHashMap<>();
        details.put("productCodeSnapshot", log.getProductCodeSnapshot());
        details.put("productNameSnapshot", log.getProductNameSnapshot());
        return details;
    }

    private String buildSummary(String operatorName, String actionName, String reason) {
        String actualOperatorName = StringUtils.hasText(operatorName) ? operatorName : "系统";
        if (StringUtils.hasText(reason)) {
            return String.format("%s执行了【%s】：%s", actualOperatorName, actionName, reason);
        }
        return String.format("%s执行了【%s】", actualOperatorName, actionName);
    }

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

    private record CreateSignature(LocalDateTime occurredAt, Long userId) {
    }

    private record ActivityItem(Long sourceId, LocalDateTime occurredAt, ProductActivityTimelineRespVO respVO) {
    }

}
