// facade actions:define(8) + instance(14) + task(10) —— 对齐 java JeeflowFacade 各私有方法

// ═══ 流程定义 ═══

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::define_page(
  self : Facade[R, E],
  args : Map[String, Json]
) -> Json raise @error.JeeflowError {
  let page_num = arg_i64_or(args, "pageNum", 1L)
  let page_size = arg_i64_or(args, "pageSize", 20L)
  let filters = facade_parse_m_filters(args)
  let page = (self.repo).page_defines(@model.PageQuery::new(page_num, page_size))
  finalize_page(page, define_row_to_json, filters, page_num, page_size)
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::define_detail(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_i64(args, "id") {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  let define_info = match (self.repo).find_define_by_id(id) {
    Some(d) => d
    None => raise @error.DefineNotFound(id)
  }
  let o = @json.empty_object()
  @json.obj_set(o, "id", i64_json(define_info.id))
  @json.obj_set(o, "name", @json.string_of(define_info.name))
  @json.obj_set(o, "display_name", @json.string_of(define_info.display_name))
  @json.obj_set(o, "type", @json.string_of(define_info.define_type))
  @json.obj_set(o, "state", num_json(define_info.state))
  @json.obj_set(o, "version", num_json(define_info.version))
  @json.obj_set(o, "jsonObject", parse_graph(define_info.content) |> opt_json_wrap)
  o
}

///|
fn opt_json_wrap(v : Json?) -> Json {
  match v {
    Some(x) => x
    None => @json.null_json()
  }
}

///|
/// 发起并自动完成申请节点(define/instance 共用)
pub async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Facade::start_and_execute(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let define_id = match arg_i64(args, "processDefineId") {
    Some(v) => v
    None => raise @error.Business("缺少processDefineId参数")
  }
  let operator = operator_arg(args)
  let flow_args = args_to_flow_data(args, ["processDefineId", "operator"])
  let inst = (self.engine).start_async(define_id, operator, flow_args)
  // 自动完成申请节点(assignee=applicant → 发起人)
  let doing = (self.repo).find_doing_tasks(inst.instance_id, [])
  for task in doing {
    (self.repo).add_task_actor(task.task_id, [operator])
    flow_args.insert_i64("submitType", 0L)
    match args.get("f_nextNodeOperator") {
      Some(v) => flow_args.insert("tf_nextNodeOperator", v)
      None => ()
    }
    let _ = (self.engine).execute_task_async(task.task_id, operator, flow_args)
  }
  let o = @json.empty_object()
  @json.obj_set(o, "processInstanceId", i64_json(inst.instance_id))
  o
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::deploy(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let content = match content_bytes(args) {
    Some(c) => c
    None => raise @error.Business("content 缺失")
  }
  let model = @parser.parse_model(content)
  // 按 name 查最新 version+1(含停用)
  let query = @model.PageQuery::new(1L, 1L)
  query.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", model.name))
  let page = (self.repo).page_defines(query)
  let version = if page.rows().is_empty() {
    0
  } else {
    page.rows()[0].version + 1
  }
  let define_info : @model.ProcessDefine = {
    id: 0L,
    name: model.name,
    display_name: model.display_name,
    define_type: model.model_type,
    state: 1,
    content,
    version,
    create_time: Some(@model.current_time_str()),
    create_user: arg_str_or(args, "operator", "system") |> Some,
    update_time: None,
    update_user: None,
  }
  (self.repo).save_define(define_info)
  let o = @json.empty_object()
  @json.obj_set(o, "processDefineId", i64_json(define_info.id))
  o
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::redeploy(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let define_id = match arg_i64(args, "processDefineId") {
    Some(v) => v
    None => raise @error.Business("缺少processDefineId参数")
  }
  let content = match content_bytes(args) {
    Some(c) => c
    None => raise @error.Business("content 缺失")
  }
  let model = @parser.parse_model(content)
  let define_info = match (self.repo).find_define_by_id(define_id) {
    Some(d) => d
    None => raise @error.DefineNotFound(define_id)
  }
  let updated : @model.ProcessDefine = { ..define_info,
    name: model.name,
    display_name: model.display_name,
    define_type: model.model_type,
    content,
    update_user: Some(arg_str_or(args, "operator", "system")),
  }
  (self.repo).update_define(updated)
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::define_remove(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  for id in arg_ids(args) {
    (self.repo).remove_define(id)
  }
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::define_up_and_down(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  // opType 优先,state 兜底(C15)
  let state = match arg_i64(args, "opType") {
    Some(v) => v.to_int()
    None =>
      match arg_i64(args, "state") {
        Some(v) => v.to_int()
        None => raise @error.Business("缺少state参数")
      }
  }
  for id in arg_ids(args) {
    (self.repo).update_define_state(id, state)
  }
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::get_last_by_name(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let name = match arg_str(args, "processDefineName") {
    Some(s) => s
    None => raise @error.Business("缺少processDefineName参数")
  }
  let query = @model.PageQuery::new(1L, 1L)
  query.add_filter(@model.QueryFilter::make("t", @model.Eq, "name", name))
  query.set_order_by("t.version desc")
  let page = (self.repo).page_defines(query)
  // 内存仓储无 SQL orderBy:facade 级按 version 取最大(rust 同款语义)
  let mut best : @model.DefineRow? = None
  for r in page.rows() {
    match best {
      Some(b) => if r.version > b.version { best = Some(r) }
      None => best = Some(r)
    }
  }
  match best {
    Some(def) => {
      let o = @json.empty_object()
      @json.obj_set(o, "id", i64_json(def.id))
      @json.obj_set(o, "name", @json.string_of(def.name))
      @json.obj_set(o, "display_name", @json.string_of(def.display_name))
      @json.obj_set(o, "type", @json.string_of(def.define_type))
      @json.obj_set(o, "state", num_json(def.state))
      @json.obj_set(o, "version", num_json(def.version))
      o
    }
    None => raise @error.Business("流程定义不存在: \{name}")
  }
}

// ═══ 流程实例 ═══

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::instance_page(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let page_num = arg_i64_or(args, "pageNum", 1L)
  let page_size = arg_i64_or(args, "pageSize", 20L)
  let filters = facade_parse_m_filters(args)
  let query = @model.PageQuery::new(page_num, page_size)
  query.set_operator(Some(operator_arg(args)))
  let page = (self.repo).page_instances(query)
  finalize_page(page, instance_row_to_json, filters, page_num, page_size)
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::instance_detail(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_id(args, ["id"]) {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  let inst = match (self.repo).find_instance_by_id(id) {
    Some(i) => i
    None => raise @error.InstanceNotFound(id)
  }
  let define_info = (self.repo).find_define_by_id(inst.define_id)
  let json_object = match define_info {
    Some(d) => parse_graph(d.content)
    None => None
  }
  let first_task = first_task_node_id(json_object)
  let o = @json.empty_object()
  @json.obj_set(o, "id", i64_json(inst.instance_id))
  @json.obj_set(o, "parentId", opt_i64_json(inst.parent_id))
  @json.obj_set(o, "processDefineId", i64_json(inst.define_id))
  @json.obj_set(o, "state", num_json(inst.state))
  @json.obj_set(o, "parentNodeName", opt_json(inst.parent_node_name))
  @json.obj_set(o, "businessNo", opt_json(inst.business_no))
  @json.obj_set(o, "operator", @json.string_of(inst.operator))
  @json.obj_set(o, "ext", flow_data_to_object(inst.variables)) // issues/124:变量唯一对外出口(ext 豁免 camel,键保持下划线)
  @json.obj_set(o, "formData", form_data_of_flow(inst.variables, "f_"))
  @json.obj_set(o, "createTime", opt_json(inst.create_time))
  @json.obj_set(o, "createUser", opt_json(inst.create_user))
  match define_info {
    Some(d) => {
      @json.obj_set(o, "displayName", @json.string_of(d.display_name))
      @json.obj_set(o, "name", @json.string_of(d.name))
      @json.obj_set(o, "version", num_json(d.version))
    }
    None => ()
  }
  @json.obj_set(o, "jsonObject", opt_json_wrap(json_object))
  // 任务列表 + activeTaskList(issues/05-4)
  let tasks = @json.empty_array()
  let active = @json.empty_array()
  for t in inst.tasks {
    let vo = task_vo(t)
    let ext = parse_json_map(Some(@json.stringify(t.variables.to_object_json())))
    let doing = t.is_doing()
    // issues/121 P1:行上值优先(建单已把标记落库),只有存量行没这个键才回退现算。
    // 现算带"仅进行中"判定 ⇒ 拿它覆写历史行会把 true 洗成 false(P2 回退判参与者正是要避开那套口径)。
    if ext.get("isFirstTaskNode") == None {
      ext["isFirstTaskNode"] = bool_of_bool(doing && t.task_name == first_task.unwrap_or(""))
    }
    @json.obj_set(vo, "ext", obj_json(ext))
    @json.arr_push(tasks, vo)
    if doing {
      @json.arr_push(active, vo)
    }
  }
  @json.obj_set(o, "tasks", tasks)
  @json.obj_set(o, "activeTaskList", active)
  o
}

///|
fn bool_of_bool(b : Bool) -> Json {
  @json.bool_of(b)
}

///|
/// 必填字符串参数(issues/114 语义:缺失即报错、返回 trim 后的值)
///
/// issues/142 B 批(spec 06-facade §2.11「transfer 的 fromActor/toActor 归一后再用」):判空判据
/// 不再本地另写一份 trim/is_empty,**复用** @model.normalize_actor(内部就是集合那一枚
/// normalize_actors)—— 于是 `processTask/transfer` 的 fromActor/toActor 与参与者表的比对、
/// 账本与便捷键落库的都是 trim 后的值(" leader " 与 "leader" 同一个人)。
/// 空白档沿用**调用方传入的既有"必填"文案**(§2.11 要求③:不新造错误码与文案);
/// 反向哨兵:"0" 是有效值,判据是 trim()=="" 而不是 falsy。
fn arg_required_str(
  args : Map[String, Json],
  key : String,
  msg : String
) -> String raise @error.JeeflowError {
  match @model.normalize_actor(arg_str(args, key).unwrap_or("")) {
    Some(v) => v
    None => raise @error.Business(msg)
  }
}

///|
/// flow.auto / flow.admin 放行约定(is_allowed 既有语义,撤回/转办共用;大小写不敏感,对齐 java)
fn is_privileged_operator(operator : String) -> Bool {
  let lower = operator.to_lower()
  lower == "flow.auto" || lower == "flow.admin"
}

///|
/// 撤回归属判据(issues/114,spec/06 §withdraw 三条,命中任一放行):
/// 1. operator = 实例发起人(wf_process_instance.operator)
///    ⚠️ 不可复用 ProcessTask::is_allowed——各栈它都只判"任务 actorIds + auto/admin",不查发起人;
/// 2. operator 是该实例任一**进行中**任务的参与者(以 wf_process_task_actor 参与者表为准,
///    不用聚合副本——加签/转办的增量只落参与者表);
/// 3. operator ∈ {flow.auto, flow.admin}。
async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Facade::can_withdraw(
  self : Facade[R, E],
  inst : @model.ProcessInstance,
  operator : String
) -> Bool raise @error.JeeflowError {
  if is_privileged_operator(operator) {
    return true
  }
  if operator == inst.operator {
    return true
  }
  for t in (self.repo).find_doing_tasks(inst.instance_id, []) {
    if ((self.repo).find_task_actors(t.task_id)).contains(operator) {
      return true
    }
  }
  false
}

///|
/// 撤回(issues/113/114):operator 硬必填 + 三条归属判据 + 任务态 30 级联 + update_user 回写
pub async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Facade::withdraw(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_i64(args, "id") {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  // 严禁缺省回落 user1 等固定账号:那会把撤回人静默记成别人,审计链失真且不报错
  let operator = arg_required_str(args, "operator", "operator 必填")
  let inst = match (self.repo).find_instance_by_id(id) {
    Some(i) => i
    None => raise @error.InstanceNotFound(id)
  }
  if !(self.can_withdraw(inst, operator)) {
    raise @error.Business("无权限撤回该流程实例")
  }
  // C28:实例 30 + 进行中任务 30(级联持久化);已完成(20)/已终止(40) 行不经聚合根下发,不被改写
  // issues/134 案 A:实例状态守卫(非进行中(10) ⇒ 内部码 20010009 + 固定文案)在聚合根 withdraw 内,
  // 排在任务行循环之前 ⇒ 被拒时这里一行都没改,下面的 update_task/update_instance 一次都不执行(不落库)。
  inst.withdraw(operator)
  for t in inst.tasks {
    if t.task_state == @model.TaskState::Withdraw.code() {
      (self.repo).update_task(t)
    }
  }
  (self.repo).update_instance(inst)
  // issues/132 code 8 TASK_WITHDRAW:实例 state→30(update_instance)与被撤任务行(update_task)
  // 均落库之后 fire;每轮撤回只 fire 一次(sourceId=instanceId,载荷 instanceId/operator)。
  (self.engine).notify_instance_withdraw(inst.instance_id, operator)
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::approval_record(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_i64(args, "id") {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  let history = (self.repo).find_history_tasks(id)
  let rows = @json.empty_array()
  for t in history {
    let vo = @json.empty_object()
    @json.obj_set(vo, "taskName", @json.string_of(t.task_name))
    @json.obj_set(vo, "displayName", @json.string_of(t.display_name))
    @json.obj_set(vo, "taskType", num_json(t.task_type))
    @json.obj_set(vo, "performType", num_json(t.perform_type))
    @json.obj_set(vo, "taskState", num_json(t.task_state))
    @json.obj_set(vo, "operator", opt_json(t.actor_id))
    @json.obj_set(vo, "finishTime", opt_json(t.finish_time))
    @json.obj_set(vo, "ext", t.variables.to_object_json()) // issues/124:variable 原串出口下线
    @json.arr_push(rows, vo)
  }
  rows
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::get_assignee_text_data(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_i64(args, "id") {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  let include_node_name = match args.get("includeNodeName") {
    Some(Json::False) => false
    _ => true
  }
  let doing = (self.repo).find_doing_tasks(id, [])
  let rows = @json.empty_array()
  for t in doing {
    let actors = (self.repo).find_task_actors(t.task_id)
    for actor in actors {
      let item = @json.empty_object()
      @json.obj_set(item, "value", @json.string_of(actor))
      @json.obj_set(item, "label", @json.string_of(if include_node_name {
        "\{t.display_name}:\{actor}"
      } else {
        actor
      }))
      @json.arr_push(rows, item)
    }
  }
  rows
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::create_cc_instance(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let instance_id = match arg_i64(args, "processInstanceId") {
    Some(v) => v
    None => raise @error.Business("processInstanceId 缺失")
  }
  let operator = operator_arg(args)
  // issues/141 G10(spec 06 §2.10「空抄送人不建 cc 行」)+ issues/142 B 批(§2.11 同一枚判据):
  // 手动腿与两条引擎腿共用 @engine.parse_actors 的形状适配,判据只在 @model.normalize_actors
  // 那一处 —— 空串/纯空白/数组里的空元素一律丢弃,落库与比较取 trim 后的值,同调用折叠
  // (改前本行是"再归一一次",因为 arg_actor_ids 的数组臂不 trim ⇒ [" a "] 与 "a" 判成两个人)。
  // 丢完为空 ⇒ 与下面这条既有的"空集合"档同判(沿用 `actorIds 缺失`,不新造错误码/文案)。
  let actors = arg_actor_ids(args)
  if actors.is_empty() {
    raise @error.Business("actorIds 缺失")
  }
  // issues/141 G2 写侧判重=幂等空操作(spec 06 §4):手动腿与引擎腿同一条判据
  // (spec §11.7「三条入口共用一支」)——已有 cc 行的 (实例, 人) 跳过,不新增行、不重置未读、
  // 不更新原行时间;只有**实际新建的子集**拿去 fire。
  let created = (self.repo).create_cc_instance_if_absent(instance_id, operator, actors)
  // issues/127·132 事件代码腿:手动 createCCInstance 也 fire CC_CREATE(spec §11.2 原则1
  // ——「新增一条抄送记录」这一事实成立;§4.5 原话:java 不 fire 是缺,本栈按基准补齐)。
  // cc 行已在上一行落库 ⇒ 逐抄送人 fire(与发起 f_ccActors/办理 tf_ccActors 同 notify_cc_create 通道)。
  // 入参=实际新建子集(issues/141 G2):重复抄送没发生"创建"⇒ 不发码 4,子集为空整支不发。
  if !created.is_empty() {
    (self.engine).notify_cc_create(instance_id, created)
  }
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::update_cc_status(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let instance_id = match arg_i64(args, "processInstanceId") {
    Some(v) => v
    None => raise @error.Business("processInstanceId 缺失")
  }
  // issues/142 §9.2 复核补(spec 06 §2.11 表第四行):operator 归一后再交给仓储比——
  // operator_arg 已把空档回落 demo 缺省 user1(issues/129 案 A),这里补 trim 那半:
  // 不 trim 则「 9101 」打不中库里 trim 后的行(点了已读没反应)。与 java
  // normalizeActor(operatorArg(args)) 同构,其 error(「operator 必填」) 分支因回落
  // 恒非空而不可达,本栈 None ⇒ 同文案保形。
  let operator = match @model.normalize_actor(operator_arg(args)) {
    Some(op) => op
    None => raise @error.Business("operator 必填")
  }
  (self.repo).update_cc_status(instance_id, operator)
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::cc_list(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let page_num = arg_i64_or(args, "pageNum", 1L)
  let page_size = arg_i64_or(args, "pageSize", 20L)
  let filters = facade_parse_m_filters(args)
  let query = @model.PageQuery::new(page_num, page_size)
  query.set_operator(Some(operator_arg(args)))
  let page = (self.repo).page_cc_instances(query)
  finalize_page(page, instance_row_to_json, filters, page_num, page_size)
}

// ═══ 流程任务 ═══

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::todo_list(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let page_num = arg_i64_or(args, "pageNum", 1L)
  let page_size = arg_i64_or(args, "pageSize", 20L)
  let filters = facade_parse_m_filters(args)
  let query = @model.PageQuery::new(page_num, page_size)
  query.set_operator(Some(operator_arg(args)))
  let page = (self.repo).page_todo_tasks(query)
  finalize_page(page, task_row_to_json, filters, page_num, page_size)
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::done_list(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let page_num = arg_i64_or(args, "pageNum", 1L)
  let page_size = arg_i64_or(args, "pageSize", 20L)
  let filters = facade_parse_m_filters(args)
  let query = @model.PageQuery::new(page_num, page_size)
  query.set_operator(Some(operator_arg(args)))
  let page = (self.repo).page_done_tasks(query)
  finalize_page(page, task_row_to_json, filters, page_num, page_size)
}

///|
/// 提交任务(submitType 分发,java execute 同款)
pub async fn[R : @spi.ProcessRepository, E : @spi.ProcessExtRepository] Facade::execute(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let task_id = match arg_i64(args, "processTaskId") {
    Some(v) => v
    None => raise @error.Business("缺少processTaskId参数")
  }
  let operator = operator_arg(args)
  let submit_type = arg_i64_or(args, "submitType", 1L)
  let flow_args = args_to_flow_data(args, ["processTaskId", "operator"])
  flow_args.insert_i64("submitType", submit_type)
  if submit_type == 2L {
    // REJECT → 跳转结束(办结语义合并派)
    let _ = (self.engine).execute_and_jump_to_end_async(task_id, operator, flow_args)
  } else if submit_type == 3L {
    // ROLLBACK → 沿首入边回退(首任务节点)
    let _ = (self.engine).execute_and_jump_async(task_id, operator, flow_args, None)
  } else if submit_type == 4L {
    // JUMP → 指定节点(非法名 → "无法找到节点模型")
    let task_name = arg_str_or(args, "taskName", "")
    let _ = (self.engine).execute_and_jump_async(task_id, operator, flow_args, Some(task_name))
  } else if submit_type == 6L {
    // ROLLBACK_TO_OPERATOR → 首任务节点重执行给发起人
    let _ = (self.engine).execute_and_jump_to_first_async(task_id, operator, flow_args)
  } else if submit_type == 20L {
    // 会签不同意 → 注入 flag 普通执行(20 软拒绝,C8)
    flow_args.insert_str("countersignDisagreeFlag", "1")
    let _ = (self.engine).execute_task_async(task_id, operator, flow_args)
  } else {
    // 0 APPLY / 1 AGREE / 5 重新提交 → 普通执行
    let _ = (self.engine).execute_task_async(task_id, operator, flow_args)
  }
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::task_detail(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let id = match arg_id(args, ["processTaskId", "id"]) {
    Some(v) => v
    None => raise @error.Business("缺少id参数")
  }
  let operator = operator_arg(args)
  let task = match (self.repo).find_task_by_id(id) {
    Some(t) => t
    None => raise @error.TaskNotFound(id)
  }
  let vo = task_vo(task)
  let actors = (self.repo).find_task_actors(id)
  let actor_list = @json.empty_array()
  for a in actors {
    @json.arr_push(actor_list, @json.string_of(a))
  }
  @json.obj_set(vo, "taskActorIdList", actor_list)
  @json.obj_set(vo, "executable", @json.bool_of(task.is_allowed(operator)))
  // issues/82-5:任务级 ext.isFirstTaskNode(java taskDetail 写 false 兜底 + define 匹配时修正)
  let t_ext = parse_json_map(Some(@json.stringify(task.variables.to_object_json())))
  // 行上值(P1 起建单必写);只有存量行没这个键时才走 false 兜底 + define 匹配修正
  let row_first = t_ext.get("isFirstTaskNode")
  if row_first == None {
    t_ext["isFirstTaskNode"] = @json.bool_of(false)
  }
  let inst = (self.repo).find_task_by_id(id) |> ignore
  let _ = inst
  @json.obj_set(vo, "ext", obj_json(t_ext))
  // jsonObject + taskModel
  let doing_task = task
  let instance_opt = (self.repo).find_instance_by_id(doing_task.process_instance_id)
  match instance_opt {
    Some(instance) => {
      let define_info = (self.repo).find_define_by_id(instance.define_id)
      let json_object = match define_info {
        Some(d) => parse_graph(d.content)
        None => None
      }
      @json.obj_set(vo, "jsonObject", opt_json_wrap(json_object))
      match define_info {
        Some(d) => {
          if row_first == None {
            // 存量行才回退现算(仅进行中口径);新行一律以落库值为准
            t_ext["isFirstTaskNode"] = @json.bool_of(
              doing_task.is_doing() && doing_task.task_name == first_task_node_id(json_object).unwrap_or(""),
            )
          }
          @json.obj_set(vo, "ext", obj_json(t_ext))
          let model = @parser.parse_model(d.content)
          for node in model.nodes {
            if node.id == doing_task.task_name {
              let tm = @json.empty_object()
              @json.obj_set(tm, "name", @json.string_of(node.id))
              @json.obj_set(tm, "displayName", @json.string_of(node.display_name))
              @json.obj_set(tm, "type", @json.string_of(node_type_name(node.node_type)))
              // issues/62:taskModel 补 form/ext(C14/C19)
              @json.obj_set(tm, "form", opt_json(node.form_key()))
              let ext_obj = @json.empty_object()
              match node.properties.get("ext") {
                Some(e) =>
                  match e {
                    Json::Object(m) =>
                      for k, v in m {
                        @json.obj_set(ext_obj, k, v)
                      }
                    _ => ()
                  }
                None => ()
              }
              @json.obj_set(tm, "ext", ext_obj)
              @json.obj_set(vo, "taskModel", tm)
              break
            }
          }
        }
        None => ()
      }
    }
    None => ()
  }
  vo
}

///|
fn node_type_name(t : @parser.NodeType) -> String {
  t.as_str()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::jump_able_task_name_list(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let instance_id = match arg_i64(args, "processInstanceId") {
    Some(v) => v
    None => raise @error.Business("processInstanceId 缺失")
  }
  let done = (self.repo).find_done_tasks(instance_id, [])
  let rows = @json.empty_array()
  let seen : Map[String, Unit] = Map([])
  for t in done {
    if t.perform_type == 1 {
      continue
    }
    if seen.get(t.task_name) is Some(_) {
      continue
    }
    seen[t.task_name] = ()
    let item = @json.empty_object()
    @json.obj_set(item, "label", @json.string_of(t.display_name))
    @json.obj_set(item, "value", @json.string_of(t.task_name))
    @json.arr_push(rows, item)
  }
  rows
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::candidate_page(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let task_id = match arg_id(args, ["processTaskId", "id"]) {
    Some(v) => v
    None => raise @error.Business("processTaskId 缺失")
  }
  let task = match (self.repo).find_task_by_id(task_id) {
    Some(t) => t
    None => raise @error.TaskNotFound(task_id)
  }
  let instance = match (self.repo).find_instance_by_id(task.process_instance_id) {
    Some(i) => i
    None => raise @error.InstanceNotFound(task.process_instance_id)
  }
  let define_info = match (self.repo).find_define_by_id(instance.define_id) {
    Some(d) => d
    None => raise @error.DefineNotFound(instance.define_id)
  }
  // 模型候选:当前节点输出边指向的任务节点静态参与人
  let model = @parser.parse_model(define_info.content)
  let node = model.get_node(task.task_name)
  let candidates : Array[String] = []
  match node {
    Some(n) => {
      for e in model.get_output_edges(n.id) {
        match model.get_target_node(e) {
          Some(next) =>
            if next.node_type is @parser.Task {
              match next.assignee() {
                Some(a) => {
                  for actor in split_comma(a) {
                    candidates.push(actor)
                  }
                }
                None => ()
              }
            }
          None => ()
        }
      }
    }
    None => ()
  }
  let rows = @json.empty_array()
  if !candidates.is_empty() {
    for actor in candidates {
      // provider: `(String) -> UserInfo? raise JeeflowError`;取不到或报错 ⇒ 空串(旧语义)
      let info = match self.engine.context().user_provider {
        Some(get_user) =>
          try {
            match (get_user)(actor) {
              Some(u) => u.real_name()
              None => ""
            }
          } catch {
            _ => ""
          }
        None => ""
      }
      @json.arr_push(rows, candidate_row(actor, info, None))
    }
    // 分页信封(pageNum=1 pageSize=rows)
    let obj = @json.empty_object()
    @json.obj_set(obj, "pageNum", @json.number_of(1L))
    @json.obj_set(obj, "pageSize", @json.number_of(10L))
    @json.obj_set(obj, "recordCount", @json.number_of(candidates.length().to_int64()))
    @json.obj_set(obj, "totalPage", @json.number_of(1L))
    @json.obj_set(obj, "rows", rows)
    return obj
  }
  // 无模型候选 → 用户搜索分页
  match self.engine.context().user_search_provider {
    Some(usp) => {
      let page = (usp.page)(@model.PageQuery::new(1L, 10L))
      let out = @json.empty_object()
      @json.obj_set(out, "pageNum", @json.number_of(page.page_num()))
      @json.obj_set(out, "pageSize", @json.number_of(page.page_size()))
      @json.obj_set(out, "recordCount", @json.number_of(page.record_count()))
      @json.obj_set(out, "totalPage", @json.number_of(page.total_page()))
      let arr = @json.empty_array()
      for row in page.rows() {
        @json.arr_push(arr, obj_json(row))
      }
      @json.obj_set(out, "rows", arr)
      out
    }
    None => raise @error.Business("未配置 IUserSearchProvider(用户搜索钩子)")
  }
}

///|
/// 逗号串切元素(出口侧候选人名册用):issues/142 B 批把判据并进 @model.normalize_actors
/// 那一枚单点,本函数只负责"切成元素",不再本地各写一份 trim/is_empty(两把尺子迟早分叉)。
fn split_comma(s : String) -> Array[String] {
  @model.normalize_comma_actors(s)
}

///|
/// 加签(addTaskActor=去重追加非全删全插,C14)
pub async fn[R : @spi.ProcessRepository, E] Facade::task_surrogate(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let task_id = match arg_i64(args, "processTaskId") {
    Some(v) => v
    None => raise @error.Business("processTaskId/actorIds 缺失")
  }
  // 主键另判一档(spec 06-facade §2.11 末段,issues/142 B 批):`processTaskId` 缺失/空串/非正数
  // 都是调用方写错了,必须响亮报错,**不得拿 0 当 id 往下落参与者行**。空串档在 arg_i64 已落
  // 既有"非法id"信封;0/负数与缺键同判这一档 —— 沿用既有错误码与文案(§2.11 要求③),
  // 与"归属值为空 ⇒ 丢弃"是两件事:归属值可有可无,主键没有就是错,静默接受会把脏数据钉进表里。
  if task_id <= 0L {
    raise @error.Business("processTaskId/actorIds 缺失")
  }
  // issues/142 B 批(§2.11「两形同判据+写侧兜底+trim+哨兵」):arg_actor_ids 现在自带
  // @engine.parse_actors 的形状适配 + @model.normalize_actors 那一枚判据(逐元素 trim、
  // 空串/纯空白丢弃、同一次调用折叠、数字元素按 repr 原文收敛成串),与紧邻的
  // create_cc_instance 从此同一把尺子;绕过门面直连仓储的调用方由两仓 add_task_actor 再挡一次。
  let actors = arg_actor_ids(args)
  if actors.is_empty() {
    raise @error.Business("processTaskId/actorIds 缺失")
  }
  (self.repo).add_task_actor(task_id, actors)
  @json.null_json()
}

///|
/// 转办末跳可读文案:`A 转办给 B(原因…)`(无原因不带括号;前端审批意见既有读取位)
fn transfer_text(from_actor : String, to_actor : String, reason : String) -> String {
  let head = "\{from_actor} \{@model.SubmitType::Transfer.message()}给 \{to_actor}"
  if reason.is_empty() {
    head
  } else {
    "\{head}(\{reason})"
  }
}

///|
/// 转办(issues/115,spec/06 §processTask/transfer 七条语义):
/// 摘原人 → 加新人 → 同一 taskId 不新建 → 三件留痕(submitType=7 槽位 + tf_transferHistory
/// 追加式账本 + tf_transferTo/tf_transferReason 便捷键 + 末跳 tf_approvalComment)→
/// 去重/前置态四类明确报错。区别于 processTask/surrogate 加签的"只追加不动原人"。
pub async fn[R : @spi.ProcessRepository, E] Facade::task_transfer(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let task_id = match arg_i64(args, "processTaskId") {
    Some(v) => v
    None => raise @error.Business("缺少processTaskId参数")
  }
  let operator = arg_required_str(args, "operator", "operator 必填")
  let from_actor = arg_required_str(args, "fromActor", "fromActor 必填")
  let to_actor = arg_required_str(args, "toActor", "toActor 必填")
  let task = match (self.repo).find_task_by_id(task_id) {
    Some(t) => t
    None => raise @error.TaskNotFound(task_id)
  }
  // 归属判据:只能转自己那一条待办(flow.auto/flow.admin 除外),与撤回同口径
  if !is_privileged_operator(operator) && operator != from_actor {
    raise @error.Business("无权限转办该任务")
  }
  // 前置态:仅进行中(DOING=10)任务可转办
  if task.task_state != @model.TaskState::Doing.code() {
    raise @error.Business("任务非进行中,不可转办")
  }
  // 以参与者表为判据(聚合副本会滞后于加签/转办的增量写入)
  let actors = (self.repo).find_task_actors(task_id)
  if !actors.contains(from_actor) {
    raise @error.Business("原办理人不是该任务参与人")
  }
  if actors.contains(to_actor) {
    raise @error.Business("目标人已是该任务参与人")
  }
  // 语义 1+2+3:只摘 fromActor 一行(会签其余成员不受影响)、toActor 追加、沿用同一 taskId
  (self.repo).remove_task_actor(task_id, [from_actor])
  (self.repo).add_task_actor(task_id, [to_actor])
  // 语义 4 留痕①:跨跳追加式账本 tf_transferHistory——每跳 append,只追加不覆盖
  let reason = arg_str(args, "reason").unwrap_or("").trim(chars=" \t\r\n").to_owned()
  let history = @json.empty_array()
  match task.variables.get("tf_transferHistory") {
    Some(Json::Array(items)) =>
      for it in items {
        @json.arr_push(history, it)
      }
    _ => ()
  }
  // 六键固定 camelCase;reason 无值写 "";time 一律 yyyy-MM-dd HH:mm:ss(spec §2.4,
  // 不得用本地 ISO 方言——账本要跨栈同形)
  @json.arr_push(
    history,
    @json.object_of([
      ("submitType", @json.number_of(@model.SubmitType::Transfer.code())),
      ("fromActor", @json.string_of(from_actor)),
      ("toActor", @json.string_of(to_actor)),
      ("reason", @json.string_of(reason)),
      ("time", @json.string_of(@model.current_time_str())),
      ("operator", @json.string_of(operator)),
    ]),
  )
  task.variables.insert("tf_transferHistory", history)
  // 留痕①当前槽位 + ②单跳便捷键 + ③末跳可读文案(B 办结后槽位由 1/2/20 覆盖,属预期)
  task.variables.insert_i64("submitType", @model.SubmitType::Transfer.code())
  task.variables.insert_str("tf_transferTo", to_actor)
  task.variables.insert_str("tf_transferReason", reason)
  task.variables.insert_str("tf_approvalComment", transfer_text(from_actor, to_actor, reason))
  // ⚠️ 契约第 4 条禁写项:**绝不覆写 task.actor_id**(进行中任务该列恒无值是既有不变量;
  // 把被摘走的人写进去,该单一旦撤回/终止就会凭空出现在他从没办过的 doneList——Node 实测)。
  // "办理人记谁"由 update_user + tf_transferHistory[].operator 承载。
  task.update_user = Some(operator)
  task.update_time = Some(@model.current_time_str())
  // 参与者表是真相:同步副本后再落库(内存仓 page_todo_tasks 按 actor_ids 过滤,
  // 不同步则 A 的待办不消失、B 的待办不出现;SQL 仓 update_task 也会按副本覆写)
  task.actor_ids = (self.repo).find_task_actors(task_id)
  (self.repo).update_task(task)
  // issues/132 code 7 TASK_TRANSFER:摘原人+加新人(参与者表)与副本 update_task 均落库之后 fire。
  // sourceId=taskId,载荷 instanceId/taskId/fromActor/toActor/operator。
  (self.engine).notify_task_transfer(task.process_instance_id, task_id, from_actor, to_actor, operator)
  @json.null_json()
}

///|
pub async fn[R : @spi.ProcessRepository, E] Facade::task_latest(self : Facade[R, E], args : Map[String, Json]) -> Json raise @error.JeeflowError {
  let instance_id = match arg_i64(args, "processInstanceId") {
    Some(v) => v
    None => raise @error.Business("processInstanceId 缺失")
  }
  let doing = (self.repo).find_doing_tasks(instance_id, [])
  if doing.is_empty() {
    @json.null_json()
  } else {
    task_vo(doing[0])
  }
}