import { Injectable, Logger, OnApplicationBootstrap } 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 implements OnApplicationBootstrap {
  private readonly logger = new Logger(OrgTreeService.name);

  constructor(private readonly prisma: PrismaService) {}

  /**
   * 启动预热 —— 补上"每日 sync 预热"覆盖不到的空窗。
   *
   * 冷派生要全扫 patient_transactions(生产 379 万行,实测约 3-5s)。每日 sync 完
   * 会 forceRefresh 预热,但内存缓存**随进程重启/部署清空**:重启后到下次 sync 之间,
   * 第一个非集团级(orgScope 非空)用户登录就撞冷派生 —— 正是"首次无缓存"卡顿的来源。
   * 这里在应用启动就绪后(DB 已连)后台把各 (host,tenant) 树填进缓存,真实用户不再承担。
   *
   * 后台跑,不阻塞启动;单个 tenant 失败不影响其余。env 关:PAC_ORGTREE_WARM_ON_BOOT=false。
   */
  onApplicationBootstrap(): void {
    if (process.env.PAC_ORGTREE_WARM_ON_BOOT === 'false') return;
    void this.warmAll();
  }

  private async warmAll(): Promise<void> {
    const startedAt = Date.now();
    try {
      const hosts = await this.prisma.host.findMany({
        where: { active: true },
        select: { id: true },
      });
      let warmed = 0;
      for (const h of hosts) {
        const tenants = await this.prisma.patient.findMany({
          where: { hostId: h.id },
          distinct: ['tenantId'],
          select: { tenantId: true },
        });
        for (const t of tenants) {
          try {
            await this.getTree(h.id, t.tenantId); // 非 force:填冷缓存即可
            warmed++;
          } catch (err) {
            this.logger.warn(
              `org-tree 启动预热失败 host=${h.id} tenant=${t.tenantId}: ${(err as Error).message}`,
            );
          }
        }
      }
      this.logger.log(
        `org-tree 启动预热完成:${warmed} 个 (host,tenant) elapsed=${Date.now() - startedAt}ms`,
      );
    } catch (err) {
      // 预热是尽力而为的优化,查 host/tenant 失败不该拖垮启动(真实请求仍会按需派生)。
      this.logger.warn(`org-tree 启动预热跳过: ${(err as Error).message}`);
    }
  }

  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 之前跑,纵深防御扩展不兜,自带条件)。
    //
    // 朴素 `SELECT DISTINCT source_unit, clinic_id` 会全扫 patient_transactions(生产 379 万行、
    // ~470MB)就为拿个位数行的「品牌→诊所」映射 —— 冷缓存下这是登录尾延迟的根。改成两步、
    // 全走索引(实测 470MB→~60 页,3.8s→~1ms):
    //   ① loose index-scan:靠 (host_id,tenant_id,clinic_id) 索引"跳"着取 distinct clinic_id
    //      (递归 CTE 每次 seek clinic_id > 上一个,N 家诊所只 N 次索引下探,不读全表);
    //   ② 每诊所 LIMIT 1 采样一条患者的 source_unit 定其品牌。
    //
    // **前提(诊所×品牌 1:1)**:一家诊所只属一个品牌(source_unit = 拥有该诊所的品牌)——
    //   领域不变量,当前数据零反例。故 ① distinct 诊所 + ② 单条采样即可定归属;若脏数据让同一
    //   clinic_id 跨了品牌,② 只取其一(另一归属丢失),这类问题应在摄入侧治理,不在此兜。
    // enable_seqscan=off:LATERAL 里 `clinic_id = c.clinic_id`(值来自 CTE)行数估不准,planner
    //   会误选 seq scan;所需索引((host,tenant,clinic) + patients_pkey)都在,强制走索引安全。
    // SET LOCAL 只在本事务生效,COMMIT 自动复原,不污染连接池其它请求。
    const [, , rows] = await this.prisma.$transaction([
      this.prisma.$executeRaw`SET LOCAL jit = off`,
      this.prisma.$executeRaw`SET LOCAL enable_seqscan = off`,
      this.prisma.$queryRaw<{ source_unit: string; clinic_id: string }[]>`
        WITH RECURSIVE clinics AS (
          (SELECT clinic_id FROM patient_transactions
             WHERE host_id = ${hostId}::uuid AND tenant_id = ${tenantId} AND clinic_id <> ''
             ORDER BY clinic_id LIMIT 1)
          UNION ALL
          SELECT (SELECT pt.clinic_id FROM patient_transactions pt
                    WHERE pt.host_id = ${hostId}::uuid AND pt.tenant_id = ${tenantId}
                      AND pt.clinic_id <> '' AND pt.clinic_id > c.clinic_id
                    ORDER BY pt.clinic_id LIMIT 1)
          FROM clinics c WHERE c.clinic_id IS NOT NULL
        )
        SELECT c.clinic_id AS clinic_id, su.source_unit AS source_unit
        FROM clinics c
        CROSS JOIN LATERAL (
          SELECT p.source_unit
          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 pt.clinic_id = c.clinic_id AND p.source_unit <> ''
          LIMIT 1
        ) su
        WHERE c.clinic_id IS NOT NULL
      `,
    ]);
    const elapsed = Date.now() - startedAt;
    // 走索引后派生应恒 <100ms;>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);
    }
    // 品牌名 → 宿主品牌 GUID 别名(host 配置)。让契约 orgScope(品牌 GUID)也能命中品牌节点。
    const host = await this.prisma.host.findUnique({
      where: { id: hostId },
      select: { orgAliases: true },
    });
    const brandAliases = (host?.orgAliases ?? undefined) as
      | Record<string, string>
      | undefined;
    return Object.keys(brandClinics).length > 0
      ? buildOrgTree(tenantId, brandClinics, brandAliases)
      : 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 };
  }
}
