/**
 * Recompute Persona CLI
 *
 * 批量重算指定 host(默认 demo)所有 patient 的 Persona,产新版本。
 *
 * Usage:
 *   pnpm recompute-persona                   # 默认 host=demo,所有 active patient
 *   pnpm recompute-persona -- --host=friday  # 指定 host
 *   pnpm recompute-persona -- --pid=P001     # 单 patient(host 内 externalId)
 *   pnpm recompute-persona -- --host=jvs-dw --clinics=<orgId> --concurrency=16  # 按诊所补摄后定向重算
 */
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from '../app.module';
import { PersonaService } from '../modules/persona/persona.service';
import { PrismaService } from '../prisma/prisma.service';

interface Args {
  host: string;
  pid?: string;
  pids?: string[]; // 多个 externalId(逗号分隔),定向重算受影响子集用
  force?: boolean; // 跳过水位幂等闸:算法/特征变更后(数据没变)也强制重算
  concurrency: number; // 并发度:每患者 recompute 含 ~15 次 DB 往返,并发可重叠 I/O 等待
  shard?: { i: number; k: number }; // 分片 i/k:多进程并行(吃多核),按患者序号取模
  clinics?: string[]; // --clinics=X,Y:只重算"在这些诊所有过操作"的患者(按诊所补摄后定向重算,免全 host 白扫)
}

function parseArgs(argv: string[]): Args {
  const args: Args = { host: 'demo', concurrency: 1 };
  for (const a of argv) {
    if (a.startsWith('--host=')) args.host = a.slice('--host='.length);
    else if (a.startsWith('--pid=')) args.pid = a.slice('--pid='.length);
    else if (a === '--force') args.force = true;
    else if (a.startsWith('--pids=')) {
      args.pids = a.slice('--pids='.length).split(',').map((s) => s.trim()).filter(Boolean);
    } else if (a.startsWith('--concurrency=')) {
      const n = parseInt(a.slice('--concurrency='.length), 10);
      if (Number.isFinite(n) && n >= 1) args.concurrency = n;
    } else if (a.startsWith('--shard=')) {
      const m = a.slice('--shard='.length).match(/^(\d+)\/(\d+)$/);
      if (m) args.shard = { i: Number(m[1]), k: Number(m[2]) };
    } else if (a.startsWith('--clinics=')) {
      args.clinics = a.slice('--clinics='.length).split(',').map((s) => s.trim()).filter(Boolean);
    }
  }
  return args;
}

async function bootstrap() {
  const args = parseArgs(process.argv.slice(2));
  const logger = new Logger('recompute-persona');
  const app = await NestFactory.createApplicationContext(AppModule, {
    logger: ['log', 'warn', 'error'],
  });

  try {
    const prisma = app.get(PrismaService);
    const svc = app.get(PersonaService);

    const host = await prisma.host.findUnique({ where: { name: args.host } });
    if (!host) throw new Error(`Host '${args.host}' not found`);

    // --clinics:把重算范围收窄到"在这些诊所有过操作(patient_transactions.clinic_id)"的患者。
    // 和 cold-import --clinics 同口径,用于按诊所补摄后只重算新灌入的那批,避免全 host 白扫。
    let clinicPatientIds: string[] | undefined;
    if (args.clinics?.length) {
      const rows = await prisma.patientTransaction.findMany({
        where: { hostId: host.id, clinicId: { in: args.clinics } },
        select: { patientId: true },
        distinct: ['patientId'],
      });
      const ids = rows.map((r) => r.patientId).filter((x): x is string => !!x);
      logger.log(`--clinics=${args.clinics.join(',')} → 命中 ${ids.length} 位患者(按诊所收窄)`);
      if (ids.length === 0) throw new Error('--clinics 未命中任何患者(诊所 id 是否正确 / 是否已摄入?)');
      clinicPatientIds = ids;
    }

    const extraWhere = args.pids?.length
      ? { externalId: { in: args.pids } }
      : args.pid
        ? { externalId: args.pid }
        : {};
    let patients: { id: string; externalId: string; name: string | null }[];
    if (clinicPatientIds) {
      // Postgres 单条 prepared statement 绑定参数上限 32767 —— 大诊所(如建国门 ~5 万患者)
      // 一次 id IN (...) 会超限报错,分块查再合并。块大小留足余量给其他 where 条件。
      const CHUNK = 20000;
      const acc: { id: string; externalId: string; name: string | null }[] = [];
      for (let i = 0; i < clinicPatientIds.length; i += CHUNK) {
        const slice = clinicPatientIds.slice(i, i + CHUNK);
        const part = await prisma.patient.findMany({
          where: { hostId: host.id, active: true, id: { in: slice }, ...extraWhere },
          select: { id: true, externalId: true, name: true },
        });
        acc.push(...part);
      }
      acc.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)); // 稳定序供分片
      patients = acc;
    } else {
      patients = await prisma.patient.findMany({
        where: { hostId: host.id, active: true, ...extraWhere },
        select: { id: true, externalId: true, name: true },
        orderBy: { id: 'asc' }, // 稳定序,供分片按下标取模(各进程不重不漏)
      });
    }
    if (args.shard) {
      const { i, k } = args.shard;
      patients = patients.filter((_, idx) => idx % k === i);
    }

    const total = patients.length;
    logger.log(
      `Recomputing ${total} patients for host=${args.host} ` +
        `(concurrency=${args.concurrency}${args.shard ? `, shard=${args.shard.i}/${args.shard.k}` : ''}) ...`,
    );

    // 五档结果(见 PersonaService.recompute):noop=闸拦下没算 / unchanged=算了内容没变 /
    // refreshed=只有天数等易变值变(就地刷新不升版本) / ok+partial=升了版本。都不是错误。
    // 看 unchanged 占比就知道闸放宽后有多少是白算的 —— 调水位口径的依据。
    let ok = 0, partial = 0, noop = 0, unchanged = 0, refreshed = 0, failed = 0, done = 0;
    const verbose = total <= 500; // 小批逐条日志;大批只打进度,免刷屏
    const runOne = async (p: { id: string; externalId: string; name: string | null }) => {
      try {
        const r = await svc.recompute({ patientId: p.id, source: `manual:cli`, force: args.force });
        if (verbose) {
          logger.log(
            `  ${p.externalId.padEnd(6)} ${(p.name ?? '').padEnd(12)} v${r.version} features=${r.featureCount} status=${r.status} (${r.durationMs}ms)`,
          );
        }
        if (r.status === 'success') ok++;
        else if (r.status === 'partial') partial++;
        else if (r.status === 'noop') noop++;
        else if (r.status === 'unchanged') unchanged++;
        else if (r.status === 'refreshed') refreshed++;
        else failed++;
      } catch (err) {
        failed++;
        logger.error(`  ${p.externalId} FAILED: ${err instanceof Error ? err.message : err}`);
      }
      if (!verbose && ++done % 2000 === 0) logger.log(`  …进度 ${done}/${total} (failed=${failed})`);
    };

    // 并发分块:每患者 recompute 含 ~15 次串行 DB 往返(I/O 等待为主),
    // 单进程内并发可重叠这些等待;CPU 占比高的部分再靠多进程 --shard 吃多核。
    for (let i = 0; i < patients.length; i += args.concurrency) {
      await Promise.all(patients.slice(i, i + args.concurrency).map(runOne));
    }

    logger.log('─────────────────────────────────');
    logger.log(
      `Done: success=${ok} partial=${partial} refreshed=${refreshed} ` +
        `unchanged=${unchanged} noop=${noop} failed=${failed}`,
    );
  } catch (err) {
    new Logger('recompute-persona').error(err instanceof Error ? err.stack : String(err));
    process.exitCode = 2;
  } finally {
    await app.close();
  }
}

bootstrap().catch((err) => {
  console.error(err);
  process.exit(3);
});
