package com.writeoff.module.project.service;

import com.writeoff.common.api.PageResult;
import com.writeoff.common.exception.BusinessException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.writeoff.module.project.dto.CreateProjectRequest;
import com.writeoff.module.project.dto.SaveProjectBindingsRequest;
import com.writeoff.module.project.model.Project;
import com.writeoff.module.project.model.ProjectStatus;
import com.writeoff.module.project.repository.ProjectRepository;
import com.writeoff.module.system.model.BizChangeLogInfo;
import com.writeoff.module.system.service.BizChangeLogService;
import com.writeoff.module.system.service.DataPermissionService;
import com.writeoff.module.system.service.EnterpriseService;
import com.writeoff.security.PermissionService;
import com.writeoff.security.AuthContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class ProjectService {
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final ProjectRepository projectRepository;
    private final DataPermissionService dataPermissionService;
    private final EnterpriseService enterpriseService;
    private final JdbcTemplate jdbcTemplate;
    private final PermissionService permissionService;
    private final BizChangeLogService bizChangeLogService;

    @Autowired
    public ProjectService(ProjectRepository projectRepository,
                          DataPermissionService dataPermissionService,
                          EnterpriseService enterpriseService,
                          JdbcTemplate jdbcTemplate,
                          PermissionService permissionService,
                          BizChangeLogService bizChangeLogService) {
        this.projectRepository = projectRepository;
        this.dataPermissionService = dataPermissionService;
        this.enterpriseService = enterpriseService;
        this.jdbcTemplate = jdbcTemplate;
        this.permissionService = permissionService;
        this.bizChangeLogService = bizChangeLogService;
    }

    public ProjectService(ProjectRepository projectRepository) {
        this(projectRepository, null, null, null, null, null);
    }

    public PageResult<Project> list(Boolean parentOnly, Boolean includeDeleted) {
        List<Project> list = projectRepository.findAll(Boolean.TRUE.equals(includeDeleted));
        if (Boolean.TRUE.equals(parentOnly)) {
            list = list.stream()
                    .filter(project -> project.getParentProjectId() == null)
                    .collect(Collectors.toList());
        }
        if (dataPermissionService != null) {
            DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
            final Map<Long, Long> creatorMap = scope.isProjectOwnerOnly()
                    ? dataPermissionService.listProjectCreators(list.stream().map(Project::getId).collect(Collectors.toCollection(HashSet::new)))
                    : new LinkedHashMap<Long, Long>();
            list = list.stream()
                    .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
                    .collect(Collectors.toList());
        }
        return new PageResult<>(list, list.size(), 1, 20);
    }

    public List<Project> listChildren(Long parentProjectId) {
        getById(parentProjectId);
        List<Project> children = projectRepository.findByParentProjectId(parentProjectId, false);
        if (dataPermissionService != null) {
            DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
            final Map<Long, Long> creatorMap = scope.isProjectOwnerOnly()
                    ? dataPermissionService.listProjectCreators(children.stream().map(Project::getId).collect(Collectors.toCollection(HashSet::new)))
                    : new LinkedHashMap<Long, Long>();
            children = children.stream()
                    .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
                    .collect(Collectors.toList());
        }
        return children;
    }

    public List<Project> listChildren(Long parentProjectId, Boolean includeDeleted) {
        getById(parentProjectId);
        List<Project> children = projectRepository.findByParentProjectId(parentProjectId, Boolean.TRUE.equals(includeDeleted));
        if (dataPermissionService != null) {
            DataPermissionService.DataScope scope = dataPermissionService.resolveCurrentUserScope();
            final Map<Long, Long> creatorMap = scope.isProjectOwnerOnly()
                    ? dataPermissionService.listProjectCreators(children.stream().map(Project::getId).collect(Collectors.toCollection(HashSet::new)))
                    : new LinkedHashMap<Long, Long>();
            children = children.stream()
                    .filter(project -> dataPermissionService.canAccessProject(project.getId(), creatorMap.get(project.getId()), scope))
                    .collect(Collectors.toList());
        }
        return children;
    }

    public Project create(CreateProjectRequest request) {
        if (enterpriseService != null) {
            enterpriseService.assertEnabled(request.getPartnerEnterpriseId());
        }
        // 项目周期合法性校验：结束日期不能早于开始日期。
        if (request.getStartDate() != null && request.getEndDate() != null
                && request.getEndDate().isBefore(request.getStartDate())) {
            throw new BusinessException(10001, "项目结束日期不能早于开始日期");
        }
        if (request.getParentProjectId() != null) {
            getById(request.getParentProjectId());
        }
        ProjectFeeSummary projectFee = buildProjectFeeSummary(request.getProjectFeeJson());
        double budgetExecutionRatio = calculateBudgetExecutionRatio(request.getBudgetCent(), projectFee.totalCent);
        assertProjectBudgetConstraint(
                request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
                request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
                budgetExecutionRatio,
                request.getBudgetCent(),
                projectFee.totalCent
        );
        Project project = new Project(
                null,
                request.getName(),
                null,
                request.getStartDate(),
                request.getEndDate(),
                request.getPartnerEnterpriseId(),
                null,
                null,
                request.getPartnerEnterpriseId(),
                null,
                null,
                null,
                null,
                request.getBudgetCent(),
                request.getMeetingTotal(),
                0,
                request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
                request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
                request.getOverBudgetApprovalChainJson(),
                budgetExecutionRatio,
                null,

                request.getMeetingTotal(),
                0,
                0,
                request.getLaborFeeRatio() == null ? 0d : request.getLaborFeeRatio(),
                request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
                request.getInvoiceInfo(),
                request.getExpenseRatioJson(),
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                null,
                ProjectStatus.WAITING
        );
        project.setParentProjectId(request.getParentProjectId());
        project.setHostEnterpriseName(resolveCurrentTenantName());
        project.setProjectFeeJson(projectFee.normalizedJson);
        Project saved = projectRepository.save(project);
        logProjectCreate(saved);
        return saved;
    }

    public Project update(Long projectId, CreateProjectRequest request) {
        Project existing = getById(projectId);
        if (enterpriseService != null && request.getPartnerEnterpriseId() != null) {
            enterpriseService.assertEnabled(request.getPartnerEnterpriseId());
        }
        if (request.getStartDate() != null && request.getEndDate() != null
                && request.getEndDate().isBefore(request.getStartDate())) {
            throw new BusinessException(10001, "项目结束日期不能早于开始日期");
        }
        String requestProjectFeeJson = request.getProjectFeeJson();
        String sourceProjectFeeJson = (requestProjectFeeJson == null || requestProjectFeeJson.trim().isEmpty())
                ? existing.getProjectFeeJson()
                : requestProjectFeeJson;
        ProjectFeeSummary projectFee = buildProjectFeeSummary(sourceProjectFeeJson);
        double budgetExecutionRatio = calculateBudgetExecutionRatio(request.getBudgetCent(), projectFee.totalCent);
        assertProjectBudgetConstraint(
                request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
                request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
                budgetExecutionRatio,
                request.getBudgetCent(),
                projectFee.totalCent
        );
        Project project = new Project(
                existing.getId(),
                request.getName(),
                null,
                request.getStartDate(),
                request.getEndDate(),
                request.getPartnerEnterpriseId(),
                existing.getEnterpriseName(),
                null,
                request.getPartnerEnterpriseId(),
                null,
                null,
                null,
                null,
                request.getBudgetCent(),
                request.getMeetingTotal(),
                existing.getMeetingCompletedCount(),
                request.getAllowMeetingOverBudget() != null && request.getAllowMeetingOverBudget(),
                request.getOverBudgetThresholdRatio() == null ? 0.1d : request.getOverBudgetThresholdRatio(),
                request.getOverBudgetApprovalChainJson(),
                budgetExecutionRatio,
                existing.getRiskFlagsJson(),

                existing.getWriteOffNotStartedCount(),
                existing.getWriteOffInProgressCount(),
                existing.getWriteOffCompletedCount(),
                request.getLaborFeeRatio() == null ? existing.getLaborFeeRatio() : request.getLaborFeeRatio(),
                request.getAllowProjectOverBudget() != null && request.getAllowProjectOverBudget(),
                request.getInvoiceInfo(),
                request.getExpenseRatioJson(),
                null,
                null,
                null,
                null,
                existing.getTerminatedReason(),
                existing.getFreezeReason(),
                existing.getArchivedAt(),
                existing.getKeyChangeLogJson(),
                existing.getStatus()
        );
        project.setParentProjectId(request.getParentProjectId() == null ? existing.getParentProjectId() : request.getParentProjectId());
        project.setHostEnterpriseName(resolveCurrentTenantName());
        project.setProjectFeeJson(projectFee.normalizedJson);
        Project saved = projectRepository.save(project);
        logProjectUpdate(existing, saved);
        return saved;
    }

    public Project freeze(Long projectId, String reason) {
        Project project = projectRepository.findById(projectId)
                .orElseThrow(() -> new BusinessException(10003, "项目不存在"));
        if (reason == null || reason.trim().isEmpty()) {
            throw new BusinessException(10001, "冻结原因不能为空");
        }
        project.setStatus(ProjectStatus.FROZEN);
        // 冻结原因需要与状态一起落库，满足风控留痕要求。
        project.setFreezeReason(reason.trim());
        Project saved = projectRepository.save(project);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("PROJECT", saved.getId(), "PROJECT_FREEZE", reason.trim());
        }
        return saved;
    }

    public Project getById(Long projectId) {
        return projectRepository.findById(projectId)
                .orElseThrow(() -> new BusinessException(10003, "项目不存在"));
    }

    public Project unfreeze(Long projectId, String reason) {
        Project project = getById(projectId);
        if (project.getStatus() != ProjectStatus.FROZEN) {
            throw new BusinessException(30001, "仅冻结状态的项目允许解冻");
        }
        if (reason == null || reason.trim().isEmpty()) {
            throw new BusinessException(10001, "解冻原因不能为空");
        }
        project.setStatus(ProjectStatus.IN_PROGRESS);
        project.setFreezeReason(null);
        Project saved = projectRepository.save(project);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("PROJECT", saved.getId(), "PROJECT_UNFREEZE", reason.trim());
        }
        return saved;
    }

    public Project archive(Long projectId) {
        Project project = getById(projectId);
        if (project.getStatus() != ProjectStatus.COMPLETED && project.getStatus() != ProjectStatus.FROZEN) {
            throw new BusinessException(30001, "仅已完成或已冻结的项目允许归档");
        }
        project.setStatus(ProjectStatus.ARCHIVED);
        project.setArchivedAt(java.time.LocalDateTime.now());
        Project saved = projectRepository.save(project);
        if (bizChangeLogService != null) {
            bizChangeLogService.logAction("PROJECT", saved.getId(), "PROJECT_ARCHIVE", null);
        }
        return saved;
    }

    public void markInProgress(Long projectId) {
        Project project = getById(projectId);
        if (project.getStatus() == ProjectStatus.WAITING) {
            project.setStatus(ProjectStatus.IN_PROGRESS);
            projectRepository.save(project);
        }
    }

    public Map<String, Object> listBindingCandidates() {
        ensureJdbcEnabled();
        boolean projectExecutorMode = isCurrentUserProjectExecutor();
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("ownerUsers", projectExecutorMode ? new ArrayList<Map<String, Object>>() : listUsersByRoleCode("PROJECT_OWNER"));
        result.put("executorUsers", projectExecutorMode ? new ArrayList<Map<String, Object>>() : listUsersByRoleCode("PROJECT_EXECUTOR"));
        result.put("legacyExecutorUsers", (hasExecutorBindingPermission() || projectExecutorMode) ? listUsersByRoleCode("EXECUTOR") : new ArrayList<Map<String, Object>>());
        return result;
    }

    public Map<String, Object> getBindings(Long projectId) {
        ensureJdbcEnabled();
        getById(projectId);
        boolean projectExecutorMode = isCurrentUserProjectExecutor();
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("ownerUsers", projectExecutorMode ? new ArrayList<Map<String, Object>>() : listProjectBoundUsers(projectId, "PROJECT_OWNER"));
        result.put("executorUsers", projectExecutorMode ? new ArrayList<Map<String, Object>>() : listProjectBoundUsers(projectId, "PROJECT_EXECUTOR"));
        result.put("legacyExecutorUsers", (hasExecutorBindingPermission() || projectExecutorMode) ? listProjectBoundUsers(projectId, "EXECUTOR") : new ArrayList<Map<String, Object>>());
        return result;
    }

    public void saveBindings(Long projectId, SaveProjectBindingsRequest request) {
        ensureJdbcEnabled();
        getById(projectId);
        boolean projectExecutorMode = isCurrentUserProjectExecutor();
        List<Map<String, Object>> beforeOwnerUsers = listProjectBoundUsers(projectId, "PROJECT_OWNER");
        List<Map<String, Object>> beforeExecutorUsers = listProjectBoundUsers(projectId, "PROJECT_EXECUTOR");
        List<Map<String, Object>> beforeLegacyExecutorUsers = listProjectBoundUsers(projectId, "EXECUTOR");
        List<Long> ownerUserIds = request.getOwnerUserIds() == null ? new ArrayList<Long>() : request.getOwnerUserIds();
        List<Long> executorUserIds = request.getExecutorUserIds() == null ? new ArrayList<Long>() : request.getExecutorUserIds();
        List<Long> legacyExecutorUserIds = request.getLegacyExecutorUserIds() == null ? new ArrayList<Long>() : request.getLegacyExecutorUserIds();
        boolean canBindLegacyExecutor = hasExecutorBindingPermission() || projectExecutorMode;
        if (projectExecutorMode) {
            ownerUserIds = listProjectBoundUserIds(projectId, "PROJECT_OWNER");
            executorUserIds = listProjectBoundUserIds(projectId, "PROJECT_EXECUTOR");
        }
        if (!canBindLegacyExecutor && !legacyExecutorUserIds.isEmpty()) {
            throw new BusinessException(10001, "无 project.bind.executor_user 权限");
        }
        for (Long userId : ownerUserIds) {
            assertUserHasRole(userId, "PROJECT_OWNER");
        }
        for (Long userId : executorUserIds) {
            assertUserHasRole(userId, "PROJECT_EXECUTOR");
        }
        if (canBindLegacyExecutor) {
            for (Long userId : legacyExecutorUserIds) {
                assertUserHasRole(userId, "EXECUTOR");
            }
        } else {
            legacyExecutorUserIds = listProjectBoundUserIds(projectId, "EXECUTOR");
        }
        jdbcTemplate.update(
                "DELETE FROM project_user_binding WHERE tenant_id=? AND project_id=?",
                tenantId(),
                projectId
        );
        for (Long userId : ownerUserIds) {
            insertBinding(projectId, userId, "PROJECT_OWNER");
        }
        for (Long userId : executorUserIds) {
            insertBinding(projectId, userId, "PROJECT_EXECUTOR");
        }
        for (Long userId : legacyExecutorUserIds) {
            insertBinding(projectId, userId, "EXECUTOR");
        }
        logProjectBindingChanges(projectId, beforeOwnerUsers, beforeExecutorUsers, beforeLegacyExecutorUsers, ownerUserIds, executorUserIds, legacyExecutorUserIds);
    }

    public List<Map<String, Object>> listKeyChangeLogs(Long projectId) {
        getById(projectId);
        if (bizChangeLogService == null) {
            return new ArrayList<Map<String, Object>>();
        }
        return bizChangeLogService.listByBiz("PROJECT", projectId).stream()
                .map(this::toProjectChangeLogRow)
                .collect(Collectors.toList());
    }

    private List<Map<String, Object>> listUsersByRoleCode(String roleCode) {
        return jdbcTemplate.query(
                "SELECT DISTINCT u.id AS user_id, u.user_name, u.phone, u.created_by " +
                        "FROM sys_user u " +
                        "JOIN user_role ur ON u.tenant_id=ur.tenant_id AND u.id=ur.user_id " +
                        "JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id " +
                        "WHERE u.tenant_id=? AND u.is_deleted=0 AND u.status='ENABLED' AND r.role_code=? " +
                        "ORDER BY u.id DESC",
                (rs, n) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("userId", rs.getLong("user_id"));
                    row.put("userName", rs.getString("user_name"));
                    row.put("phone", rs.getString("phone"));
                    row.put("createdBy", rs.getLong("created_by"));
                    return row;
                },
                tenantId(),
                roleCode
        );
    }

    private List<Map<String, Object>> listProjectBoundUsers(Long projectId, String roleCode) {
        return jdbcTemplate.query(
                "SELECT b.user_id, u.user_name, u.phone, u.created_by " +
                        "FROM project_user_binding b " +
                        "JOIN sys_user u ON b.tenant_id=u.tenant_id AND b.user_id=u.id " +
                        "WHERE b.tenant_id=? AND b.project_id=? AND b.bind_role_code=? AND b.is_deleted=0 AND u.is_deleted=0 " +
                        "ORDER BY b.id DESC",
                (rs, n) -> {
                    Map<String, Object> row = new LinkedHashMap<>();
                    row.put("userId", rs.getLong("user_id"));
                    row.put("userName", rs.getString("user_name"));
                    row.put("phone", rs.getString("phone"));
                    row.put("createdBy", rs.getLong("created_by"));
                    return row;
                },
                tenantId(),
                projectId,
                roleCode
        );
    }

    private List<Long> listProjectBoundUserIds(Long projectId, String roleCode) {
        return jdbcTemplate.queryForList(
                "SELECT user_id FROM project_user_binding WHERE tenant_id=? AND project_id=? AND bind_role_code=? AND is_deleted=0",
                Long.class,
                tenantId(),
                projectId,
                roleCode
        );
    }

    private void assertUserHasRole(Long userId, String roleCode) {
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) " +
                        "FROM user_role ur " +
                        "JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id " +
                        "WHERE ur.tenant_id=? AND ur.user_id=? AND r.role_code=?",
                Integer.class,
                tenantId(),
                userId,
                roleCode
        );
        if (count == null || count == 0) {
            throw new BusinessException(10001, "用户未分配角色: " + roleCode);
        }
    }

    private void insertBinding(Long projectId, Long userId, String roleCode) {
        jdbcTemplate.update(
                "INSERT INTO project_user_binding (tenant_id, project_id, user_id, bind_role_code, is_deleted, created_by, updated_by) " +
                        "VALUES (?, ?, ?, ?, 0, 0, 0)",
                tenantId(),
                projectId,
                userId,
                roleCode
        );
    }

    private void ensureJdbcEnabled() {
        if (jdbcTemplate == null) {
            throw new BusinessException(10001, "当前仓储模式不支持项目人员绑定");
        }
    }

    private boolean hasExecutorBindingPermission() {
        Long userId = AuthContext.userId();
        return permissionService != null && userId != null && permissionService.hasPermission(userId, "project.bind.executor_user");
    }

    private boolean isCurrentUserProjectExecutor() {
        Long userId = AuthContext.userId();
        if (userId == null) {
            return false;
        }
        Integer count = jdbcTemplate.queryForObject(
                "SELECT COUNT(1) " +
                        "FROM user_role ur " +
                        "JOIN role r ON ur.tenant_id=r.tenant_id AND ur.role_id=r.id " +
                        "WHERE ur.tenant_id=? AND ur.user_id=? AND r.role_code='PROJECT_EXECUTOR' AND r.is_deleted=0",
                Integer.class,
                tenantId(),
                userId
        );
        return count != null && count > 0;
    }

    private Long tenantId() {
        return AuthContext.requireTenantId();
    }

    private String resolveCurrentTenantName() {
        if (jdbcTemplate == null) {
            return null;
        }
        List<String> names = jdbcTemplate.query(
                "SELECT tenant_name FROM tenant WHERE id=?",
                (rs, n) -> rs.getString("tenant_name"),
                tenantId()
        );
        return names.isEmpty() ? null : names.get(0);
    }

    private ProjectFeeSummary buildProjectFeeSummary(String rawProjectFeeJson) {
        ObjectNode root;
        try {
            if (rawProjectFeeJson == null || rawProjectFeeJson.trim().isEmpty()) {
                root = objectMapper.createObjectNode();
            } else {
                JsonNode parsed = objectMapper.readTree(rawProjectFeeJson);
                root = parsed != null && parsed.isObject() ? (ObjectNode) parsed : objectMapper.createObjectNode();
            }
        } catch (Exception e) {
            throw new BusinessException(10001, "项目费用配置格式不正确");
        }

        long managementFeeCent = readNonNegativeLong(root, "managementFeeCent");
        long taxFeeCent = readNonNegativeLong(root, "taxFeeCent");
        long paidAmountCent = readNonNegativeLong(root, "paidAmountCent");

        ArrayNode customFeesNode = objectMapper.createArrayNode();
        long customTotalCent = 0L;
        JsonNode customFees = root.get("customFees");
        if (customFees != null && customFees.isArray()) {
            for (JsonNode feeNode : customFees) {
                if (feeNode == null || !feeNode.isObject()) {
                    continue;
                }
                String name = feeNode.path("name").asText("").trim();
                if (name.isEmpty()) {
                    continue;
                }
                long amountCent = readNonNegativeLong(feeNode, "amountCent");
                String remark = feeNode.path("remark").asText("");
                ObjectNode normalizedFee = objectMapper.createObjectNode();
                normalizedFee.put("name", name);
                normalizedFee.put("amountCent", amountCent);
                normalizedFee.put("remark", remark);
                customFeesNode.add(normalizedFee);
                customTotalCent += amountCent;
            }
        }

        ObjectNode normalizedRoot = objectMapper.createObjectNode();
        normalizedRoot.put("managementFeeCent", managementFeeCent);
        normalizedRoot.put("taxFeeCent", taxFeeCent);
        normalizedRoot.put("paidAmountCent", paidAmountCent);
        normalizedRoot.set("customFees", customFeesNode);

        try {
            return new ProjectFeeSummary(
                    objectMapper.writeValueAsString(normalizedRoot),
                    managementFeeCent + taxFeeCent + paidAmountCent + customTotalCent
            );
        } catch (Exception e) {
            throw new BusinessException(10001, "项目费用配置序列化失败");
        }
    }

    private long readNonNegativeLong(JsonNode node, String fieldName) {
        JsonNode valueNode = node.get(fieldName);
        if (valueNode == null || valueNode.isNull()) {
            return 0L;
        }
        if (!valueNode.isNumber()) {
            throw new BusinessException(10001, "项目费用字段格式不正确: " + fieldName);
        }
        long value = valueNode.asLong();
        if (value < 0L) {
            throw new BusinessException(10001, "项目费用字段不能为负数: " + fieldName);
        }
        return value;
    }

    private double calculateBudgetExecutionRatio(Long budgetCent, long feeTotalCent) {
        long budget = budgetCent == null ? 0L : budgetCent;
        if (budget <= 0L) {
            return 0d;
        }
        return (double) feeTotalCent / budget;
    }

    private void assertProjectBudgetConstraint(boolean allowProjectOverBudget,
                                              double thresholdRatio,
                                              double budgetExecutionRatio,
                                              Long budgetCent,
                                              long feeTotalCent) {
        if (allowProjectOverBudget) {
            return;
        }
        double allowedRatio = 1d + Math.max(0d, thresholdRatio);
        if (budgetExecutionRatio > allowedRatio) {
            long budget = budgetCent == null ? 0L : budgetCent;
            long allowedTotalCent = Math.round(budget * allowedRatio);
            long overCent = Math.max(0L, feeTotalCent - allowedTotalCent);
            String detail = String.format(
                    Locale.ROOT,
                    "项目总费用已超过预算阈值：当前执行率%.2f%%，阈值%.2f%%，超出%.2f元",
                    budgetExecutionRatio * 100d,
                    allowedRatio * 100d,
                    overCent / 100d
            );
            throw new BusinessException(10001, detail);
        }
    }

    private void logProjectCreate(Project project) {
        if (bizChangeLogService == null) {
            return;
        }
        bizChangeLogService.logAction("PROJECT", project.getId(), "PROJECT_CREATE", null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "name", "项目名称", null, project.getName(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "parentProjectId", "上级项目ID", null, project.getParentProjectId(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "startDate", "项目开始日期", null, project.getStartDate(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "endDate", "项目结束日期", null, project.getEndDate(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "partnerEnterpriseId", "合作企业ID", null, project.getPartnerEnterpriseId(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "budgetCent", "项目预算(分)", null, project.getBudgetCent(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "meetingTotal", "会议总期数", null, project.getMeetingTotal(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "laborFeeRatio", "劳务费用占比", null, project.getLaborFeeRatio(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "invoiceInfo", "发票信息", null, project.getInvoiceInfo(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "expenseRatioJson", "费用占比配置", null, project.getExpenseRatioJson(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "projectFeeJson", "项目费用配置", null, project.getProjectFeeJson(), null);
        logProjectFieldChange(project.getId(), "PROJECT_CREATE", "status", "项目状态", null, project.getStatus(), null);
    }

    private void logProjectUpdate(Project before, Project after) {
        if (bizChangeLogService == null) {
            return;
        }
        String batchId = bizChangeLogService.newBatchId();
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "name", "项目名称", before.getName(), after.getName(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "parentProjectId", "上级项目ID", before.getParentProjectId(), after.getParentProjectId(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "startDate", "项目开始日期", before.getStartDate(), after.getStartDate(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "endDate", "项目结束日期", before.getEndDate(), after.getEndDate(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "partnerEnterpriseId", "合作企业ID", before.getPartnerEnterpriseId(), after.getPartnerEnterpriseId(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "budgetCent", "项目预算(分)", before.getBudgetCent(), after.getBudgetCent(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "meetingTotal", "会议总期数", before.getMeetingTotal(), after.getMeetingTotal(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "laborFeeRatio", "劳务费用占比", before.getLaborFeeRatio(), after.getLaborFeeRatio(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "invoiceInfo", "发票信息", before.getInvoiceInfo(), after.getInvoiceInfo(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "expenseRatioJson", "费用占比配置", before.getExpenseRatioJson(), after.getExpenseRatioJson(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "projectFeeJson", "项目费用配置", before.getProjectFeeJson(), after.getProjectFeeJson(), batchId);
        logProjectFieldChange(after.getId(), "PROJECT_UPDATE", "status", "项目状态", before.getStatus(), after.getStatus(), batchId);
    }

    private void logProjectBindingChanges(Long projectId,
                                          List<Map<String, Object>> beforeOwnerUsers,
                                          List<Map<String, Object>> beforeExecutorUsers,
                                          List<Map<String, Object>> beforeLegacyExecutorUsers,
                                          List<Long> ownerUserIds,
                                          List<Long> executorUserIds,
                                          List<Long> legacyExecutorUserIds) {
        if (bizChangeLogService == null) {
            return;
        }
        String batchId = bizChangeLogService.newBatchId();
        logBindingDiff(projectId, "PROJECT_OWNER", "项目负责人", beforeOwnerUsers, ownerUserIds, batchId);
        logBindingDiff(projectId, "PROJECT_EXECUTOR", "项目执行人", beforeExecutorUsers, executorUserIds, batchId);
        logBindingDiff(projectId, "EXECUTOR", "执行人", beforeLegacyExecutorUsers, legacyExecutorUserIds, batchId);
    }

    private void logBindingDiff(Long projectId,
                                String fieldCode,
                                String fieldName,
                                List<Map<String, Object>> beforeUsers,
                                List<Long> afterUserIds,
                                String batchId) {
        Map<Long, String> beforeMap = new LinkedHashMap<Long, String>();
        for (Map<String, Object> item : beforeUsers) {
            if (item == null) {
                continue;
            }
            Object userIdVal = item.get("userId");
            long userId = userIdVal instanceof Number ? ((Number) userIdVal).longValue() : 0L;
            if (userId > 0L) {
                beforeMap.put(userId, String.valueOf(item.get("userName") == null ? "" : item.get("userName")).trim());
            }
        }
        Map<Long, String> afterMap = loadUserNameMap(afterUserIds);
        Set<Long> allIds = new HashSet<Long>();
        allIds.addAll(beforeMap.keySet());
        allIds.addAll(afterMap.keySet());
        List<Long> sortedIds = new ArrayList<Long>(allIds);
        sortedIds.sort(Comparator.naturalOrder());
        for (Long userId : sortedIds) {
            boolean beforeExists = beforeMap.containsKey(userId);
            boolean afterExists = afterMap.containsKey(userId);
            String userName = beforeExists ? beforeMap.get(userId) : afterMap.get(userId);
            if (!beforeExists && afterExists) {
                bizChangeLogService.logRelationAdd("PROJECT", projectId, "PROJECT_BIND_ADD", fieldCode, fieldName, userId, userName, batchId, null);
            } else if (beforeExists && !afterExists) {
                bizChangeLogService.logRelationRemove("PROJECT", projectId, "PROJECT_BIND_REMOVE", fieldCode, fieldName, userId, userName, batchId, null);
            }
        }
    }

    private Map<Long, String> loadUserNameMap(List<Long> userIds) {
        Map<Long, String> map = new HashMap<Long, String>();
        if (bizChangeLogService == null) {
            return map;
        }
        for (BizChangeLogService.UserRef item : bizChangeLogService.loadUserRefs(userIds)) {
            map.put(item.getUserId(), item.getUserName());
        }
        return map;
    }

    private void logProjectFieldChange(Long projectId,
                                       String changeType,
                                       String fieldCode,
                                       String fieldName,
                                       Object beforeValue,
                                       Object afterValue,
                                       String batchId) {
        if (bizChangeLogService == null) {
            return;
        }
        bizChangeLogService.logFieldChange(
                "PROJECT",
                projectId,
                changeType,
                fieldCode,
                fieldName,
                normalizeProjectFieldValue(beforeValue),
                normalizeProjectFieldValue(afterValue),
                batchId,
                null
        );
    }

    private Object normalizeProjectFieldValue(Object value) {
        if (value instanceof Enum<?>) {
            return ((Enum<?>) value).name();
        }
        return value;
    }

    private Map<String, Object> toProjectChangeLogRow(BizChangeLogInfo item) {
        Map<String, Object> row = new LinkedHashMap<String, Object>();
        row.put("id", item.getId());
        row.put("fieldCode", item.getFieldCode());
        row.put("fieldName", resolveProjectFieldName(item));
        row.put("beforeValue", item.getBeforeValue());
        row.put("afterValue", item.getAfterValue());
        row.put("changeReason", item.getRemark());
        row.put("handoverAt", null);
        row.put("createdBy", item.getOperatorUserId());
        row.put("createdAt", item.getCreatedAt());
        row.put("changeType", item.getChangeType());
        row.put("operatorUserName", item.getOperatorUserName());
        row.put("batchId", item.getBatchId());
        return row;
    }

    private String resolveProjectFieldName(BizChangeLogInfo item) {
        String fieldName = item.getFieldName() == null ? "" : item.getFieldName().trim();
        if (!fieldName.isEmpty()) {
            return fieldName;
        }
        if ("PROJECT_CREATE".equals(item.getChangeType())) {
            return "项目创建";
        }
        if ("PROJECT_FREEZE".equals(item.getChangeType())) {
            return "项目冻结";
        }
        if ("PROJECT_UNFREEZE".equals(item.getChangeType())) {
            return "项目解冻";
        }
        if ("PROJECT_ARCHIVE".equals(item.getChangeType())) {
            return "项目归档";
        }
        return "项目变更";
    }

    private static class ProjectFeeSummary {
        private final String normalizedJson;
        private final long totalCent;

        private ProjectFeeSummary(String normalizedJson, long totalCent) {
            this.normalizedJson = normalizedJson;
            this.totalCent = totalCent;
        }
    }
}
