/**
 * Reparse CLI — 用已存的 transaction.rawPayload 离线重跑装配+解析(不连 DW)。
 *
 * 改了 assembler yaml(enum/keyword/field_mapping / transforms)后,把存量 fact 按新口径
 * 重新衍生(版本流 supersede),**不 truncate、不碰账本、不拉 DW**,执行历史/分配全保住。
 *
 * Usage:
 *   pnpm reparse -- --host=jvs-dw --subject-type=diagnosis            # 实跑 + 定向重算
 *   pnpm reparse -- --host=jvs-dw --subject-type=diagnosis --dry-run  # 只报会改多少,不写
 *   pnpm reparse -- --host=jvs-dw --patient=<uuid>,<uuid>             # 限定患者(少量,命令行)
 *   pnpm reparse -- --host=jvs-dw --patients-file=./ids.txt           # 限定患者(大批,见下)
 *   pnpm reparse -- --host=jvs-dw --subject-type=diagnosis --no-recompute  # 只重衍 fact,稍后单独 recompute
 *
 * ⭐ --patients-file:**按受影响患者收窄**,reparse 最有效的提速手段。
 *   全量 reparse 的时间几乎全花在"证明没变"上 —— 2026-07-27 测试服实测:
 *   260,434 患者 / 87 批 / ~4.5 小时,而其中真正会变的:
 *     · 改 clinical-signals 字典  → 约 4 万患者(15%)   → 6.5 倍
 *     · 改 planned「二期」归类规则 → 1,017 患者(0.4%)  → 250 倍(2 分钟跑完)
 *   用法:先用 SQL 圈出受影响患者,dump 成一行一个 id 的文件,再喂给本参数。例:
 *     psql -tAc "SELECT DISTINCT patient_id FROM patient_facts
 *                WHERE type='treatment_record' AND kind='planned'
 *                  AND content->>'subtype' LIKE '%二期%'" > /tmp/ids.txt
 *     pnpm reparse -- --host=jvs-dw --subject-type=treatment --patients-file=/tmp/ids.txt
 *   文件支持 patient.id(uuid)与 externalId 混写,# 开头为注释,空行忽略。
 *   ⚠️ 解析结果为空时**直接报错退出**,绝不退化成全量 —— 服务侧 `patientIds` 传空数组
 *      等同"不限定"(cold-import.service.ts `if (!scopePatientIds?.length)`),
 *      一个写错路径的收窄命令若静默变成 4.5 小时全量,是最难发现的那种事故。
 *
 * 通过 NestApplicationContext 启动(不起 HTTP),复用 DI。
 */
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { existsSync, readFileSync } from 'node:fs';
// 纯函数拆在独立模块 —— 本文件末尾 `void main()`,被 import 就会真跑 reparse,测试不能碰
import { parsePatientsFile, splitPatientTokens } from './reparse-patients-file';
import { AppModule } from '../app.module';
import { PrismaService } from '../prisma/prisma.service';
import { ColdImportService } from '../modules/sync/cold-import/cold-import.service';
import { PersonaService } from '../modules/persona/persona.service';
import { PlanEngineService } from '../modules/plan/engine/plan-engine.service';

interface CliArgs {
  dir: string;
  host: string;
  subjectTypes?: string[];
  patientIds?: string[];
  patientsFile?: string;
  dryRun: boolean;
  recompute: boolean;
  help: boolean;
}

function parseArgs(argv: string[]): CliArgs {
  const args: CliArgs = { dir: './data/jvs-dw', host: 'jvs-dw', dryRun: false, recompute: true, help: false };
  for (const a of argv) {
    if (a === '--help' || a === '-h') args.help = true;
    else if (a === '--dry-run') args.dryRun = true;
    else if (a === '--no-recompute') args.recompute = false;
    else if (a.startsWith('--dir=')) args.dir = a.slice('--dir='.length);
    else if (a.startsWith('--host=')) args.host = a.slice('--host='.length);
    else if (a.startsWith('--subject-type=')) args.subjectTypes = a.slice('--subject-type='.length).split(',').map((s) => s.trim()).filter(Boolean);
    else if (a.startsWith('--patient=')) args.patientIds = a.slice('--patient='.length).split(',').map((s) => s.trim()).filter(Boolean);
    else if (a.startsWith('--patients-file=')) args.patientsFile = a.slice('--patients-file='.length).trim();
  }
  return args;
}

async function main(): Promise<void> {
  const logger = new Logger('reparse-cli');
  const args = parseArgs(process.argv.slice(2));
  if (args.help) {
    // eslint-disable-next-line no-console
    console.log(
      'pnpm reparse -- --host=<name> [--dir=./data/<host>] [--subject-type=diagnosis,treatment]\n' +
        '               [--patient=<uuid>,...] [--patients-file=<path>] [--dry-run] [--no-recompute]\n' +
        '\n' +
        '  --patients-file  一行一个 patient.id(uuid)或 externalId,# 注释、空行忽略。\n' +
        '                   按受影响患者收窄是 reparse 最有效的提速手段(实测可达 250 倍)。\n' +
        '                   解析为空会直接报错,不会退化成全量。',
    );
    return;
  }

  const app = await NestFactory.createApplicationContext(AppModule, { logger: ['log', 'warn', 'error'] });
  try {
    const prisma = app.get(PrismaService);
    const coldImport = app.get(ColdImportService);
    const persona = app.get(PersonaService);
    const planEngine = app.get(PlanEngineService);

    // ── --patients-file:解析 → 认领 externalId → 与 --patient 合并 ──
    let patientIds = args.patientIds;
    if (args.patientsFile) {
      if (!existsSync(args.patientsFile)) {
        throw new Error(`--patients-file 不存在:${args.patientsFile}`);
      }
      const tokens = parsePatientsFile(readFileSync(args.patientsFile, 'utf8'));
      if (tokens.length === 0) {
        // ⚠️ 不能放过 —— 空 patientIds 到服务侧等同"不限定" = 全量 reparse(4.5 小时)
        throw new Error(`--patients-file 解析后为 0 个患者:${args.patientsFile}(拒绝退化成全量)`);
      }
      const { ids, externalIds } = splitPatientTokens(tokens);

      // externalId 只在 host 内唯一 —— 必须带 hostId 查,否则跨宿主撞号
      let resolved: string[] = [];
      if (externalIds.length > 0) {
        const host = await prisma.host.findUnique({ where: { name: args.host }, select: { id: true } });
        if (!host) throw new Error(`host '${args.host}' 不存在`);
        const rows = await prisma.patient.findMany({
          where: { hostId: host.id, externalId: { in: externalIds } },
          select: { id: true, externalId: true },
        });
        resolved = rows.map((r) => r.id);
        const found = new Set(rows.map((r) => r.externalId));
        const missing = externalIds.filter((e) => !found.has(e));
        if (missing.length > 0) {
          // 报出来但不中断:圈人清单里混进几个别的 host / 已删患者是常态,
          // 静默丢会让"为什么少跑了几百人"变成无头案。
          logger.warn(
            `--patients-file 有 ${missing.length} 个 externalId 在 host=${args.host} 查不到,已跳过` +
              `(前 5 个:${missing.slice(0, 5).join(', ')})`,
          );
        }
      }

      const merged = [...new Set([...(args.patientIds ?? []), ...ids, ...resolved])];
      if (merged.length === 0) {
        throw new Error(`--patients-file 里没有一个能对上库中的患者:${args.patientsFile}(拒绝退化成全量)`);
      }
      patientIds = merged;
      logger.log(
        `--patients-file ${args.patientsFile}:读到 ${tokens.length} 个 token ` +
          `(uuid ${ids.length} / externalId ${externalIds.length} → 认领 ${resolved.length});` +
          `最终限定 ${merged.length} 个患者`,
      );
    }

    logger.log(
      `reparse start: host=${args.host} dir=${args.dir} ` +
        `subjectTypes=${args.subjectTypes?.join(',') ?? 'ALL'} ` +
        `patients=${patientIds?.length ?? 'all'} dryRun=${args.dryRun} recompute=${args.recompute}`,
    );

    const result = await coldImport.reparseFromTransactions({
      dir: args.dir,
      hostName: args.host,
      subjectTypes: args.subjectTypes,
      patientIds,
      dryRun: args.dryRun,
    });

    if (args.dryRun) {
      logger.log(
        `[DRY-RUN] 上方 reparse[dry] 日志列出了各资源的源行数 → 会装配的 canonical 数。` +
          `不写库。去掉 --dry-run 实跑(非破坏:版本流 supersede 变更的、跳过不变的,会精确报 superseded/unchanged)。`,
      );
      await app.close();
      return;
    }

    const totalSuperseded = result.perResource.reduce((s, r) => s + r.factsSuperseded, 0);
    const totalCreated = result.perResource.reduce((s, r) => s + r.factsCreated, 0);
    logger.log(
      `reparse fact 重衍完成:superseded=${totalSuperseded} created=${totalCreated} ` +
        `affectedPatients=${result.affectedPatientIds.length}`,
    );

    if (!args.recompute) {
      logger.log(`--no-recompute:跳过 persona/plan 重算。记得稍后跑 recompute-persona / recompute-plans。`);
      await app.close();
      return;
    }

    // 定向重算受影响患者(persona force=true:reparse 不动 transaction 水位,需跳幂等闸)
    const ids = result.affectedPatientIds;
    logger.log(`定向重算 ${ids.length} 个患者的 persona + plan …`);
    let done = 0;
    let personaErr = 0;
    let planErr = 0;
    for (const patientId of ids) {
      const patient = await prisma.patient.findUnique({
        where: { id: patientId },
        select: { hostId: true, tenantId: true },
      });
      if (!patient) continue;
      try {
        await persona.recompute({ patientId, source: 'reparse', force: true });
      } catch (e) {
        personaErr++;
        logger.warn(`persona recompute 失败 patient=${patientId}: ${(e as Error).message}`);
      }
      try {
        await planEngine.recomputeForPatient({ hostId: patient.hostId, tenantId: patient.tenantId, patientId });
      } catch (e) {
        planErr++;
        logger.warn(`plan recompute 失败 patient=${patientId}: ${(e as Error).message}`);
      }
      if (++done % 200 === 0) logger.log(`  recompute 进度 ${done}/${ids.length}`);
    }
    logger.log(`reparse DONE:重算 ${done} 患者(persona 失败 ${personaErr} / plan 失败 ${planErr})`);
    await app.close();
  } catch (err) {
    logger.error(`reparse 失败:${err instanceof Error ? err.message : String(err)}`);
    await app.close();
    process.exitCode = 1;
  }
}

// ⚠️ 只在被直接执行时跑 —— 没这道闸,任何 import 本文件的代码(测试/工具脚本)
//   都会当场启动 Nest 并对连着的库跑一次真实 reparse。2026-07-27 实际踩过。
if (require.main === module) void main();
