import { ForbiddenException } from '@nestjs/common';
import { Permission } from '@pac/types';

/**
 * 分配写路径的两道通用闸 —— **独立于传输层**(REST / MCP / 将来别的入口都能用)。
 *
 * ── 为什么现在就写,而 v1 的写路径明明只有 REST ──────────────────
 * MCP 写工具(`assign_plans`)最终一定要实现,只是分期到 S4 等鉴权补齐。
 * 但 MCP 端点现在是 `@Public()`(`permissions.guard.ts` 对 public 直接 return true),
 * 也就是说**框架级的 @RequirePermission 在那条路上完全不生效**,
 * 护栏只能退化成 handler 内自查。等到那时才动手,写路径的形状很可能已经把自查挤不进去了。
 *
 * 所以这两个 helper 现在就落地、现在就被 REST 路径调用(与 guard 叠加,幂等),
 * S4 挂 MCP 工具时直接复用同一份判据 —— 而不是在第二个地方重写一遍权限逻辑。
 */

/** 调用方的最小身份形状(REST 从 JWT 来,MCP 从 McpAuthContext 来,两边都能满足) */
export interface DispatchActor {
  userId: string;
  permissions: readonly string[];
}

/**
 * 权限自查。
 *
 * ⚠️ 与 `@RequirePermission(PLAN_DISPATCH)` **重复是有意的**:
 * 装饰器只在 Nest 的 guard 链上生效,`@Public()` 的端点直接短路 ——
 * 而 MCP 恰好就是 `@Public()`。把判据同时放进 service,
 * 就不存在"换个入口进来护栏就没了"这回事。
 */
export function requirePermission(actor: DispatchActor, permission: Permission): void {
  if (!actor.permissions.includes(permission)) {
    throw new ForbiddenException(`缺少权限:${permission}`);
  }
}

/**
 * 合成身份前缀 —— 这些 id 不对应任何真人。
 * 目前只有企微机器人:`weixin-aibot.service.mintToken` 会造一个 `role='staff'` + 全池 scope
 * 的 token,给群里任何 @ 机器人的人用。读没问题(本来就是给客服查患者的),
 * **写绝对不行** —— 那等于让"谁在群里说话"决定几百条单子的归属,而且账本上的
 * `created_by` 会指向一个查无此人的 id,事后追责追不到人。
 */
const SYNTHETIC_ID_PREFIXES = ['wx:'] as const;

/**
 * 拒绝合成身份发起写操作。
 *
 * 教条六·已定取舍写的是「企微通道的合成身份本期不管(demo 用途)」——
 * 那句话的前提是**当时还没有写路径**。有了写路径,"不管"就必须落成"硬拒",
 * 否则就是把一个已知的身份伪造面留在最危险的位置上。
 * 将来做真身份映射时,把映射成功的真 id 传进来即可自然通过。
 */
export function rejectSyntheticIdentity(actor: DispatchActor): void {
  if (SYNTHETIC_ID_PREFIXES.some((p) => actor.userId.startsWith(p))) {
    throw new ForbiddenException(
      '当前身份为渠道合成身份(非真实登录用户),不能发起分配。请在 PAC 工作台登录后操作。',
    );
  }
}
