/**
 * 初选矩阵基准 —— 「改之前 / 改之后」用**同一把尺**量。
 *
 * ⚠️ 只量 SQL 执行时间（`EXPLAIN ANALYZE` 的 Execution Time），⛔ 不含 HTTP / 序列化：
 *   那两段在改动前后是一样的，混进来只会稀释信号。
 * ⚠️ 每档跑 N 轮取**最快**，⛔ 不取平均 —— 这台机器上跑着别的东西，
 *   平均值被噪声主导（实测同一条件下 1388~2130ms 都出现过）。最快值才稳定可比。
 *
 * 用法：
 *   npx ts-node -T scripts/bench-matrix.ts            # 跑全部诊所档位
 *   npx ts-node -T scripts/bench-matrix.ts --rounds 5
 */
import { PrismaClient } from '@prisma/client';
import { poolBaseSql } from '../src/modules/plan/cohort-filter';
import { planLabelAnchorsSql } from '../src/modules/plan/reason-temperature.sql';

const prisma = new PrismaClient();
const ROUNDS = Number(process.argv[process.argv.indexOf('--rounds') + 1]) || 3;

/** 把 Prisma.Sql 的 `?` 占位换成字面量 —— EXPLAIN 不吃参数化语句。 */
function inline(q: { sql: string; values: unknown[] }): string {
  let i = 0;
  return q.sql.replace(/\?/g, () => {
    const v = q.values[i++];
    return typeof v === 'string' ? `'${v.replace(/'/g, "''")}'` : String(v);
  });
}

/**
 * ⚠️ **sourceUnits 必须按真实值传** —— 它非空时 `poolBaseSql` 会拼 `AND p.source_unit IN (...)`，
 *   走的是另一条 SQL 分支。2026-08-17 这里硬编 `[]` 导致本地永远测不到那一支，
 *   把删掉 `JOIN patients p` 的改动一路放行到线上，矩阵直接 500
 *   （`missing FROM-clause entry for table "p"`）。⛔ 别再退回硬编。
 */
function matrixSql(
  hostId: string,
  tenantId: string,
  clinicId: string,
  sourceUnits: string[],
): string {
  const scope = { hostId, tenantId, clinicIds: [], sourceUnits } as never;
  const inner = inline(planLabelAnchorsSql(poolBaseSql(scope, clinicId)));
  // 外层与 `CohortAttributesService.matrix` 同构（温度 CASE 简化成三档，量的是同一条 join 路径）
  return `EXPLAIN (ANALYZE, BUFFERS)
WITH la AS (${inner}),
b AS (SELECT patient_id, label,
        CASE WHEN NOW() <= hot_until THEN 'hot'
             WHEN NOW() <= warm_until THEN 'warm' ELSE 'cold' END AS temp
      FROM la)
SELECT label, temp, count(DISTINCT patient_id) AS n
FROM b GROUP BY GROUPING SETS ((label, temp), (temp));`;
}

async function run(sql: string) {
  const rows = await prisma.$queryRawUnsafe<Array<Record<string, string>>>(sql);
  const text = rows.map((r) => Object.values(r)[0]).join('\n');
  const ms = Number(/Execution Time: ([\d.]+)/.exec(text)?.[1] ?? NaN);
  const reads = [...text.matchAll(/read=(\d+)/g)].reduce((a, m) => a + Number(m[1]), 0);
  // ⚠️ 并行查询里每个 worker 各报一段 JIT，这里取首段；⛔ 别拿它跟墙钟直接比大小
  const jit = Number(/JIT[\s\S]*?Total (\d+\.?\d*) ms/.exec(text)?.[1] ?? 0);
  return { ms, reads, jit };
}

/**
 * 🔴 **结果快照** —— 性能改动必须同时证明「24 格一个数都没变」。
 *   ⛔ 只比耗时是不够的：把 join 改掉、把标签预计算，最容易的失败是**悄悄少算一批人**，
 *   而那正是产品最不能接受的（「主管一对数就觉得系统在骗他」）。
 */
async function snapshot(sqlWithExplain: string) {
  const plain = sqlWithExplain.replace(/^EXPLAIN \([^)]*\)\n/, '');
  const rows = await prisma.$queryRawUnsafe<
    Array<{ label: string | null; temp: string | null; n: bigint }>
  >(plain);
  return rows
    .map((r) => `${r.label ?? '∑'}/${r.temp ?? '∑'}=${r.n}`)
    .sort()
    .join(' ');
}

(async () => {
  const clinics = await prisma.$queryRaw<
    Array<{ host_id: string; tenant_id: string; target_clinic_id: string; n: bigint }>
  >`SELECT host_id, tenant_id, target_clinic_id, count(*) AS n
    FROM followup_plans WHERE status='active' AND assignee_user_id IS NULL
    GROUP BY 1,2,3 HAVING count(*) >= 10 ORDER BY 4 DESC LIMIT 5`;

  // 真实存在的品牌命名空间 —— 拿来跑「sourceUnits 非空」那一支
  const units = await prisma.$queryRaw<Array<{ source_unit: string }>>`
    SELECT DISTINCT source_unit FROM patients WHERE source_unit IS NOT NULL LIMIT 3`;
  const unitList = units.map((u) => u.source_unit);
  console.log(`  （另跑一遍 sourceUnits=${JSON.stringify(unitList)} 那一支）\n`);

  console.log(`初选矩阵基准 · 每档取 ${ROUNDS} 轮最快值\n`);
  console.log('  plan 数   最快耗时      磁盘读     JIT(首段)');
  console.log('  ' + '─'.repeat(46));
  const out: Array<Record<string, number>> = [];
  const snaps: string[] = [];
  for (const c of clinics) {
    const sql = matrixSql(c.host_id, c.tenant_id, c.target_clinic_id, []);
    // 🔴 同一诊所再跑一遍**带品牌过滤**的那一支 —— 只测一支等于没测
    await run(matrixSql(c.host_id, c.tenant_id, c.target_clinic_id, unitList));
    let best = { ms: Infinity, reads: 0, jit: 0 };
    for (let r = 0; r < ROUNDS; r++) {
      const x = await run(sql);
      if (x.ms < best.ms) best = x;
    }
    const n = Number(c.n);
    console.log(
      `  ${String(n).padStart(7)}   ${best.ms.toFixed(0).padStart(6)} ms   ` +
        `${String(best.reads).padStart(7)}   ${best.jit ? best.jit.toFixed(0) + ' ms' : '—'}`,
    );
    out.push({ plans: n, ms: best.ms, reads: best.reads, jit: best.jit });
    snaps.push(`${n}: ${await snapshot(sql)}`);
  }
  console.log('\nJSON ' + JSON.stringify(out));
  console.log('\n结果快照（改动后必须逐字相同）：');
  for (const s of snaps) console.log('  ' + s);
  await prisma.$disconnect();
})().catch((e) => {
  console.error(e);
  process.exit(1);
});
