import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';

/**
 * 召回池优先级分布 —— 让「9.78 分」这个数字有参照系。
 *
 * ── 为什么需要 ──
 * 详情页 hover 把三维拆解讲得很细,但客服看完仍然不知道**这分算高还是低** ——
 * 光看 9.78/10 会以为满分 10 就是天花板、9.78 只是"还行"。生产实测分布完全不是这样:
 *   p50=31 · p80=56 · p90=66 · p95=76 · p99=82(active+assigned 共 23.6 万)
 * 也就是说 97.8 分是**前 0.05%**,是整池最该先打的那几个。不给参照,这条信息就丢了。
 *
 * ── 实现 ──
 * 整数分 0-100 的直方图(101 个桶),按租户缓存 30 分钟;算某个分数的百分位纯内存,
 * 每行零 DB 开销。分布是慢变量(单个患者进出撼动不了 23 万人的分位),30 分钟足够。
 *
 * ⚠️ 口径是**租户级整池**,不跟随调用者的诊所/品牌可见范围。诊所主管看到的
 *   「高于池内 98%」指的是全租户召回池,不是他自己那几家诊所 —— 文案上说「召回池」即此意。
 *   要做到按范围分位得给每种 scope 组合各缓存一份,收益不抵复杂度。
 */
@Injectable()
export class PriorityDistributionService {
  private readonly logger = new Logger(PriorityDistributionService.name);

  /** key=host:tenant → { at, hist:101 桶计数, total } */
  private readonly cache = new Map<string, { at: number; hist: Int32Array; total: number }>();
  /** single-flight:缓存空/过期时并发调用共享同一个在飞查询(列表每页一次,别打成惊群) */
  private readonly inflight = new Map<string, Promise<{ hist: Int32Array; total: number }>>();
  private static readonly TTL_MS = 30 * 60 * 1000;

  constructor(private readonly prisma: PrismaService) {}

  /**
   * 该分数在租户召回池里的百分位(0-100)= 严格低于它的占比。
   * 池子太小(<50)时返回 null —— 样本不足,给出的百分位会误导。
   */
  async percentileOf(hostId: string, tenantId: string, score: number): Promise<number | null> {
    const { hist, total } = await this.load(hostId, tenantId);
    if (total < 50) return null;
    const s = Math.max(0, Math.min(100, Math.round(score)));
    let below = 0;
    for (let i = 0; i < s; i++) below += hist[i]!;
    return Math.round((below / total) * 100);
  }

  private async load(hostId: string, tenantId: string) {
    const key = `${hostId}:${tenantId}`;
    const hit = this.cache.get(key);
    if (hit && Date.now() - hit.at < PriorityDistributionService.TTL_MS) return hit;
    const flying = this.inflight.get(key);
    if (flying) return flying;

    const promise = (async () => {
      const hist = new Int32Array(101);
      let total = 0;
      try {
        const rows = await this.prisma.$queryRaw<Array<{ s: number; n: bigint }>>`
          SELECT priority_score AS s, COUNT(*) AS n
          FROM followup_plans
          WHERE host_id = ${hostId}::uuid AND tenant_id = ${tenantId}
            AND status IN ('active', 'assigned')
          GROUP BY 1`;
        for (const r of rows) {
          const i = Math.max(0, Math.min(100, Number(r.s)));
          const n = Number(r.n);
          hist[i]! += n;
          total += n;
        }
      } catch (err) {
        // 分位只是锦上添花,查失败不能拖垮列表 —— 返回空分布,调用方拿到 null 不渲染
        this.logger.warn(`优先级分布查询失败 (${key}): ${err instanceof Error ? err.message : err}`);
      } finally {
        this.inflight.delete(key);
      }
      this.cache.set(key, { at: Date.now(), hist, total });
      return { hist, total };
    })();
    this.inflight.set(key, promise);
    return promise;
  }
}
