package com.yunhai.flowcenter.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yunhai.flowcenter.engine.DefValidator;
import com.yunhai.flowcenter.engine.FlowDefModel;
import com.yunhai.flowcenter.engine.VariableContract;
import com.yunhai.flowcenter.engine.FlowEngine;
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
import com.yunhai.flowcenter.entity.FlwDef;
import com.yunhai.flowcenter.enums.FlwEnums;
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;

/**
 * 流程定义管理（M1：JSON 文本编辑模式，概览 §9.2）：
 * 保存=草稿直改/已发布升版本（flowGuid 跨版本不变）；发布跑静态校验（把"任务无主"拦在发布期）。
 */
@Service
@RequiredArgsConstructor
public class FlowDefAdminService {

    private final FlwDefRepository defRepository;
    private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
    private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
    private final com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
    private final com.yunhai.flowcenter.repository.FlwInstanceRepository instanceRepository;
    private final FlowDeptAdminService deptAdminService;
    private final FlowEngine flowEngine;
    private final ObjectMapper objectMapper;
    private final FormCenterSchemaClient formCenterSchemaClient;

    /** 定义列表：status/appKey 可选过滤（各流程线各版本都展示；appKey 精确匹配，2026-10-10 按应用筛选） */
    public List<FlwDef> list(Long schId, Integer status, String appKey) {
        List<FlwDef> rows = status != null
                ? defRepository.findBySchIdAndStatusOrderByUpdateTimeDesc(schId, status)
                : defRepository.findBySchIdOrderByUpdateTimeDesc(schId);
        String key = appKey == null ? null : appKey.trim();
        if (key != null && !key.isEmpty()) {
            rows = rows.stream().filter(d -> key.equals(d.getAppKey())).collect(Collectors.toList());
        }
        return rows;
    }

    /** 应用清单（"按应用筛选"下拉数据源）：本校 flw_def 已标注 ∪ 已注册回调的应用，去重排序 */
    public List<String> distinctAppKeys(Long schId) {
        TreeSet<String> keys = new TreeSet<>();
        keys.addAll(defRepository.findDistinctAppKeys(schId));
        keys.addAll(regRepository.findDistinctAppKeysBySchId(schId));
        return new ArrayList<>(keys);
    }

    /** 全量列表（管理端首页：各流程线最新版本+状态聚合展示简化为全量返回） */
    public List<FlwDef> listAll(Long schId) {
        return defRepository.findPublishedLatestPerGuid(schId);
    }

    public FlwDef get(Long schId, Long id) {
        FlwDef def = requireDef(schId, id);
        return def;
    }

    /**
     * 保存（2026-10-10 起唯一键为 uk(sch,flow_guid,version)，编码降级为可重名的分类标签）：
     * - id 非空：草稿直改；已发布/停用=复制升版本（同 flowGuid，version=max+1）
     * - id 空 + baseGuid 非空：在该流程线上开新版本草稿（version=max+1，flowGuid 不变）——前端"升版本"
     * - id 空：永远新开一条流程线（flowGuid 生成，version=1）；同编码多条线合法，前端保存前有软提示
     * appKey=归属应用（筛选维度，2026-10-12 拍板必填）：新建必须显式传；升版本/直改可继承源行，
     * 但源行为空时必须显式补填——存量 NULL 线随编辑自然收敛（白名单只校验显式传入值，继承值不重校）
     * changeLog=升版本改动说明（议题二③）：只随新建行落库（三个建新行分支），草稿直改行不写也不清空
     */
    @Transactional
    public FlwDef save(Long schId, Long id, String code, String name, String contentJson,
                      String baseGuid, String appKey, String changeLog) {
        String cleanLog = (changeLog == null || changeLog.trim().isEmpty()) ? null
                : changeLog.trim().substring(0, Math.min(500, changeLog.trim().length()));
        String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
        // 归属应用白名单（2026-10-10 应用设置）：显式传入非空时必须 ∈ flw_app(sch, enabled=1)；
        // 继承值不重校（既成事实，应用停用不应卡死升版本）。不在校池/未开启的应用由此拦下（拍板 D2：不做存量兜底）
        if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
            throw new BizException(ErrorCode.PARAM_ERROR,
                    "归属应用未开启或不存在（" + cleanAppKey + "）：请在 流程管理 > 应用设置 中开启后重试");
        }
        // JSON 先解析再落库（语法错误不进库）
        FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
        if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "code/name 必填");
        }
        FlwDef def;
        if (id == null && baseGuid != null && !baseGuid.trim().isEmpty()) {
            // 在既有流程线上开新版本草稿（appKey 缺省继承源线）
            FlwDef base = defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, baseGuid.trim())
                    .orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "baseGuid 流程线不存在"));
            def = new FlwDef();
            def.setSchId(schId);
            def.setFlowGuid(baseGuid.trim());
            def.setCode(code.trim());
            def.setVersion(defRepository.maxVersionByGuid(schId, baseGuid.trim()) + 1);
            def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
            def.setAppKey(cleanAppKey != null ? cleanAppKey : base.getAppKey());
            def.setFormUuid(base.getFormUuid());
            def.setFormVersion(base.getFormVersion());
        } else if (id == null) {
            // 新建：永远新开一条流程线，版本从 1 起（不再做同编码草稿复用）
            def = new FlwDef();
            def.setSchId(schId);
            def.setFlowGuid(UUID.randomUUID().toString().replace("-", "").toLowerCase());
            def.setCode(code.trim());
            def.setVersion(1);
            def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
            def.setAppKey(cleanAppKey);
        } else {
            def = requireDef(schId, id);
            if (!def.getCode().equals(code.trim())) {
                throw new BizException(ErrorCode.PARAM_ERROR, "流程编码不可修改");
            }
            if (cleanAppKey != null) {
                def.setAppKey(cleanAppKey);
            }
            if (def.getStatus() != FlwEnums.DefStatus.DRAFT.getCode()) {
                // 升版本：复制行，保持 flowGuid
                FlwDef next = new FlwDef();
                next.setSchId(schId);
                next.setFlowGuid(def.getFlowGuid());
                next.setCode(def.getCode());
                next.setVersion(defRepository.maxVersionByGuid(schId, def.getFlowGuid()) + 1);
                next.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
                next.setAppKey(cleanAppKey != null ? cleanAppKey : def.getAppKey());
                next.setFormUuid(def.getFormUuid());
                next.setFormVersion(def.getFormVersion());
                def = next;
            }
        }
        def.setCode(code.trim());
        def.setName(name.trim());
        // 归属应用必填（2026-10-12 拍板）：各分支归一后落库值不得为空——新建必须显式传；
        // 升版本/直改继承源行，源行为空的存量线编辑时强制补填（19 条存量随编辑自然收敛）
        if (def.getAppKey() == null || def.getAppKey().trim().isEmpty()) {
            throw new BizException(ErrorCode.PARAM_ERROR,
                    "归属应用必填：请选择归属应用后再保存（应用需先在 流程管理 > 应用设置 中开启）");
        }
        // 改动说明只写新建行（新建/baseGuid 开版/已发布复制升版三分支 id 必为 null）；草稿直改不动
        if (def.getId() == null) {
            def.setChangeLog(cleanLog);
        }
        def.setContentJson(withoutFormContract(contentJson)); // Only publish may author a contract snapshot.
        // 定义 JSON 里的 code/name 与行字段一致性校验（给绑定回显用行字段，JSON 内为主）
        if (parsed.code != null && !parsed.code.trim().isEmpty() && !parsed.code.trim().equals(def.getCode())) {
            def.setCode(parsed.code.trim());
        }
        if (parsed.name != null && !parsed.name.trim().isEmpty()) {
            def.setName(parsed.name.trim());
        }
        try {
            return defRepository.save(def);
        } catch (DataIntegrityViolationException e) {
            // 撞唯一键 (sch,flow_guid,version)：并发升版本同一线时可能撞号，转可理解提示（原样抛出=笼统"内部服务错误"）
            throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突：该流程可能正被他人同时升版本，请返回列表刷新后重试");
        }
    }

    /**
     * 保存重载（2026-10-20 C1 第一刀）：formUuid=关联表单定义 uuid（form-center 对外身份，32 位）。<br>
     * <b>发布后不可改，改=升版本</b>由既有升版分派天然保证（非 DRAFT 直改必走升版新行）；
     * 参数 null=保持不动（缺省语义，兼容旧调用面）；空串=显式取消关联（归一 null 落库）；
     * 非空=设置，保存前按确切版本远端核验，失败不写入。
     * <br>v3/TASK_1 追加 {@code formVersion}：与 formUuid <b>成对</b>，规则是——
     * ① 两个都给了值=设关联（版本按服务端收到的整数存，之后发布按这组确切坐标核验）；
     * ② 只给一个=拒（半个坐标既存不下也没法核验，比"存了再说"便宜）；
     * ③ formUuid 给空串=取消关联，<b>同时清空版本</b>（留着一个没人认领的版本号是下一轮误用的起点）；
     * ④ formUuid 给 null=这条指令不存在（两个坐标都不动，兼容旧调用面）。
     */
    @Transactional
    public FlwDef save(Long schId, Long id, String code, String name, String contentJson,
                      String baseGuid, String appKey, String changeLog, String formUuid, Integer formVersion) {
        boolean clearing = formUuid != null && formUuid.trim().isEmpty();
        boolean pairing = formUuid != null && !clearing && formVersion == null;
        boolean versionOnly = formUuid == null && formVersion != null;
        if (pairing || versionOnly) {
            throw new BizException(formVersion == null ? FormBindingErrorCode.VERSION_REQUIRED
                    : FormBindingErrorCode.INVALID_COORDINATE,
                    "关联表单必须成对提交 formUuid 与 formVersion（只传一个拒）：当前 formUuid="
                            + (formUuid == null ? "未传" : formUuid.trim()) + "，formVersion=" + formVersion);
        }
        if (formUuid != null && !clearing) {
            checkedSchema(schId, formUuid.trim(), formVersion, "保存"); // Validate before the first save.
        }
        FlwDef saved = save(schId, id, code, name, contentJson, baseGuid, appKey, changeLog);
        if (formUuid != null) {
            String clean = formUuid.trim();
            saved.setFormUuid(clean.isEmpty() ? null : clean);
            saved.setFormVersion(clean.isEmpty() ? null : formVersion);
        }
        try {
            return defRepository.save(saved);
        } catch (DataIntegrityViolationException e) {
            throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突：该流程可能正被他人同时升版本，请返回列表刷新后重试");
        }
    }

    /** 发布：静态校验（START/END 唯一、无孤儿无环、DEPT_KEY 有启用映射）后置为已发布。<br>
     * 2026-10-20 C1 第一刀：def 关联了表单（form_uuid 非空）时，发布期拉 form-center schema 快照
     * 嵌入版本行 content_json.formContract（与 defId 快照纪律同构：在途办件按发起时快照校验），
     * 并做表达式<b>键查①</b>红牌（变量 ⊆ 快照字段 ∪ 内置变量）。拉取失败=发布阻断（可重试）；
     * 运行期对 form-center 零依赖；新发布的无绑定版本必须显式声明 variables。 */
    @Transactional
    public FlwDef publish(Long schId, Long id) {
        FlwDef def = requireDef(schId, id);
        if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "已是发布状态");
        }
        if (def.getStatus() != FlwEnums.DefStatus.DRAFT.getCode()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "停用版本请走启用；修改表单关联须保存为新版本草稿");
        }
        FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
        if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
            Map<String, Object> snapshot = checkedSchema(schId, def.getFormUuid().trim(),
                    def.getFormVersion(), "发布");
            model.variables = VariableContract.fromSnapshot(snapshot);
            VariableContract.validateDefinition(model, true);
            DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
            def.setContentJson(embedFormContract(def.getContentJson(), snapshot, model.variables));
        } else if (def.getFormVersion() != null) {
            throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
        }
        if (def.getFormUuid() == null || def.getFormUuid().trim().isEmpty()) {
            VariableContract.validateDefinition(model, true);
            DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
        }
        def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
        def.setPublishTime(LocalDateTime.now());
        return defRepository.save(def);
    }

    /** content_json 顶层嵌入 formContract 节（ObjectNode 保字段顺序；原值存在则整体替换=本版快照） */
    private String embedFormContract(String contentJson, Map<String, Object> snapshot,
                                     List<FlowDefModel.Variable> variables) {
        try {
            com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
                    objectMapper.readTree(contentJson);
            root.set("formContract", objectMapper.valueToTree(snapshot));
            root.set("variables", objectMapper.valueToTree(variables));
            return objectMapper.writeValueAsString(root);
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            throw new BizException(ErrorCode.PARAM_ERROR, "快照嵌入失败：" + e.getMessage());
        }
    }

    /** 停用：不可新发起，存量跑完 */
    @Transactional
    public void offline(Long schId, Long id) {
        FlwDef def = requireDef(schId, id);
        if (def.getStatus() == FlwEnums.DefStatus.DRAFT.getCode()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "草稿不能停用后启用，请先通过发布校验");
        }
        def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
        defRepository.save(def);
    }

    /**
     * 线级停发（2026-10-25 T03）：整条 flowGuid 停止新发起/预览，在途实例按各自固定 defId 原版跑完。
     * 与版本级 {@link #offline} 正交（可叠加）；批量 UPDATE 全版本行，读任一行即读线状态。
     * 不改写任何发布内容字节，恢复走 {@link #enableLine}。
     */
    @Transactional
    public int offlineLine(Long schId, String flowGuid) {
        requireLine(schId, flowGuid);
        return defRepository.updateLineOffline(schId, flowGuid.trim(), 1);
    }

    /**
     * 线级恢复（T03）：只恢复线的可用性——line_offline 归零；各版本 status/发布内容/版本指针
     * 一律不动（停发期间发布新版不受影响，恢复后新发起即用当时最高已发布版）。
     */
    @Transactional
    public int enableLine(Long schId, String flowGuid) {
        requireLine(schId, flowGuid);
        return defRepository.updateLineOffline(schId, flowGuid.trim(), 0);
    }

    private void requireLine(Long schId, String flowGuid) {
        if (flowGuid == null || flowGuid.trim().isEmpty()
                || defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid.trim())
                        .isPresent()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "流程线不存在：" + flowGuid);
        }
    }

    /**
     * 启用（2026-09-18）：只对"停用"版恢复为已发布，与 offline 对称。
     * 复用发布期的静态校验（把"任务无主"重新拦一次）——若建的时候某科室还有启用办理人、之后
     * 被停用，这里会点名报错（如"科室 X 无启用办理人"），管理员在办理人映射页点"启用"即可解。
     */
    @Transactional
    public FlwDef enable(Long schId, Long id) {
        FlwDef def = requireDef(schId, id);
        if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "该流程已是发布状态，无需启用");
        }
        if (def.getStatus() != FlwEnums.DefStatus.DISABLED.getCode()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "仅停用的流程可启用；草稿请走发布");
        }
        FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
        DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
        if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
            checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion(), "启用");
        } else if (def.getFormVersion() != null) {
            throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
        }
        def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
        def.setPublishTime(LocalDateTime.now());
        return defRepository.save(def);
    }

    private Map<String, Object> checkedSchema(Long schId, String uuid, Integer version, String operation) {
        if (version == null) {
            throw new BizException(FormBindingErrorCode.VERSION_REQUIRED, "关联表单缺少明确版本，"
                    + operation + "已阻断，请重新选择表单与版本");
        }
        if (version < 1 || uuid == null || uuid.trim().isEmpty()) {
            throw new BizException(FormBindingErrorCode.INVALID_COORDINATE,
                    "关联表单必须选择确切的正整数版本，" + operation + "已阻断，请重新选择");
        }
        try {
            return formCenterSchemaClient.fetchSchema(schId, uuid, version);
        } catch (FormCenterSchemaClient.SchemaUnavailableException e) {
            throw new BizException(FormBindingErrorCode.SCHEMA_UNAVAILABLE,
                    "关联表单快照拉取失败，" + operation + "已阻断（可重试）", e);
        } catch (FormCenterSchemaClient.SchemaContractViolationException e) {
            throw new BizException(FormBindingErrorCode.SCHEMA_CONTRACT_VIOLATION,
                    "关联表单版本核验失败，" + operation + "已阻断（请重新选择表单与版本）", e);
        } catch (FormCenterSchemaClient.SchemaResourceUnavailableException e) {
            throw new BizException(FormBindingErrorCode.RESOURCE_UNAVAILABLE,
                    "关联表单或指定发布版本不存在或不可读，" + operation + "已阻断", e);
        } catch (FormCenterSchemaClient.SchemaAccessDeniedException e) {
            throw new BizException(FormBindingErrorCode.ACCESS_DENIED,
                    "关联表单访问身份或授权被拒绝，" + operation + "已阻断，请检查授权", e);
        }
    }

    private String withoutFormContract(String contentJson) {
        try {
            com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
                    objectMapper.readTree(contentJson);
            root.remove("formContract");
            return objectMapper.writeValueAsString(root);
        } catch (Exception e) {
            throw new BizException(ErrorCode.PARAM_ERROR, "流程定义必须是有效的JSON对象");
        }
    }

    /** 同一流程线全部版本（降序） */
    public List<FlwDef> versions(Long schId, String flowGuid) {
        return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
    }

    /** 在途办件数（议题二④）：flowGuid → 审批中(10)办件数；口径=def_id 精确匹配该线各版本；请求的 guid 全部有键（无在途=0） */
    public Map<String, Long> inflightCounts(Long schId, Collection<String> flowGuids) {
        Map<String, Long> result = new HashMap<>();
        if (flowGuids == null || flowGuids.isEmpty()) return result;
        flowGuids.forEach(g -> result.put(g, 0L));
        List<FlwDef> defs = defRepository.findBySchIdAndFlowGuidIn(schId, flowGuids);
        if (defs.isEmpty()) return result;
        Map<Long, String> idToGuid = new HashMap<>();
        defs.forEach(d -> idToGuid.put(d.getId(), d.getFlowGuid()));
        instanceRepository.countApprovingByDefIds(schId, idToGuid.keySet()).forEach(r -> {
            Long defId = ((Number) r[0]).longValue();
            Long cnt = ((Number) r[1]).longValue();
            result.merge(idToGuid.get(defId), cnt, Long::sum);
        });
        return result;
    }

    /**
     * 元数据原地编辑（2026-10-10 四议题·议题2，拍板 D4）：已发布/停用/草稿均可只改 name/appKey
     * 而<b>不升版本</b>——结构改动仍走 save 的升版本路径，两者完全隔离（本方法不碰
     * code/flow_guid/version/status，不进升版本分支）。appKey 校验同 save 口径（显式传入须在白名单）。
     * content_json 顶层 name 同步更新（只动顶层字段不动结构，消除"行 name 与 JSON name 的脏窗口"；
     * 亦修复 API 直调方经 parsed.name 覆盖行字段时读到旧名的口径）。
     * 并发说明：与升版本路径无共享可变行（升版本建新行），不加锁。
     */
    @Transactional
    public FlwDef updateMeta(Long schId, Long id, String name, String appKey) {
        FlwDef def = requireDef(schId, id);
        if (name == null || name.trim().isEmpty()) {
            throw new BizException(ErrorCode.PARAM_ERROR, "流程名称不能为空");
        }
        String cleanName = name.trim();
        String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
        if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
            throw new BizException(ErrorCode.PARAM_ERROR,
                    "归属应用未开启或不存在（" + cleanAppKey + "）：请在 流程管理 > 应用设置 中开启后重试");
        }
        def.setName(cleanName);
        def.setAppKey(cleanAppKey);
        // 发布封存（2026-10-25 T03）：非草稿版本的执行文档不可变——行 name 照改（目录元数据），
        // content_json 顶层 name 保持发布时字节，消除"已发布文档被原地改写"的旁路
        //（审计 E02：updateMeta 曾同步改已发布 contentJson 顶层 name）。
        if (def.getStatus() != null && def.getStatus() == FlwEnums.DefStatus.DRAFT.getCode()) {
            def.setContentJson(syncTopLevelName(def.getContentJson(), cleanName));
        }
        try {
            return defRepository.save(def);
        } catch (DataIntegrityViolationException e) {
            throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突：该流程可能正被他人同时编辑，请刷新后重试");
        }
    }

    /** contentJson 顶层 name 同步（ObjectNode 保字段顺序；结构零改动），返回新 JSON 串 */
    private String syncTopLevelName(String contentJson, String name) {
        try {
            com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode)
                    objectMapper.readTree(contentJson);
            root.put("name", name);
            return objectMapper.writeValueAsString(root);
        } catch (Exception e) {
            // contentJson 在 save 期已保证可解析；防御口径：解析失败不同步（行 name 已更新，JSON 维持原值）
            return contentJson;
        }
    }

    private Set<String> enabledDeptKeys(Long schId) {
        // 2026-10-10 科室管理切源：发布校验白名单改查 flw_dept/flw_dept_member（拍板 D1）
        return deptAdminService.enabledDeptKeys(schId);
    }

    /** CONDITION routerKey 发布校验白名单（flw_router 启用键全集，全局不分校） */
    private Set<String> enabledRouterKeys() {
        return routerRepository.findByStatusOrderByIdAsc(1).stream()
                .map(com.yunhai.flowcenter.entity.FlwRouter::getRouterKey)
                .collect(Collectors.toSet());
    }

    private FlwDef requireDef(Long schId, Long id) {
        FlwDef def = defRepository.findById(id)
                .orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义不存在：" + id));
        if (!def.getSchId().equals(schId)) {
            throw new BizException(ErrorCode.FORBIDDEN, "流程定义不属于该学校");
        }
        return def;
    }

}
