package com.kingdee.eas.custom.perfweb.utils;

import com.alibaba.fastjson.JSON;
import com.kingdee.bos.BOSException;
import com.kingdee.bos.Context;
import com.kingdee.bos.metadata.entity.*;
import com.kingdee.bos.metadata.query.util.CompareType;
import com.kingdee.shr.perfweb.app.base.evalplan.*;
import com.kingdee.shr.perfweb.app.workflow.GradeTaskNodeInstInfo;
import com.lark.oapi.Client;
import com.lark.oapi.core.Config;
import com.lark.oapi.core.Transport;
import com.lark.oapi.core.request.RequestOptions;
import com.lark.oapi.core.response.RawResponse;
import com.lark.oapi.core.token.AccessTokenType;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.core.utils.Sets;
import com.lark.oapi.service.auth.v3.model.InternalTenantAccessTokenReq;
import com.lark.oapi.service.auth.v3.model.InternalTenantAccessTokenReqBody;
import com.lark.oapi.service.auth.v3.model.InternalTenantAccessTokenResp;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.util.CollectionUtils;

import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 飞书消息/待办推送工具类
 * <p>
 * 提供：
 * 1. sendInitialFeishuTodos — 启动考核后推送飞书消息（按处理人去重）
 * 2. sendAfterScoreSubmit — 评分提交后向下一节点处理人推送飞书消息（按处理人去重）
 * 3. sendRemindMessage / sendMessage — 飞书 SDK 自定义 Bot 消息
 * 4. sendFeishuCall / sendFeishuTodo — 时创意 OA 飞书待办接口（新建/修改/办结）
 * 5. sendAfterScoreSubmitOaTodo — 评分提交后 OA 待办处理（修改/新建/办结）
 * 6. sendCustomApprovalBotMessage / sendTodoApprovalBotMessage — 飞书开放平台审批 Bot API
 * 7. updateApprovalBotMessage — 飞书开放平台更新审批 Bot 消息状态
 *
 * @author 青梧
 * @date 2026-06-11
 */
public class FeishuMessageUtil {

    private static Logger logger = Logger.getLogger(FeishuMessageUtil.class);

    // ===== 飞书开放平台配置（HR流程）=====
    /**
     * 正式环境 App ID
     */
    public static String FEISHU_APP_ID_PROD = "cli_a9c200a401389cbb";
    /**
     * 正式环境 App Secret
     */
    public static String FEISHU_APP_SECRET_PROD = "Pho6oFfaFT2CRBB8HyWHnfhigokwtjMD";
    /**
     * 测试环境 App ID
     */
    public static String FEISHU_APP_ID_TEST = "cli_a9ca684911b81cc5";
    /**
     * 测试环境 App Secret
     */
    public static String FEISHU_APP_SECRET_TEST = "oL2rxjpbKCpwl5vnCxn5PLiDy8A1IR7Q";
    /**
     * 当前生效 App ID（发版切换 PROD / TEST）
     */
    public static String FEISHU_APP_ID = FEISHU_APP_ID_PROD;
    /**
     * 当前生效 App Secret
     */
    public static String FEISHU_APP_SECRET = FEISHU_APP_SECRET_PROD;
    private static final String APPROVAL_BOT_SEND_PATH = "/open-apis/approval/v1/message/send";
    private static final String APPROVAL_BOT_UPDATE_PATH = "/open-apis/approval/v1/message/update";
    /**
     * 通用模板-收到审批待办（支持快捷审批）
     */
    public static final String TEMPLATE_APPROVAL_TODO = "1008";
    /**
     * 审批待办模板（approval_name + comment + content.summaries）
     */
    public static final String TEMPLATE_APPROVAL_COMMENT = "1001";
    /**
     * 自定义模板（custom_title + custom_content；sendCustomApprovalBotMessage 使用）
     */
    public static final String TEMPLATE_CUSTOM = "1021";
    /**
     * 更新消息-已处理
     */
    public static final String BOT_STATUS_PROCESSED = "PROCESSED";
    /**
     * 更新消息-已同意
     */
    public static final String BOT_STATUS_APPROVED = "APPROVED";
    /**
     * 更新消息-自定义状态
     */
    public static final String BOT_STATUS_CUSTOM = "CUSTOM";

    /**
     * 启动考核 / 评分提交等绩效评定类飞书消息卡片标题（不使用流程定义名如「一级考评流程」）
     */
    public static final String PERF_SCORE_TODO_TITLE = "\u7EE9\u6548\u8BC4\u5B9A\u5F85\u529E\u63D0\u9192";

    private static volatile Client feishuClient;
    private static volatile Config feishuConfig;
    private static volatile String cachedTenantAccessToken;
    private static volatile long tokenExpireAtMs;
    private static final Object FEISHU_INIT_LOCK = new Object();
    private static final Object TOKEN_LOCK = new Object();
    /**
     * flowId(GradeTaskNodeInst.id) → 飞书 message_id，用于后续更新 Bot 消息
     */
    private static final Map<String, String> FLOW_MESSAGE_ID_CACHE = new HashMap<>();

    // ===== 时创意 OA 待办接口默认参数 =====
    private static final String DEFAULT_SYSCODE = "SHR";
    private static final String DEFAULT_NODENAME = "sHR";
    /**
     * 0-待审批（新建待办）
     */
    public static final String ISREMARK_NEW = "0";
    /**
     * 2-已办（修改待办状态）
     */
    public static final String ISREMARK_DONE = "2";
    /**
     * 4-办结（流程结束）
     */
    public static final String ISREMARK_COMPLETE = "4";
    /**
     * 0-未读
     */
    public static final String VIEWTYPE_UNREAD = "0";
    /**
     * 1-已读
     */
    public static final String VIEWTYPE_READ = "1";

    /**
     * 启动考核后向一级节点处理人发送飞书消息（SDK 自定义 Bot 消息）
     * <p>
     * 去重逻辑：勾选多个考核对象时，若处理人相同，只发送一条消息
     *
     * @param ctx              上下文
     * @param evaObjHandlerMap key=evaObjId$workFlowId, value=处理人工号集合
     * @param evaObjSet        成功的考核对象ID集合
     * @param period           考核周期
     * @param pcurl            PC 端跳转地址
     * @param workFlowName     流程名称（如 "绩效考核"）
     * @param appurl           移动端跳转地址
     */
    public static void sendInitialFeishuTodos(Context ctx, Map<String, Set<String>> evaObjHandlerMap, Set<String> evaObjSet, PerfPeriodInfo period, String pcurl, String workFlowName, String appurl) {
        Set<String> uniqueHandlers = collectUniqueHandlers(evaObjHandlerMap, evaObjSet);
        if (uniqueHandlers.isEmpty()) {
            logger.error("无处理人信息，跳过飞书消息推送");
            return;
        }
        String title = PERF_SCORE_TODO_TITLE;
        String periodName = period != null ? period.getName() : "";
        Map<String, String[]> displayMap = loadEvaObjDisplayMap(ctx, evaObjSet);
        String periodId = period != null && period.getId() != null ? period.getId().toString() : "";
        logger.error("启动考核飞书消息推送参数：periodId=" + periodId + "，workFlowName=" + workFlowName + "，title=" + title + "，handlerCount=" + uniqueHandlers.size() + "，handlers=" + uniqueHandlers + "，pcurl=" + pcurl + "，appurl=" + appurl);
        for (String handlerId : uniqueHandlers) {
            String content = buildRemindContentForHandler(handlerId, evaObjHandlerMap, evaObjSet, displayMap, periodName);
            String uuid = "start_eva_" + periodId + "_" + handlerId + "_" + System.currentTimeMillis();
            ApprovalBotResult result = sendCustomApprovalBotMessage(handlerId, uuid, title, content, pcurl, appurl, null);
            if (!result.isSuccess()) {
                logger.error("启动考核飞书消息发送失败，handler=" + handlerId + "，code=" + result.getCode() + "，msg=" + result.getMsg());
            } else {
                logger.error("启动考核飞书消息发送成功，handler=" + handlerId + "，messageId=" + result.getMessageId());
            }
        }
    }

    /**
     * 从 evaObjHandlerMap 中收集去重后的处理人工号
     * <p>
     * 仅统计 evaObjSet 中启动成功的考核对象
     *
     * @param evaObjHandlerMap key=evaObjId$workFlowId, value=处理人工号集合
     * @param evaObjSet        成功的考核对象ID集合
     * @return 去重后的处理人工号集合（保持插入顺序）
     */
    private static Set<String> collectUniqueHandlers(Map<String, Set<String>> evaObjHandlerMap, Set<String> evaObjSet) {
        Set<String> handlers = new LinkedHashSet<>();
        if (evaObjHandlerMap == null || CollectionUtils.isEmpty(evaObjSet)) {
            return handlers;
        }
        for (Map.Entry<String, Set<String>> entry : evaObjHandlerMap.entrySet()) {
            String evaObjId = parseEvaObjId(entry.getKey());
            if (StringUtils.isBlank(evaObjId) || !evaObjSet.contains(evaObjId)) {
                continue;
            }
            if (CollectionUtils.isEmpty(entry.getValue())) {
                continue;
            }
            for (String handlerId : entry.getValue()) {
                if (StringUtils.isNotBlank(handlerId)) {
                    handlers.add(handlerId);
                }
            }
        }
        return handlers;
    }

    /**
     * 启动考核后向一级节点处理人发送 OA 飞书待办（按考核对象逐条，flowId=GradeTaskNodeInst.id）
     *
     * @param ctx              上下文
     * @param evaObjHandlerMap key=evaObjId$workFlowId, value=处理人工号集合
     * @param evaObjSet        成功的考核对象ID集合
     * @param period           考核周期
     * @param pcurl            PC 端跳转地址
     * @param workFlowName     流程名称
     * @param appurl           移动端跳转地址
     * @param creatorId        创建人工号（OA 待办 creator 字段，由调用方传入）
     */
    public static void sendInitialFeishuTodosOa(Context ctx, Map<String, Set<String>> evaObjHandlerMap, Set<String> evaObjSet, PerfPeriodInfo period, String pcurl, String workFlowName, String appurl, String creatorId) {
        if (StringUtils.isBlank(creatorId)) {
            logger.error("creatorId 不能为空，跳过 OA 飞书待办推送");
            return;
        }
        if (evaObjHandlerMap == null || evaObjHandlerMap.isEmpty()) {
            logger.error("无处理人信息，跳过 OA 飞书待办推送");
            return;
        }
        String requestName = buildScoreRequestName(period);
        for (Map.Entry<String, Set<String>> entry : evaObjHandlerMap.entrySet()) {
            String evaObjId = parseEvaObjId(entry.getKey());
            if (StringUtils.isBlank(evaObjId) || !evaObjSet.contains(evaObjId)) {
                continue;
            }
            String flowId = PerfEvaObjectUtil.getGradeTaskNodeInstId(ctx, evaObjId, 1);
            if (StringUtils.isBlank(flowId)) {
                logger.error("未找到一级节点实例，跳过 OA 待办，evaObjId=" + evaObjId);
                continue;
            }
            sendToHandlers(entry.getValue(), requestName, pcurl, workFlowName, appurl, flowId, VIEWTYPE_UNREAD, ISREMARK_NEW, creatorId);
        }
    }

    /**
     * 评分提交后向下一节点处理人发送飞书消息（SDK 自定义 Bot 消息）
     * <p>
     * 去重逻辑：批量提交多个考核对象时，若下一节点处理人相同，只发送一条消息
     * <p>
     * 流程结束时（无下一节点处理人）不推送消息
     *
     * @param ctx                    上下文（保留参数，便于扩展）
     * @param evaObjSubmitWfLevelMap key=evaObjId, value=当前提交的节点等级 wfLevel
     * @param nextHandlerMap         key=evaObjId$workFlowId, value=下一节点处理人工号集合
     * @param evaObjSet              提交成功的考核对象ID集合
     * @param period                 考核周期
     * @param pcurl                  PC 端跳转地址
     * @param workFlowName           流程名称
     * @param appurl                 移动端跳转地址
     */
    public static void sendAfterScoreSubmit(Context ctx, Map<String, Integer> evaObjSubmitWfLevelMap, Map<String, Set<String>> nextHandlerMap, Set<String> evaObjSet, PerfPeriodInfo period, String pcurl, String workFlowName, String appurl) {
        if (evaObjSet == null || evaObjSet.isEmpty() || period == null) {
            return;
        }
        Set<String> nextHandlers = collectUniqueNextHandlers(evaObjSet, nextHandlerMap);
        if (nextHandlers.isEmpty()) {
            logger.error("评分提交后无下一节点处理人，跳过飞书消息推送");
            return;
        }
        String title = PERF_SCORE_TODO_TITLE;
        String periodName = period.getName();
        Map<String, String[]> displayMap = loadEvaObjDisplayMap(ctx, evaObjSet);
        String periodId = period.getId() != null ? period.getId().toString() : "";
        logger.error("评分提交飞书消息推送参数：periodId=" + periodId + "，workFlowName=" + workFlowName + "，title=" + title + "，evaObjCount=" + evaObjSet.size() + "，handlerCount=" + nextHandlers.size() + "，handlers=" + nextHandlers + "，pcurl=" + pcurl + "，appurl=" + appurl);
        for (String handlerId : nextHandlers) {
            String content = buildRemindContentForHandler(handlerId, nextHandlerMap, evaObjSet, displayMap, periodName);
            String uuid = "score_next_" + periodId + "_" + handlerId + "_" + System.currentTimeMillis();
            ApprovalBotResult result = sendCustomApprovalBotMessage(handlerId, uuid, title, content, pcurl, appurl, null);
            if (!result.isSuccess()) {
                logger.error("评分提交飞书消息发送失败，handler=" + handlerId + "，code=" + result.getCode() + "，msg=" + result.getMsg());
            } else {
                logger.error("评分提交飞书消息发送成功，handler=" + handlerId + "，messageId=" + result.getMessageId());
            }
        }
    }

    /**
     * 收集下一节点去重后的处理人工号
     *
     * @param evaObjSet      提交成功的考核对象ID集合
     * @param nextHandlerMap 下一节点处理人 Map
     * @return 去重后的处理人工号集合
     */
    private static Set<String> collectUniqueNextHandlers(Set<String> evaObjSet, Map<String, Set<String>> nextHandlerMap) {
        Set<String> handlers = new LinkedHashSet<>();
        if (CollectionUtils.isEmpty(evaObjSet)) {
            return handlers;
        }
        for (String evaObjId : evaObjSet) {
            Set<String> objHandlers = findHandlersForEvaObj(evaObjId, nextHandlerMap);
            if (!CollectionUtils.isEmpty(objHandlers)) {
                for (String handlerId : objHandlers) {
                    if (StringUtils.isNotBlank(handlerId)) {
                        handlers.add(handlerId);
                    }
                }
            }
        }
        return handlers;
    }

    /**
     * 从处理人 Map 中查找指定考核对象的下一节点处理人工号
     * <p>
     * handlerMap 的 key 格式为 evaObjId$workFlowId，按 evaObjId 前缀匹配并合并所有处理人
     *
     * @param evaObjId   考核对象ID
     * @param handlerMap key=evaObjId$workFlowId, value=处理人工号集合
     * @return 该考核对象对应的处理人工号集合（去重，可能为空）
     */
    private static Set<String> findHandlersForEvaObj(String evaObjId, Map<String, Set<String>> handlerMap) {
        Set<String> handlers = new LinkedHashSet<>();
        if (handlerMap == null || StringUtils.isBlank(evaObjId)) {
            return handlers;
        }
        String prefix = evaObjId + "$";
        for (Map.Entry<String, Set<String>> entry : handlerMap.entrySet()) {
            if (entry.getKey() != null && entry.getKey().startsWith(prefix) && !CollectionUtils.isEmpty(entry.getValue())) {
                handlers.addAll(entry.getValue());
            }
        }
        return handlers;
    }

    /**
     * 批量加载考核对象展示信息：key=evaObjId，value=[部门, 工号, 姓名]
     */
    private static Map<String, String[]> loadEvaObjDisplayMap(Context ctx, Set<String> evaObjSet) {
        Map<String, String[]> result = new HashMap<>();
        if (ctx == null || CollectionUtils.isEmpty(evaObjSet)) {
            return result;
        }
        try {
            FilterInfo filter = new FilterInfo();
            filter.getFilterItems().add(new FilterItemInfo("id", evaObjSet, CompareType.INCLUDE));
            EntityViewInfo evi = new EntityViewInfo();
            evi.setFilter(filter);
            SelectorItemCollection sic = new SelectorItemCollection();
            sic.add(new SelectorItemInfo("id"));
            sic.add(new SelectorItemInfo("person.number"));
            sic.add(new SelectorItemInfo("person.name"));
            sic.add(new SelectorItemInfo("adminOrgUnit.name"));
            evi.setSelector(sic);
            PerfEvaObjectCollection coll = PerfEvaObjectFactory.getLocalInstance(ctx).getPerfEvaObjectCollection(evi);
            if (coll == null) {
                return result;
            }
            for (int i = 0; i < coll.size(); i++) {
                PerfEvaObjectInfo info = coll.get(i);
                if (info == null || info.getId() == null) {
                    continue;
                }
                String deptName = info.getAdminOrgUnit() != null ? StringUtils.defaultString(info.getAdminOrgUnit().getName(), "") : "";
                String personNumber = "";
                String personName = "";
                if (info.getPerson() != null) {
                    personNumber = StringUtils.defaultString(info.getPerson().getNumber(), "");
                    personName = StringUtils.defaultString(info.getPerson().getName(ctx.getLocale()), "");
                }
                result.put(info.getId().toString(), new String[]{deptName, personNumber, personName});
            }
        } catch (BOSException e) {
            logger.error("loadEvaObjDisplayMap failed", e);
        }
        return result;
    }

    /**
     * 按处理人拼装提醒正文：取该处理人名下、勾选顺序中第一条考核对象的信息
     */
    private static String buildRemindContentForHandler(String handlerId, Map<String, Set<String>> handlerMap, Set<String> evaObjSet, Map<String, String[]> displayMap, String periodName) {
        if (StringUtils.isBlank(handlerId) || CollectionUtils.isEmpty(evaObjSet)) {
            return buildEvaResultSummaryRemindMessage("", "", "", periodName);
        }
        for (String evaObjId : evaObjSet) {
            Set<String> handlers = findHandlersForEvaObj(evaObjId, handlerMap);
            if (CollectionUtils.isEmpty(handlers) || !handlers.contains(handlerId)) {
                continue;
            }
            String[] display = displayMap != null ? displayMap.get(evaObjId) : null;
            if (display != null) {
                return buildEvaResultSummaryRemindMessage(display[0], display[1], display[2], periodName);
            }
            return buildEvaResultSummaryRemindMessage("", "", "", periodName);
        }
        return buildEvaResultSummaryRemindMessage("", "", "", periodName);
    }

    /**
     * 从 evaObjHandlerMap 的 key 中解析考核对象ID
     * <p>
     * key 格式为 evaObjId$workFlowId，取 $ 前面的部分；无 $ 时返回原 key
     *
     * @param key evaObjHandlerMap 的 key
     * @return 考核对象ID；key 为空时返回 null
     */
    private static String parseEvaObjId(String key) {
        if (StringUtils.isBlank(key)) {
            return null;
        }
        int idx = key.indexOf("$");
        return idx > 0 ? key.substring(0, idx) : key;
    }

    /**
     * 发送飞书提醒消息（SDK 自定义 Bot 消息）
     *
     * @param receiverId 接收人工号（飞书 user_id，与现有 OA 接口一致）
     * @param title      消息标题
     * @param content    消息正文
     * @param pcurl      PC 端跳转地址
     * @param appurl     移动端跳转地址
     * @param uuid       幂等 ID
     * @return 调用结果
     */
    public static ApprovalBotResult sendRemindMessage(String receiverId, String title, String content, String pcurl, String appurl, String uuid) {
        if (StringUtils.isBlank(receiverId)) {
            ApprovalBotResult result = new ApprovalBotResult();
            result.code = -1;
            result.msg = "receiverId 不能为空";
            return result;
        }
        String safeTitle = StringUtils.defaultIfBlank(title, "绩效考核提醒");
        String safeUuid = StringUtils.isNotBlank(uuid) ? uuid : String.valueOf(System.currentTimeMillis());
        logger.error("飞书 sendRemindMessage 参数：receiverId=" + receiverId + "，title=" + safeTitle + "，content=" + content + "，pcurl=" + pcurl + "，appurl=" + appurl + "，uuid=" + safeUuid);
        return sendCustomApprovalBotMessage(receiverId, safeUuid, safeTitle, content, pcurl, appurl, null);
    }

    /**
     * 发送飞书消息（兼容旧调用签名）
     *
     * @param receiverId      接收人工号
     * @param content         消息正文
     * @param pcurl           PC 端跳转地址
     * @param workFlowName    消息标题（流程名称）
     * @param appurl          移动端跳转地址
     * @param uuid            幂等 ID（可传 flowId 或业务唯一键）
     * @param receivets       保留参数，兼容旧调用
     * @param receivedatetime 保留参数，兼容旧调用
     * @return 调用结果
     */
    public static ApprovalBotResult sendMessage(String receiverId, String content, String pcurl, String workFlowName, String appurl, String uuid, String receivets, String receivedatetime) {
        return sendRemindMessage(receiverId, workFlowName, content, pcurl, appurl, uuid);
    }

    /**
     * 拼装启动考核/评分提交飞书消息正文
     * <p>
     * 消息格式：
     * <pre>
     * 您（工号：{personNumber}，姓名：{personName}）{periodDisplay}绩效尚未完成审批评定
     * 请点击此消息进入 HR 系统完成绩效等级评定。
     * </pre>
     * <p>
     * 卡片标题（custom_title）使用 {@link #PERF_SCORE_TODO_TITLE}，流程名称 workFlowName 仅用于日志排查。
     *
     * @param deptName      保留参数兼容调用方，消息正文不再展示部门
     * @param personNumber  被考核人工号
     * @param personName    被考核人姓名
     * @param periodDisplay 考核周期展示文本（如 "SCY-2026年02季度"）
     * @return 完整飞书待办消息正文（含换行）
     */
    public static String buildEvaResultSummaryRemindMessage(String deptName, String personNumber, String personName, String periodDisplay) {
        String number = StringUtils.defaultString(personNumber, "");
        String name = StringUtils.defaultString(personName, "");
        String period = StringUtils.defaultString(periodDisplay, "");
        return "您组织下的（工号：" + number + "，姓名：" + name + "）" + period + "绩效尚未完成审批评定\n" + "请点击此消息进入 HR 系统完成绩效等级评定。";
    }

    /**
     * 绩效结果审批通过通知正文（保留模板，当前无业务调用）
     *
     * @param periodDisplay 考核周期名称
     * @param gradeLevel    无等级时传空，展示为「—」
     */
    public static String buildEvaResultApprovedNotifyMessage(String periodDisplay, String gradeLevel) {
        String period = StringUtils.defaultString(periodDisplay, "");
        String grade = StringUtils.isNotBlank(gradeLevel) ? gradeLevel : "—";
        return "【绩效结果通知】\n" + "您" + period + "绩效考核已审批通过，等级为 " + grade + "。\n" + "请点击此消息进入 HR 系统查看详情。";
    }

    /**
     * 考核结果汇总列表「发送飞书消息」按钮专用模板（sendFeishuToLeadersAction）
     *
     * @param periodDisplay 考核周期名称（如「2026 年 一 季度」）
     */
    public static String buildEvaResultSummaryFeishuMessage(String periodDisplay) {
        String period = StringUtils.defaultString(periodDisplay, "");
        return "【绩效结果公示通知】您的" + period + "绩效考核结果已出，可前往 HR 系统查看。";
    }

    // ===== 时创意 OA 飞书待办通道（保留） =====

    /**
     * 拼装绩效考核 OA 待办标题
     *
     * @param period 考核周期
     * @return 待办标题
     */
    public static String buildScoreRequestName(PerfPeriodInfo period) {
        String periodName = period != null ? period.getName() : "";
        return "绩效考核待办:【" + periodName + "】考核周期内存在考核任务待处理，请前往绩效工作台进行评分!!";
    }

    /**
     * 评分提交后 OA 飞书待办处理（修改上一级/新建下一级/办结）
     * <p>
     * flowId 均取 GradeTaskNodeInst.id
     *
     * @param ctx                    上下文
     * @param evaObjSubmitWfLevelMap key=evaObjId, value=当前提交的节点等级 wfLevel
     * @param nextHandlerMap         key=evaObjId$workFlowId, value=下一节点处理人工号集合
     * @param evaObjSet              提交成功的考核对象ID集合
     * @param period                 考核周期
     * @param pcurl                  PC端跳转地址
     * @param workFlowName           流程名称
     * @param appurl                 移动端跳转地址
     * @param creatorId              创建人工号（OA 待办 creator 字段，由调用方传入）
     */
    public static void sendAfterScoreSubmitOaTodo(Context ctx, Map<String, Integer> evaObjSubmitWfLevelMap, Map<String, Set<String>> nextHandlerMap, Set<String> evaObjSet, PerfPeriodInfo period, String pcurl, String workFlowName, String appurl, String creatorId) {
        if (StringUtils.isBlank(creatorId)) {
            logger.error("creatorId 不能为空，跳过 OA 飞书待办处理");
            return;
        }
        if (evaObjSet == null || evaObjSet.isEmpty() || period == null) {
            return;
        }
        String requestName = buildScoreRequestName(period);
        for (String evaObjId : evaObjSet) {
            Integer submitWfLevel = evaObjSubmitWfLevelMap != null ? evaObjSubmitWfLevelMap.get(evaObjId) : null;
            if (submitWfLevel == null) {
                continue;
            }
            try {
                if (submitWfLevel != 1) {
                    modifyPreviousTodo(ctx, evaObjId, submitWfLevel, requestName, pcurl, workFlowName, appurl, creatorId);
                }
                Set<String> nextHandlers = findHandlersForEvaObj(evaObjId, nextHandlerMap);
                if (!CollectionUtils.isEmpty(nextHandlers)) {
                    String nextFlowId = PerfEvaObjectUtil.getGradeTaskNodeInstId(ctx, evaObjId, submitWfLevel + 1);
                    if (StringUtils.isBlank(nextFlowId)) {
                        logger.error("未找到下一节点实例，跳过 OA 新待办，evaObjId=" + evaObjId + ", wfLevel=" + (submitWfLevel + 1));
                        continue;
                    }
                    sendToHandlers(nextHandlers, requestName, pcurl, workFlowName, appurl, nextFlowId, VIEWTYPE_UNREAD, ISREMARK_NEW, creatorId);
                } else {
                    completeCurrentTodo(ctx, evaObjId, submitWfLevel, requestName, pcurl, workFlowName, appurl, creatorId);
                }
            } catch (Exception e) {
                logger.error("评分提交 OA 待办处理失败，evaObjId=" + evaObjId, e);
            }
        }
    }

    /**
     * 修改上一级 OA 飞书待办为已办（viewtype=1,isremark=2）
     */
    private static void modifyPreviousTodo(Context ctx, String evaObjId, int submitWfLevel, String requestName, String pcurl, String workFlowName, String appurl, String creatorId) throws BOSException {
        GradeTaskNodeInstInfo prevInst = PerfEvaObjectUtil.getGradeTaskNodeInst(ctx, evaObjId, String.valueOf(submitWfLevel - 1));
        if (prevInst == null || prevInst.getId() == null) {
            logger.error("未找到上一级节点实例，跳过修改 OA 待办，evaObjId=" + evaObjId + ", wfLevel=" + (submitWfLevel - 1));
            return;
        }
        String receiverId = PerfEvaObjectUtil.getNodeHandlerNumber(prevInst);
        if (StringUtils.isBlank(receiverId)) {
            logger.error("上一级节点无处理人，跳过修改 OA 待办，evaObjId=" + evaObjId);
            return;
        }
        sendFeishuCall(receiverId, requestName, pcurl, workFlowName, appurl, prevInst.getId().toString(), VIEWTYPE_READ, ISREMARK_DONE, creatorId);
    }

    /**
     * 流程结束时办结当前节点 OA 飞书待办（viewtype=1,isremark=4）
     */
    private static void completeCurrentTodo(Context ctx, String evaObjId, int submitWfLevel, String requestName, String pcurl, String workFlowName, String appurl, String creatorId) throws BOSException {
        GradeTaskNodeInstInfo currentInst = PerfEvaObjectUtil.getGradeTaskNodeInst(ctx, evaObjId, String.valueOf(submitWfLevel));
        if (currentInst == null || currentInst.getId() == null) {
            logger.error("未找到当前节点实例，跳过 OA 办结，evaObjId=" + evaObjId + ", wfLevel=" + submitWfLevel);
            return;
        }
        String receiverId = PerfEvaObjectUtil.getNodeHandlerNumber(currentInst);
        if (StringUtils.isBlank(receiverId)) {
            logger.error("当前节点无处理人，跳过 OA 办结，evaObjId=" + evaObjId);
            return;
        }
        sendFeishuCall(receiverId, requestName, pcurl, workFlowName, appurl, currentInst.getId().toString(), VIEWTYPE_READ, ISREMARK_COMPLETE, creatorId);
    }

    /**
     * 向多个处理人批量发送 OA 飞书待办/修改/办结
     */
    private static void sendToHandlers(Set<String> handlers, String requestName, String pcurl, String workFlowName, String appurl, String flowId, String viewtype, String isremark, String creatorId) {
        if (CollectionUtils.isEmpty(handlers) || StringUtils.isBlank(flowId)) {
            return;
        }
        for (String handlerId : handlers) {
            if (StringUtils.isNotBlank(handlerId)) {
                sendFeishuCall(handlerId, requestName, pcurl, workFlowName, appurl, flowId, viewtype, isremark, creatorId);
            }
        }
    }

    /**
     * 发送 OA 飞书待办（新建，viewtype=0,isremark=0）
     *
     * @param receiverId   接收人工号
     * @param requestName  待办标题
     * @param pcurl        PC 端跳转地址
     * @param workFlowName 流程名称
     * @param appurl       移动端跳转地址
     * @param flowId       流程 ID（GradeTaskNodeInst.id）
     * @param creatorId    创建人工号（OA 待办 creator 字段，由调用方传入）
     */
    public static void sendFeishuTodo(String receiverId, String requestName, String pcurl, String workFlowName, String appurl, String flowId, String creatorId) {
        sendFeishuCall(receiverId, requestName, pcurl, workFlowName, appurl, flowId, VIEWTYPE_UNREAD, ISREMARK_NEW, creatorId);
    }

    /**
     * 发送 OA 飞书待办/修改/办结（可指定 viewtype、isremark）
     * <p>
     * - 新建待办：viewtype=0, isremark=0
     * - 修改待办：viewtype=1, isremark=2
     * - 流程办结：viewtype=1, isremark=4
     *
     * @param receiverId   接收人工号
     * @param requestName  待办标题
     * @param pcurl        PC 端跳转地址
     * @param workFlowName 流程名称
     * @param appurl       移动端跳转地址
     * @param flowId       流程 ID（GradeTaskNodeInst.id）
     * @param viewtype     查看状态（0 未读/1 已读）
     * @param isremark     待办状态（0 待审批/2 已办/4 办结）
     * @param creatorId    创建人工号（OA 待办 creator 字段，由调用方传入）
     */
    public static void sendFeishuCall(String receiverId, String requestName, String pcurl, String workFlowName, String appurl, String flowId, String viewtype, String isremark, String creatorId) {
        if (StringUtils.isBlank(creatorId)) {
            logger.error("OA 飞书待办 creatorId 不能为空，receiverId=" + receiverId + ", flowId=" + flowId);
            return;
        }
        if (StringUtils.isBlank(receiverId) || StringUtils.isBlank(flowId)) {
            logger.error("OA 飞书待办参数不完整，receiverId=" + receiverId + ", flowId=" + flowId);
            return;
        }
        String receivets = String.valueOf(System.currentTimeMillis());
        String nowStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        String apiUrl = UrlParamUtil.PRODURL;
        String safePcurl = StringUtils.defaultString(pcurl);
        String safeAppurl = StringUtils.isNotBlank(appurl) ? appurl : safePcurl;
        String jsonBody = buildOaTodoRequestJson(receiverId, requestName, safePcurl, workFlowName, safeAppurl, flowId, receivets, nowStr, viewtype, isremark, creatorId);

        logger.error("OA 飞书待办请求 URL=" + apiUrl);
        logger.error("OA 飞书待办请求 Body=" + jsonBody);

        HttpURLConnection conn = null;
        try {
            URL url = new URL(apiUrl);
            conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("POST");
            conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
            conn.setDoOutput(true);
            conn.setConnectTimeout(10000);
            conn.setReadTimeout(10000);

            OutputStream os = conn.getOutputStream();
            os.write(jsonBody.getBytes("UTF-8"));
            os.flush();
            os.close();

            int responseCode = conn.getResponseCode();
            logger.error("OA 飞书待办响应码=" + responseCode + "，receiverId=" + receiverId + "，flowId=" + flowId + "，viewtype=" + viewtype + "，isremark=" + isremark);
        } catch (Exception e) {
            logger.error("OA 飞书待办推送异常，receiverId=" + receiverId + "，flowId=" + flowId, e);
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

    /**
     * 构建 OA 飞书待办请求 JSON
     */
    private static String buildOaTodoRequestJson(String receiverId, String requestName, String pcurl, String workFlowName, String appurl, String flowId, String receivets, String receivedatetime, String viewtype, String isremark, String creatorId) {
        return "{" + "\"requestname\":\"" + escapeJson(requestName) + "\"," + "\"creator\":\"" + escapeJson(creatorId) + "\"," + "\"receiver\":\"" + escapeJson(receiverId) + "\"," + "\"pcurl\":\"" + escapeJson(pcurl) + "\"," + "\"workflowname\":\"" + escapeJson(workFlowName) + "\"," + "\"viewtype\":\"" + escapeJson(viewtype) + "\"," + "\"appurl\":\"" + escapeJson(appurl) + "\"," + "\"syscode\":\"" + DEFAULT_SYSCODE + "\"," + "\"nodename\":\"" + DEFAULT_NODENAME + "\"," + "\"receivets\":\"" + escapeJson(receivets) + "\"," + "\"receivedatetime\":\"" + escapeJson(receivedatetime) + "\"," + "\"isremark\":\"" + escapeJson(isremark) + "\"," + "\"createdatetime\":\"" + escapeJson(receivedatetime) + "\"," + "\"flowid\":\"" + escapeJson(flowId) + "\"}";
    }

    /**
     * JSON 字符串转义
     */
    private static String escapeJson(String str) {
        if (str == null) {
            return "";
        }
        return str.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
    }

    // ===== 飞书开放平台官方 API（SDK Transport）=====

    /**
     * 飞书审批 Bot 消息调用结果
     */
    public static class ApprovalBotResult {
        private int code;
        private String msg;
        private String messageId;

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }

        public String getMessageId() {
            return messageId;
        }

        public boolean isSuccess() {
            return code == 0;
        }
    }

    /**
     * 获取飞书 SDK Config（单例）
     *
     * @return Config 实例；AppId/AppSecret 未配置时返回 null
     */
    private static Config getFeishuConfig() {
        if (feishuConfig != null) {
            return feishuConfig;
        }
        synchronized (FEISHU_INIT_LOCK) {
            if (feishuConfig != null) {
                return feishuConfig;
            }
            if (StringUtils.isBlank(FEISHU_APP_ID) || StringUtils.isBlank(FEISHU_APP_SECRET)) {
                logger.error("飞书 AppId/AppSecret 未配置，无法创建 SDK Config");
                return null;
            }
            Config config = new Config();
            config.setAppId(FEISHU_APP_ID);
            config.setAppSecret(FEISHU_APP_SECRET);
            feishuConfig = config;
            if (feishuClient == null) {
                feishuClient = Client.newBuilder(FEISHU_APP_ID, FEISHU_APP_SECRET).build();
            }
            return feishuConfig;
        }
    }

    /**
     * 获取飞书 SDK Client（单例）
     *
     * @return Client 实例；AppId/AppSecret 未配置时返回 null
     */
    private static Client getFeishuClient() {
        if (feishuClient != null) {
            return feishuClient;
        }
        getFeishuConfig();
        return feishuClient;
    }

    /**
     * 构建 SDK 请求选项（自动附带 tenant_access_token）
     *
     * @return RequestOptions
     */
    private static RequestOptions buildRequestOptions() {
        String token = getTenantAccessToken();
        RequestOptions.Builder builder = RequestOptions.newBuilder();
        if (StringUtils.isNotBlank(token)) {
            builder.tenantAccessToken(token);
        }
        return builder.build();
    }

    /**
     * 获取 tenant_access_token（飞书 SDK + 内存缓存，过期前 60 秒自动刷新）
     * <p>
     * 接口文档：https://open.feishu.cn/document/ukTMukTMukTM/ukDNz4SO0MjL5QzM/auth-v3/auth/tenant_access_token_internal
     *
     * @return tenant_access_token；失败返回 null
     */
    public static String getTenantAccessToken() {
        long now = System.currentTimeMillis();
        if (StringUtils.isNotBlank(cachedTenantAccessToken) && now < tokenExpireAtMs - 60000) {
            return cachedTenantAccessToken;
        }
        synchronized (TOKEN_LOCK) {
            now = System.currentTimeMillis();
            if (StringUtils.isNotBlank(cachedTenantAccessToken) && now < tokenExpireAtMs - 60000) {
                return cachedTenantAccessToken;
            }
            Client client = getFeishuClient();
            if (client == null) {
                return null;
            }
            try {
                InternalTenantAccessTokenReq req = InternalTenantAccessTokenReq.newBuilder().internalTenantAccessTokenReqBody(InternalTenantAccessTokenReqBody.newBuilder().appId(FEISHU_APP_ID).appSecret(FEISHU_APP_SECRET).build()).build();
                InternalTenantAccessTokenResp resp = client.auth().v3().tenantAccessToken().internal(req);
                if (!resp.success()) {
                    logger.error("获取 tenant_access_token 失败，code=" + resp.getCode() + "，msg=" + resp.getMsg() + "，reqId=" + resp.getRequestId());
                    return null;
                }
                String response = new String(resp.getRawResponse().getBody(), StandardCharsets.UTF_8);
                String token = extractJsonString(response, "tenant_access_token");
                if (StringUtils.isBlank(token)) {
                    logger.error("获取 tenant_access_token 失败，响应中无 token，response=" + response);
                    return null;
                }
                int expire = extractJsonInt(response, "expire", 7200);
                cachedTenantAccessToken = token;
                tokenExpireAtMs = System.currentTimeMillis() + expire * 1000L;
                return token;
            } catch (Exception e) {
                logger.error("飞书 SDK 获取 tenant_access_token 异常", e);
                return null;
            }
        }
    }

    /**
     * 调用飞书审批 Bot 发送接口（SDK Transport）
     *
     * @param body 请求体（template_id、user_id 等）
     * @return 调用结果
     */
    private static ApprovalBotResult sendApprovalBotMessage(Map<String, Object> body) {
        return invokeApprovalBotApi(APPROVAL_BOT_SEND_PATH, body);
    }

    /**
     * 调用飞书审批 Bot 更新接口（SDK Transport）
     *
     * @param body 请求体（message_id、status 等）
     * @return 调用结果
     */
    private static ApprovalBotResult updateApprovalBotMessageInternal(Map<String, Object> body) {
        return invokeApprovalBotApi(APPROVAL_BOT_UPDATE_PATH, body);
    }

    /**
     * 通过 SDK Transport 调用审批 Bot OpenAPI
     *
     * @param path 接口路径
     * @param body 请求 JSON 对象
     * @return 调用结果
     */
    private static ApprovalBotResult invokeApprovalBotApi(String path, Map<String, Object> body) {
        ApprovalBotResult result = new ApprovalBotResult();
        Config config = getFeishuConfig();
        if (config == null) {
            result.code = -1;
            result.msg = "飞书 AppId/AppSecret 未配置";
            return result;
        }
        try {
            logger.error("飞书 SDK 请求 path=" + path + "，requestBody=" + Jsons.DEFAULT.toJson(body));
            RequestOptions reqOpt = buildRequestOptions();
            RawResponse raw = Transport.send(config, reqOpt, "POST", path, Sets.newHashSet(AccessTokenType.Tenant), body);
            return parseApprovalBotRawResponse(raw, path);
        } catch (Exception e) {
            logger.error("飞书 SDK 调用审批 Bot 接口异常，path=" + path, e);
            result.code = -1;
            result.msg = e.getMessage();
            return result;
        }
    }

    /**
     * 发送审批 Bot 消息（template_id=1021 自定义模板）
     * <p>
     * 请求体结构：custom_title、custom_content、note、actions；Bot 跳转 url 使用 appurl，pc_url 使用 pcurl
     * <p>
     * 接口文档：https://open.feishu.cn/document/server-docs/approval-v4/message/send-bot-messages
     *
     * @param userId    接收人 user_id（当前与工号一致）
     * @param uuid      幂等 ID，相同 uuid 1 小时内只发一次
     * @param title     卡片标题 → custom_title
     * @param content   卡片正文 → custom_content（支持 Markdown）
     * @param pcUrl     PC 端跳转地址
     * @param mobileUrl 移动端跳转地址
     * @param note      底部备注；为空时默认「来自 HR 绩效考核系统」
     * @return 调用结果，成功时 messageId 可用于 updateApprovalBotMessage
     */
    public static ApprovalBotResult sendCustomApprovalBotMessage(String userId, String uuid, String title, String content, String pcUrl, String mobileUrl, String note) {
        ApprovalBotResult result = new ApprovalBotResult();
        if (StringUtils.isBlank(userId)) {
            result.code = -1;
            result.msg = "userId 不能为空";
            return result;
        }
        String safeUuid = StringUtils.isNotBlank(uuid) ? uuid : String.valueOf(System.currentTimeMillis());
        String safeNote = StringUtils.defaultString(note, "来自 HR 绩效考核系统");
        String safeMobileUrl = StringUtils.isNotBlank(mobileUrl) ? mobileUrl : pcUrl;
        String safePcUrl = StringUtils.isNotBlank(pcUrl) ? pcUrl : safeMobileUrl;
        String safeTitle = StringUtils.defaultIfBlank(title, "绩效考核提醒");
        String safeContent = StringUtils.defaultIfBlank(content, safeTitle);

        Map<String, String> texts = new LinkedHashMap<>();
        texts.put("@i18n@1", safeTitle);
        texts.put("@i18n@2", safeContent);
        texts.put("@i18n@3", safeNote);
        texts.put("@i18n@4", "查看详情");

        Map<String, Object> action = new LinkedHashMap<>();
        action.put("action_name", "@i18n@4");
        action.put("url", safeMobileUrl);
        action.put("android_url", safeMobileUrl);
        action.put("ios_url", safeMobileUrl);
        action.put("pc_url", safePcUrl);

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("template_id", TEMPLATE_CUSTOM);
        body.put("user_id", userId);
        body.put("uuid", safeUuid);
        body.put("custom_title", "@i18n@1");
        body.put("custom_content", "@i18n@2");
        body.put("note", "@i18n@3");
        body.put("actions", Collections.singletonList(action));
        body.put("i18n_resources", Collections.singletonList(buildI18nResource(texts)));
        logger.error("飞书 sendCustomApprovalBotMessage 参数：" + JSON.toJSON(body).toString());
        System.out.println("飞书 sendCustomApprovalBotMessage 参数：" + JSON.toJSON(body).toString());
        return sendApprovalBotMessage(body);
    }

    /**
     * 发送「收到审批待办」Bot 消息（template_id=1008）
     * <p>
     * 适用于标准待办卡片；后续可通过 updateApprovalBotMessage 更新为已办/办结
     *
     * @param userId       接收人飞书 user_id
     * @param uuid         幂等 ID
     * @param approvalName 审批名称（如「绩效考核待办」）
     * @param titleUserId  标题中的申请人/被考核人 user_id
     * @param summary      审批事由摘要
     * @param pcUrl        PC 端跳转地址
     * @param mobileUrl    移动端跳转地址
     * @return 调用结果
     */
    public static ApprovalBotResult sendTodoApprovalBotMessage(String userId, String uuid, String approvalName, String titleUserId, String summary, String pcUrl, String mobileUrl) {
        ApprovalBotResult result = new ApprovalBotResult();
        if (StringUtils.isBlank(userId)) {
            result.code = -1;
            result.msg = "userId 不能为空";
            return result;
        }
        String safeUuid = StringUtils.isNotBlank(uuid) ? uuid : String.valueOf(System.currentTimeMillis());
        String safeMobileUrl = StringUtils.isNotBlank(mobileUrl) ? mobileUrl : pcUrl;
        String safePcUrl = StringUtils.isNotBlank(pcUrl) ? pcUrl : safeMobileUrl;
        String safeTitleUserId = StringUtils.defaultIfBlank(titleUserId, userId);

        Map<String, String> texts = new LinkedHashMap<>();
        texts.put("@i18n@1", approvalName);
        texts.put("@i18n@2", summary);

        Map<String, Object> summaryItem = new LinkedHashMap<>();
        summaryItem.put("summary", "@i18n@2");
        Map<String, Object> content = new LinkedHashMap<>();
        content.put("user_id", safeTitleUserId);
        content.put("summaries", Collections.singletonList(summaryItem));

        Map<String, Object> action = new LinkedHashMap<>();
        action.put("action_name", "DETAIL");
        action.put("url", safeMobileUrl);
        action.put("android_url", safeMobileUrl);
        action.put("ios_url", safeMobileUrl);
        action.put("pc_url", safePcUrl);

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("template_id", TEMPLATE_APPROVAL_TODO);
        body.put("user_id", userId);
        body.put("uuid", safeUuid);
        body.put("approval_name", "@i18n@1");
        body.put("title_user_id", safeTitleUserId);
        body.put("content", content);
        body.put("actions", Collections.singletonList(action));
        body.put("i18n_resources", Collections.singletonList(buildI18nResource(texts)));
        logger.error("飞书 sendTodoApprovalBotMessage:" + body);
        return sendApprovalBotMessage(body);
    }

    /**
     * 更新审批 Bot 消息状态
     * <p>
     * 注意：仅支持更新 template_id=1008 的消息，且 30 天以内
     *
     * @param messageId  发送接口返回的 message_id
     * @param status     状态：PROCESSED/APPROVED/REJECTED/CUSTOM 等
     * @param statusText 自定义状态文案（status=CUSTOM 时生效）
     * @return 调用结果
     */
    public static ApprovalBotResult updateApprovalBotMessage(String messageId, String status, String statusText) {
        ApprovalBotResult result = new ApprovalBotResult();
        if (StringUtils.isBlank(messageId)) {
            result.code = -1;
            result.msg = "messageId 不能为空";
            return result;
        }
        String safeStatus = StringUtils.defaultIfBlank(status, BOT_STATUS_PROCESSED);
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("message_id", messageId);
        body.put("status", safeStatus);
        if (BOT_STATUS_CUSTOM.equals(safeStatus) && StringUtils.isNotBlank(statusText)) {
            Map<String, String> texts = new LinkedHashMap<>();
            texts.put("@i18n@1", statusText);
            texts.put("@i18n@2", "查看详情");
            body.put("status_name", "@i18n@1");
            body.put("detail_action_name", "@i18n@2");
            body.put("i18n_resources", Collections.singletonList(buildI18nResource(texts)));
        } else {
            body.put("i18n_resources", Collections.singletonList(buildI18nResource(Collections.<String, String>emptyMap())));
        }
        logger.error("飞书 updateApprovalBotMessage 参数：messageId=" + messageId + "，status=" + safeStatus + "，statusText=" + statusText);
        return updateApprovalBotMessageInternal(body);
    }

    /**
     * 缓存 flowId 与飞书 message_id 的映射，便于后续按 GradeTaskNodeInst.id 更新消息
     *
     * @param flowId    GradeTaskNodeInst.id
     * @param messageId 飞书 message_id
     */
    public static void cacheFlowMessageId(String flowId, String messageId) {
        if (StringUtils.isNotBlank(flowId) && StringUtils.isNotBlank(messageId)) {
            synchronized (FLOW_MESSAGE_ID_CACHE) {
                FLOW_MESSAGE_ID_CACHE.put(flowId, messageId);
            }
        }
    }

    /**
     * 根据 flowId 获取已缓存的飞书 message_id
     *
     * @param flowId GradeTaskNodeInst.id
     * @return message_id；未缓存时返回 null
     */
    public static String getCachedMessageIdByFlowId(String flowId) {
        if (StringUtils.isBlank(flowId)) {
            return null;
        }
        synchronized (FLOW_MESSAGE_ID_CACHE) {
            return FLOW_MESSAGE_ID_CACHE.get(flowId);
        }
    }

    /**
     * 发送自定义 Bot 消息并缓存 flowId → message_id
     *
     * @param pcUrl     PC 跳转地址
     * @param mobileUrl 移动端跳转地址
     * @return 调用结果
     */
    public static ApprovalBotResult sendCustomBotAndCache(String flowId, String userId, String title, String content, String pcUrl, String mobileUrl) {
        ApprovalBotResult result = sendCustomApprovalBotMessage(userId, flowId, title, content, pcUrl, mobileUrl, null);
        if (result.isSuccess() && StringUtils.isNotBlank(flowId)) {
            cacheFlowMessageId(flowId, result.getMessageId());
        }
        return result;
    }

    /**
     * 按 flowId 更新已缓存 Bot 消息为已处理
     *
     * @param flowId     GradeTaskNodeInst.id
     * @param statusText 自定义状态文案（可为空，默认「已处理」）
     * @return 调用结果；未找到缓存 message_id 时 code=-1
     */
    public static ApprovalBotResult updateBotByFlowId(String flowId, String statusText) {
        String messageId = getCachedMessageIdByFlowId(flowId);
        if (StringUtils.isBlank(messageId)) {
            ApprovalBotResult result = new ApprovalBotResult();
            result.code = -1;
            result.msg = "未找到 flowId 对应的 message_id，flowId=" + flowId;
            return result;
        }
        if (StringUtils.isNotBlank(statusText)) {
            return updateApprovalBotMessage(messageId, BOT_STATUS_CUSTOM, statusText);
        }
        return updateApprovalBotMessage(messageId, BOT_STATUS_PROCESSED, null);
    }

    /**
     * 构建 i18n_resources 单条资源
     *
     * @param texts i18n 文案键值对
     * @return i18n 资源 Map
     */
    private static Map<String, Object> buildI18nResource(Map<String, String> texts) {
        Map<String, Object> resource = new LinkedHashMap<>();
        resource.put("locale", "zh-CN");
        resource.put("is_default", true);
        resource.put("texts", texts);
        return resource;
    }

    /**
     * 解析 SDK RawResponse 为 ApprovalBotResult
     *
     * @param raw  SDK 原始响应
     * @param path 接口路径（日志用）
     * @return 调用结果
     */
    private static ApprovalBotResult parseApprovalBotRawResponse(RawResponse raw, String path) {
        ApprovalBotResult result = new ApprovalBotResult();
        if (raw == null || raw.getBody() == null) {
            result.code = -1;
            result.msg = "飞书 SDK 无响应，path=" + path;
            return result;
        }
        String response = new String(raw.getBody(), StandardCharsets.UTF_8);
        logger.error("飞书 SDK path=" + path + "，status=" + raw.getStatusCode() + "，body=" + response);
        result.code = extractJsonInt(response, "code", -1);
        result.msg = extractJsonString(response, "msg");
        String dataMessageId = extractNestedJsonString(response, "data", "message_id");
        if (StringUtils.isBlank(dataMessageId)) {
            dataMessageId = extractJsonString(response, "message_id");
        }
        result.messageId = dataMessageId;
        return result;
    }

    private static String extractJsonString(String json, String field) {
        if (StringUtils.isBlank(json) || StringUtils.isBlank(field)) {
            return null;
        }
        String key = "\"" + field + "\":\"";
        int start = json.indexOf(key);
        if (start < 0) {
            return null;
        }
        start += key.length();
        int end = json.indexOf("\"", start);
        if (end < 0) {
            return null;
        }
        return json.substring(start, end);
    }

    private static int extractJsonInt(String json, String field, int defaultValue) {
        if (StringUtils.isBlank(json) || StringUtils.isBlank(field)) {
            return defaultValue;
        }
        String key = "\"" + field + "\":";
        int start = json.indexOf(key);
        if (start < 0) {
            return defaultValue;
        }
        start += key.length();
        int end = start;
        while (end < json.length() && (Character.isDigit(json.charAt(end)) || json.charAt(end) == '-')) {
            end++;
        }
        if (end <= start) {
            return defaultValue;
        }
        try {
            return Integer.parseInt(json.substring(start, end));
        } catch (NumberFormatException e) {
            return defaultValue;
        }
    }

    private static String extractNestedJsonString(String json, String objectField, String field) {
        if (StringUtils.isBlank(json)) {
            return null;
        }
        String objectKey = "\"" + objectField + "\":";
        int objectStart = json.indexOf(objectKey);
        if (objectStart < 0) {
            return null;
        }
        objectStart = json.indexOf("{", objectStart);
        if (objectStart < 0) {
            return null;
        }
        int objectEnd = json.indexOf("}", objectStart);
        if (objectEnd < 0) {
            return null;
        }
        return extractJsonString(json.substring(objectStart, objectEnd + 1), field);
    }

}
