import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import {
  buildOrgTree,
  buildTree,
  expandOrgScope,
  type ExpandedScope,
  type OrgTree,
} from './org-scope';

/**
 * host org 树解析 + scope 展开 —— **从摄入数据派生,零硬编码**。
 *
 * 设计 B:JWT 只装通用 orgScope;**每个请求**在 tenant-scope 拦截器 / MCP 鉴权处调
 * expandScope() 按 host org 树展开成内部过滤维(sourceUnits + clinicIds)。
 *
 * 树不写死任何 id —— 从 `patient_transactions`(立柱 clinic_id + source_unit)派生
 * 「品牌 → 诊所」映射,对**任何 host、任何环境**自适应(指到别的 DW 自动跟着变)。
 * 按 (hostId, tenantId) 缓存(TTL);数据日更,够鲜。无数据 → 退化树(根=tenantId,无子)。
 */
@Injectable()
export class OrgTreeService {
  private readonly logger = new Logger(OrgTreeService.name);

  constructor(private readonly prisma: PrismaService) {}

  private readonly cache = new Map<string, { tree: OrgTree; at: number }>();
  /** 单飞:并发 miss 共享同一次派生,防惊群 —— 冷启动 bootstrap(loadSession + plans 列表)
   *  多个请求同时 miss 时,原本各自全扫一遍 patient_transactions(数十万行)导致登录长时间卡。 */
  private readonly inflight = new Map<string, Promise<OrgTree>>();
  // org 结构随摄入数据日更(仅新诊所/品牌才变),不必频繁重派生。
  // 现在 SyncIncrementalSchedulerService 每日同步完会主动 forceRefresh 预热(见该文件),
  // 稳态下这个 TTL 基本不会真被撞到 —— 它只是兜底(cron 没配 / 新 host 还没跑过同步的安全网),
  // 故拉到 24h(留 cron 延迟余量),比预热周期长即可,不必再纠结"空闲后重开多久算太久"。
  private readonly ttlMs = 24 * 60 * 60 * 1000;
  private readonly refreshMinMs = 30 * 1000; // 强制重建防抖:30s 内不重复查(挡 bogus ref 刷库)

  /** 取 host org 树(缓存;miss/过期或 forceRefresh 时从摄入数据派生;并发单飞)。 */
  async getTree(hostId: string, tenantId: string, forceRefresh = false): Promise<OrgTree> {
    const key = `${hostId}:${tenantId}`;
    const hit = this.cache.get(key);
    const age = hit ? Date.now() - hit.at : Infinity;
    if (hit && !forceRefresh && age < this.ttlMs) return hit.tree;
    if (hit && forceRefresh && age < this.refreshMinMs) return hit.tree; // 防抖

    // 单飞:已有在途派生 → 复用,不再各自全扫(冷并发只跑 1 次)。
    const inflight = this.inflight.get(key);
    if (inflight) return inflight;

    const p = this.deriveTree(hostId, tenantId)
      .then((tree) => {
        this.cache.set(key, { tree, at: Date.now() });
        return tree;
      })
      .finally(() => this.inflight.delete(key));
    this.inflight.set(key, p);
    return p;
  }

  /** 从摄入数据派生「品牌(source_unit)→ 诊所(clinic_id)」org 树(零硬编码)。 */
  private async deriveTree(hostId: string, tenantId: string): Promise<OrgTree> {
    const startedAt = Date.now();
    // clinic_id 立柱在 patient_transactions,source_unit 在 patients → join patient_id。
    // 显式 host+tenant 过滤(此处在 ALS 之前跑,纵深防御扩展不兜,自带条件)。
    //
    // 结果恒个位数行,但 planner 按行数估算走 JIT——生产实测(379万行)占了近 20% 执行时间,
    // 纯浪费。SET LOCAL 只在本事务生效,COMMIT 后自动复原,不污染连接池里其它请求。
    const [, rows] = await this.prisma.$transaction([
      this.prisma.$executeRaw`SET LOCAL jit = off`,
      this.prisma.$queryRaw<{ source_unit: string; clinic_id: string }[]>`
        SELECT DISTINCT p.source_unit, pt.clinic_id
        FROM patient_transactions pt
        JOIN patients p ON p.id = pt.patient_id
        WHERE pt.host_id = ${hostId}::uuid AND pt.tenant_id = ${tenantId}
          AND p.source_unit <> '' AND pt.clinic_id <> ''
      `,
    ]);
    const elapsed = Date.now() - startedAt;
    // >1s 才 warn —— 冷派生本身预期慢(全表扫),日常预热命中不会看到这条;
    // 留证据是为了"用户说卡"时能在日志里对上号,而不是靠猜。
    if (elapsed > 1000) {
      this.logger.warn(`org-tree 冷派生慢:host=${hostId} tenant=${tenantId} rows=${rows.length} elapsed=${elapsed}ms`);
    } else {
      this.logger.log(`org-tree 派生:host=${hostId} tenant=${tenantId} rows=${rows.length} elapsed=${elapsed}ms`);
    }
    const brandClinics: Record<string, string[]> = {};
    for (const r of rows) {
      (brandClinics[r.source_unit] ??= []).push(r.clinic_id);
    }
    return Object.keys(brandClinics).length > 0
      ? buildOrgTree(tenantId, brandClinics)
      : buildTree({ id: tenantId }); // 无数据 → 退化树
  }

  /** 把 orgScope(任意层级 org 节点)展开成内部过滤维 { sourceUnits, clinicIds }。 */
  async expandScope(
    hostId: string,
    tenantId: string,
    orgScope: string[],
  ): Promise<ExpandedScope> {
    // 空 orgScope = 集团级不限(admin / 全集团用户,最常见)—— 结果恒为 {[],[]},
    // 无需派生 org 树。短路掉,避免这类用户每次(尤其冷启动)白扫 patient_transactions。
    if (orgScope.length === 0) return { sourceUnits: [], clinicIds: [] };
    let r = expandOrgScope(await this.getTree(hostId, tenantId), orgScope);
    // 有 ref 树里找不到 → 树缓存可能陈旧(新诊所/品牌刚摄入),强制重派生一次再展开,
    // 既避免误拒合法新诊所用户,也避免陈旧退化树把品牌制用户 fail-open(见 org-scope 的 DENY)。
    if (r.unresolved.length > 0) {
      r = expandOrgScope(await this.getTree(hostId, tenantId, true), orgScope);
    }
    return { sourceUnits: r.sourceUnits, clinicIds: r.clinicIds };
  }
}
