import { Prisma } from '@prisma/client';
import { labelCaseSql, AGE_YEARS_SQL } from './reason-temperature.sql';

/**
 * `plan_reasons.potential_labels` 的**计算与刷新** —— 回填、夜间刷新、新写入三条路共用这一份。
 *
 * ═══ 为什么要预计算 ═══════════════════════════════════════════════
 * 初选矩阵此前每次开页面都现推：`plan_reasons` → 展开 `evidence.factIds` →
 * 回查 `patient_facts`（1567 万行 / 18 GB）。最忙的诊所一次摊三万多次随机查、372 MB I/O，
 * **而这一切的唯一产出就是一个标签字符串**。
 * 🔴 本地实测（15,884 条 plan）：958 ms → 110 ms，磁盘读 119,861 → 0，24 格逐字未变。
 *
 * ═══ 🔴 标签规则只有一处 ═══════════════════════════════════════════
 * 这里复用 `labelCaseSql`（矩阵此前用的同一个函数，由 `POTENTIAL_LABEL_RULES` 生成）——
 * ⛔ **绝不在这里另写一份 CASE**：规则表是产品配置，抄第二份必然漂，
 *   而漂了之后矩阵的数会**静默**不一样，没有任何报错（同 `enums` 里那条
 *   「同名不同义是统计事故的标准配方」）。
 *
 * ═══ ⚠️ 年龄被冻结 ═══════════════════════════════════════════════
 * 规则里有三条带年龄（K08>18 / K07 3~12 / K07 13~40）⇒ 标签不是纯函数。
 * 实测受影响 5,551 人中「明天跨档 0 人、30 天内 16 人」（≈0.5 人/天）——
 * 每晚刷一次误差可忽略，⛔ 但**不刷就会逐日漂且不报错**。
 */

/** 一次刷新的批量上限 —— ⚠️ 别一条 UPDATE 扫全表：31 万行会长时间持锁。 */
export const LABEL_REFRESH_BATCH = 5_000;

/**
 * 计算并写回一批 `potential_labels`，**按 id 游标推进**。
 *
 * @param afterId 上一批处理到的最大 id（首批传 null）
 * @param onlyMissing true = 只补 `IS NULL` 的（回填 / 补漏网）；
 *                    false = 连已算过的一起重算（夜间刷新，因为年龄会变）
 * @returns 本批**看过**的行（`seen`）与实际写了几行（`written`）、本批最大 id（`maxId`）
 *
 * 🔴 **必须带游标** —— 只写 `ORDER BY id LIMIT n` 的话：
 *   · onlyMissing 那条路碰巧能走完（写完就不再是 NULL，下一批自然换人）；
 *   · 而**全量刷新会原地打转** —— 每次都取同一批，永远推不到第二批。
 *   ⚠️ 更阴的是它不会报错，只是任务每晚白跑。同类「闸门永远追不上」的坑，
 *     留痕清理那里刚踩过一次（那次是拿 `updatedAt` 当判据）。
 *
 * ⚠️ 全程 `LEFT JOIN` 到 fact —— 推不出标签的依据也要写 `'{}'`，
 *   ⛔ 不能只更新有标签的那些：否则它们永远停在 NULL，被每一轮重复捞出来。
 */
export function refreshLabelsSql(opts: {
  afterId: string | null;
  onlyMissing: boolean;
  batch?: number;
}): Prisma.Sql {
  const limit = opts.batch ?? LABEL_REFRESH_BATCH;
  const gate = opts.onlyMissing ? Prisma.sql`AND pr.potential_labels IS NULL` : Prisma.empty;
  const cursor = opts.afterId
    ? Prisma.sql`AND pr.id > ${opts.afterId}::uuid`
    : Prisma.empty;
  return Prisma.sql`
    WITH target AS (
      SELECT pr.id, fp.patient_id
      FROM plan_reasons pr
      JOIN followup_plans fp ON fp.id = pr.plan_id
      WHERE fp.superseded_at IS NULL
        ${gate}
        ${cursor}
      ORDER BY pr.id
      LIMIT ${limit}
    ),
    computed AS (
      SELECT t.id,
             COALESCE(
               array_agg(DISTINCT lab.lbl) FILTER (WHERE lab.lbl IS NOT NULL),
               '{}'::text[]
             ) AS labels
      FROM target t
      JOIN patients p ON p.id = t.patient_id
      JOIN plan_reasons pr2 ON pr2.id = t.id
      LEFT JOIN LATERAL jsonb_array_elements_text(pr2.evidence->'factIds') fid ON TRUE
      LEFT JOIN patient_facts f
        ON f.id = fid::uuid
       AND f.status = 'active'
       AND COALESCE(f.occurred_at, f.planned_for) IS NOT NULL
      LEFT JOIN LATERAL (SELECT ${labelCaseSql('f', AGE_YEARS_SQL)} AS lbl) lab ON TRUE
      GROUP BY t.id
    ),
    upd AS (
      UPDATE plan_reasons pr
         SET potential_labels = c.labels
        FROM computed c
       WHERE pr.id = c.id
         AND (pr.potential_labels IS DISTINCT FROM c.labels)
      RETURNING pr.id
    )
    -- ⚠️ seen 与 written 必须**分开报**：全量刷新时绝大多数行算出来跟原值一样、
    --   不会被写，此时 written=0 ⛔ 不代表做完了。推进游标看的是 seen / maxId。
    SELECT (SELECT count(*)::int FROM target)      AS seen,
           (SELECT count(*)::int FROM upd)         AS written,
           (SELECT max(id::text) FROM target)     AS "maxId"`;
}

/** 还有多少条没算过 —— 回填收尾与告警用。 */
export const countMissingLabelsSql = Prisma.sql`
  SELECT count(*)::int AS n
  FROM plan_reasons pr
  JOIN followup_plans fp ON fp.id = pr.plan_id
  WHERE fp.superseded_at IS NULL AND pr.potential_labels IS NULL`;
