/**
 * 组织范围(标准数据权限)—— 不建表,内存树 + 登录时展开。
 *
 * 数据权限 = 用户的「org 范围」(一组组织节点,任意层级)。
 * 登录时 expandOrgScope() 把 org 范围**展开**成两个过滤维度,塞进 token:
 *   - sourceUnits:范围覆盖的「患者归属层」(jvs-dw = 品牌)→ 过滤患者粒度
 *   - clinicIds:范围覆盖的诊所 → 过滤 plan(target_clinic_id)
 * 过滤层(patient.sourceUnit / plan.targetClinicId)已实现,这里只做"范围→两维"。
 *
 * ── 树是「无限层级」的递归结构 ────────────────────────────────────────────
 * 节点不分固定层数:集团 → (多级区域) → 品牌 → 诊所,甚至更深,都行。
 * 因为**过滤永远只落在两类叶子属性**上(sourceUnit / clinicId),中间层(区域等)
 * 只是分组——展开任意节点 = 走它的整棵子树,把子树里的 sourceUnit + clinicId 收集起来。
 * 所以加多少层区域都不改 expand 逻辑,新层只是多几层 children。
 *
 * 树本身是 host 配置(通用态从摄入数据/manifest 派生;此处先内置 jvs-dw)。
 */

/** 组织树节点 —— 递归,层数不限。 */
export interface OrgNode {
  /** 节点 id(scope ref 用它命中:集团 id / 区域 id / 品牌名 / 诊所 id 都行)。 */
  id: string;
  /**
   * 患者归属命名空间(= patients.source_unit)。只有「发证层」节点带它
   * (jvs-dw = 品牌;别的 host 按诊所发号则诊所节点带)。区域/集团等中间层不带。
   */
  sourceUnit?: string;
  /**
   * 额外可命中的 scope ref(与 id 等价匹配)。用于"PAC 内部节点 id ≠ 宿主契约 id"的层:
   * 如品牌层 PAC 用名字("瑞尔")当 id,但宿主 orgScope 按契约传品牌 GUID
   * (`ba67e6cf…`)—— 把 GUID 挂这里,名字 / GUID 两种传法都能命中,不动 source_unit。
   */
  aliases?: string[];
  /** 诊所 id。只有叶子诊所节点带。 */
  clinicId?: string;
  /** 子节点。无限嵌套。 */
  children?: OrgNode[];
}

export interface OrgTree {
  /** 根节点 id(集团)。scope 命中根 = 不限(看全树)。 */
  groupId: string;
  root: OrgNode;
}

/**
 * 便捷构建器:从「品牌 → 诊所 id 列表」构建 3 层树(集团/品牌/诊所)。**不 hardcode**
 * —— 调用方从已有配置/数据派生(mock 从 MOCK_PRESETS;通用态从摄入数据)。
 *
 * 需要区域等更多层时,直接用 OrgNode 手搓任意深度的 root(见 buildTree),
 * 这个 helper 只是最常见 3 层形状的糖。
 */
export function buildOrgTree(
  groupId: string,
  brandClinics: Record<string, string[]>,
  /** 品牌名 → 宿主品牌 GUID(可选)。挂到品牌节点 aliases,让契约 orgScope(品牌 GUID)也能命中。 */
  brandAliases?: Record<string, string>,
): OrgTree {
  const root: OrgNode = {
    id: groupId,
    children: Object.entries(brandClinics).map(([brand, clinics]) => ({
      id: brand,
      ...(brandAliases?.[brand] ? { aliases: [brandAliases[brand]] } : {}),
      sourceUnit: brand,
      children: clinics.map((c) => ({ id: c, clinicId: c })),
    })),
  };
  return { groupId, root };
}

/** 通用构建器:直接给整棵递归树(任意层级,含多级区域)。 */
export function buildTree(root: OrgNode): OrgTree {
  return { groupId: root.id, root };
}

export interface ExpandedScope {
  /** 患者归属层范围(sourceUnit)。空数组 = 不限(看全树所有归属层) */
  sourceUnits: string[];
  /** 诊所范围。空数组 = 不限(看全树所有诊所) */
  clinicIds: string[];
}

/** expandOrgScope 完整结果:多带 unresolved(树里找不到的 ref),供调用方决定是否刷新树重试。 */
export interface ExpandResult extends ExpandedScope {
  unresolved: string[];
}

/**
 * 哨兵 source_unit —— 匹配不到任何真实 source_unit。
 * fail-closed 用:展开失败(品牌制租户却没解析出品牌)时塞它,让患者查询返回空(看不到),
 * 而不是 sourceUnits=[] 被下游当成"不限品牌"→ 跨品牌泄漏。
 */
export const DENY_SOURCE_UNIT = '__pac_deny_no_brand__';

/**
 * 收集一棵子树下所有的 sourceUnit / clinicId(到累加器)。
 * sourceUnit(命名空间)**向下继承**:诊所叶子自身不带 sourceUnit,但归属于其上的品牌
 * —— 用 inheritedSu 把祖先命名空间带下来,否则 scope 到单诊所会丢品牌、退化成不限品牌(越权)。
 */
function collectSubtree(
  node: OrgNode,
  inheritedSu: string | undefined,
  sourceUnits: Set<string>,
  clinicIds: Set<string>,
): void {
  const su = node.sourceUnit ?? inheritedSu;
  if (su) sourceUnits.add(su);
  if (node.clinicId) clinicIds.add(node.clinicId);
  for (const child of node.children ?? []) collectSubtree(child, su, sourceUnits, clinicIds);
}

/**
 * 把一组 org 节点 ref 展开成 { sourceUnits, clinicIds }。ref 可是树上**任意层级**:
 *   - 根(集团 id)→ 不限(sourceUnits=[], clinicIds=[])
 *   - 任意中间层(如区域 / 多级区域 / 品牌)→ 该节点整棵子树的 sourceUnit + clinic
 *   - 诊所叶子 → 该诊所 + 其所属命名空间
 * 多个 ref 取并集。命中根即整体不限。
 *
 * **fail-closed**:品牌制租户(树里存在非空 source_unit)下,非空 scope 却没解析出任何品牌
 * (ref 全是树外未知 id —— 退化树 / 新诊所未进树 / 缓存陈旧),**不退化成"不限品牌"**,
 * 而是塞 DENY 哨兵 → 患者查询返回空。否则因瑞尔/瑞泰共用 tenantId,sourceUnits=[] 会跨品牌泄漏。
 * unresolved 回传未知 ref,调用方可据此刷新树重试(避免误拒刚摄入的新诊所合法用户)。
 */
export function expandOrgScope(tree: OrgTree, refs: string[]): ExpandResult {
  if (refs.length === 0) return { sourceUnits: [], clinicIds: [], unresolved: [] }; // 空 = 不限

  // 扁平化:id → [{ 节点, 继承的命名空间 }]。同 id 可出现多处(如一个品牌跨多个区域),
  // 按数组**聚合**全收 —— 用 Map 直接覆盖会静默丢掉同名节点的其余出现。
  // 同时探测 branded:树里是否存在非空 source_unit(= 多命名空间 / 品牌制租户)。
  const index = new Map<string, Array<{ node: OrgNode; su: string | undefined }>>();
  let branded = false;
  (function walk(n: OrgNode, inheritedSu: string | undefined) {
    const su = n.sourceUnit ?? inheritedSu;
    if (n.sourceUnit) branded = true;
    // id + aliases 都建索引(别名与 id 等价命中,如品牌 GUID)
    for (const key of [n.id, ...(n.aliases ?? [])]) {
      (index.get(key) ?? index.set(key, []).get(key)!).push({ node: n, su });
    }
    for (const c of n.children ?? []) walk(c, su);
  })(tree.root, undefined);

  const sourceUnits = new Set<string>();
  const clinicIds = new Set<string>();
  const unresolved: string[] = [];

  for (const ref of refs) {
    if (ref === tree.groupId) {
      return { sourceUnits: [], clinicIds: [], unresolved: [] }; // 命中根 → 整体不限(唯一合法的不限来路)
    }
    const hits = index.get(ref);
    if (hits) {
      // 任意层级 → 把每处出现的子树都展开(带各自祖先命名空间)
      for (const h of hits) collectSubtree(h.node, h.su, sourceUnits, clinicIds);
      continue;
    }
    clinicIds.add(ref); // 未知 ref:当裸诊所 id 透传(plan 维度);记 unresolved
    unresolved.push(ref);
  }

  // fail-closed:品牌制租户却没解析出任何品牌 → 绝不当"不限品牌",塞哨兵 → 患者查询空。
  if (branded && sourceUnits.size === 0) {
    return { sourceUnits: [DENY_SOURCE_UNIT], clinicIds: [...clinicIds], unresolved };
  }
  return { sourceUnits: [...sourceUnits], clinicIds: [...clinicIds], unresolved };
}
