import { ApiCode } from '@pac/types';
import { BizError } from '../../common/errors/biz-error';

/**
 * 认领闸 —— 「未认领只能看,不能操作」的服务端硬约束。
 *
 * 为什么必须在服务端拦(不能只做前端禁用):
 *   1. **执行必须有归属**。execution.service 特意保留 assigneeUserId 作业绩归属
 *      (见那里的注释:清了会让「我的已完成」/ 转化率永远为 0)。未认领就提交执行 =
 *      这条执行没有任何归属人,统计里直接蒸发 —— 前端提示拦不住这个数据问题。
 *   2. **防撞单**。两个客服同时打同一个患者,只有服务端能仲裁。
 *   3. 生产实证(2026-07):缺牙召回有两条 plan 在 `assignee IS NULL` 状态下被直接关闭,
 *      当时 submitExecution 只校验了 status、完全没看 assignee。
 *
 * ⚠️ 适用边界 —— 不是所有写操作都该过这道闸:
 *   过闸:提交通话结果 / 关闭机会、重生成话术(含 SSE)、话术反馈、召回反馈
 *   不过闸:
 *     · `assign`(认领本身就是入口,过闸会变成死锁)
 *     · `recycle`(有自己的归属规则,见 assertCanRecycle —— 组长能回收任意人的单,
 *        按"必须自己认领"拦会把组长回收功能整个废掉)
 *     · `recompute`(ops/dev 工具,走 PLAN_ASSIGN 权限)
 *
 * 两种拒绝态分开报,文案不同:没人认领 → 引导去认领;别人认领了 → 说清是谁,
 * 否则组员会以为是自己没点对。
 */
export interface ClaimablePlan {
  assigneeUserId: string | null;
}

/**
 * 返池归属闸 —— staff 只能退**自己认领的**单(2026-07-29 放开 staff 返池时加)。
 *
 * 为什么必须有:光把 PLAN_RECYCLE 给 staff,客服 A 就能把客服 B 手里的单退回池、
 * 再自己认领 —— 认领闸挡的是"没认领就作业",挡不住"先把别人的单退了"。
 *
 * 分流依据是 canRecycleOthers(controller 按 PLAN_VIEW_ALL 传入),不是角色字符串:
 * 权限模型的真理源是 ROLE_PERMISSIONS,service 层不该再认一次角色。
 *
 * 无人认领(assignee=null)一律放行:那种"返池"本来就是空操作(recycle 里 hadAssignee=false
 * 不记账),拦它只会让前端多一种没意义的报错。
 */
export function assertCanRecycle(
  plan: ClaimablePlan,
  actorUserId: string | undefined,
  canRecycleOthers: boolean,
): void {
  if (canRecycleOthers) return;
  if (!plan.assigneeUserId) return;
  if (plan.assigneeUserId !== actorUserId) {
    throw new BizError(ApiCode.PLAN_CLAIMED_BY_OTHER, undefined, {
      assigneeUserId: plan.assigneeUserId,
    });
  }
}

export function assertPlanClaimedBy(plan: ClaimablePlan, userId: string): void {
  if (!plan.assigneeUserId) {
    throw new BizError(ApiCode.PLAN_NOT_CLAIMED);
  }
  if (plan.assigneeUserId !== userId) {
    // details 带上占用人,前端可查字典换成姓名展示("已被 张三 认领")
    throw new BizError(ApiCode.PLAN_CLAIMED_BY_OTHER, undefined, {
      assigneeUserId: plan.assigneeUserId,
    });
  }
}
