/**
 * Sync CLI(统一入口 — PAC v1 唯一推荐 sync 命令)
 *
 * 从上次 sync_logs.cursor_after 增量拉 DW 数据,跑完触发 persona + plan recompute。
 *
 * 链路:
 *   1. ColdImportService.importDirectory({ incremental: true, cohortBatchSize })
 *      - 读 sync_logs[host=jvs-dw, resource=incremental_bundle].cursor_after 拿上次 cursor
 *      - SQL 注入 WHERE updated_date > '${cursor}'(per-query)
 *      - **首跑无 cursor = 等价全量**(自动按 patient 分批,内存恒定)
 *      - 跑完写新 cursor_after = run_start ISO(PR1 — 不漏批次间 DW 变更)
 *   2. 收集本批 affected patient_ids
 *   3. 对每个 affected patient 跑 PersonaService.recompute
 *   4. 跑 PlanEngineService.runHost(SQL 召回 + 6 因子打分,生成/更新 followup_plans)
 *
 * 用法(推荐 `pnpm sync` 别名,等价 sync-incremental):
 *   pnpm sync -- --dir=./data/jvs-dw                          # 首跑(全量)或日常增量
 *   pnpm sync -- --dir=./data/jvs-dw --cohort-batch=5000      # 显式分批(默认 5000,可调)
 *   pnpm sync -- --dir=./data/jvs-dw --full                   # 强制忽略 cursor,全量重新拉(灾难恢复)
 *   pnpm sync -- --dir=./data/jvs-dw --no-recompute           # 只拉数据不联动重算(debug)
 *   pnpm sync -- --dir=./data/jvs-dw --dry-run                # 不写库(测试 cursor 注入)
 *
 * 部署:BullMQ cron 每天 02:00 触发(DW 上游 02:00 完成刷新后)
 *
 * 推荐路径:
 *   - 首次部署:`pnpm sync:prod -- --dir=./data/jvs-dw`(等价全量,分批跑稳)
 *   - 日常增量:cron 自动跑(env PAC_INCREMENTAL_CRON)
 *   - 灾后重灌:TRUNCATE patient_transactions; pnpm sync(cursor 无值 = 全量)
 *
 * cold-import.cli.ts(`pnpm cold-import`)是 legacy 入口,**不读不写 cursor**;
 * 仅用于"想要全量但不动 cursor 状态"的极少场景。**默认请用 `pnpm sync`**。
 */

import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from '../app.module';
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';
import { PrismaService } from '../prisma/prisma.service';

interface CliArgs {
  dir?: string;
  dryRun: boolean;
  noRecompute: boolean;
  full: boolean; // 忽略 cursor,等价 cold-import legacy 行为(但仍写 cursor_after = run_start)
  cohortBatchSize?: number | null;
  help: boolean;
}

function parseArgs(argv: string[]): CliArgs {
  const out: CliArgs = {
    dryRun: false,
    noRecompute: false,
    full: false,
    help: false,
  };
  for (const a of argv) {
    if (a === '--help' || a === '-h') out.help = true;
    else if (a === '--dry-run') out.dryRun = true;
    else if (a === '--no-recompute') out.noRecompute = true;
    else if (a === '--full') out.full = true;
    else if (a === '--no-cohort') out.cohortBatchSize = 0;
    else if (a.startsWith('--cohort-batch=')) {
      const n = parseInt(a.slice('--cohort-batch='.length), 10);
      out.cohortBatchSize = Number.isFinite(n) && n >= 0 ? n : undefined;
    } else if (a.startsWith('--dir=')) out.dir = a.slice('--dir='.length);
  }
  return out;
}

function printHelp() {
  console.log(`
PAC Sync CLI(统一入口,推荐用 \`pnpm sync\`)

DW 直连增量摄入 + 自动 persona/plan 联动重算

Usage:
  pnpm sync -- --dir=<manifest_dir> [options]

Options:
  --dir=<path>           必填,manifest.yaml 所在目录
  --cohort-batch=<N>     按 patient 分批跑(默认 5000;env PAC_COHORT_BATCH_SIZE 兜底)
                         首跑(全量)强烈建议保留默认或调小;日常增量自动跑
  --no-cohort            显式禁用分批,跑 single-shot(文件源 / 调试)
  --full                 忽略上次 cursor,等价"完整重新拉一次"
                         (灾后重灌 / cursor 损坏时用,日常 sync 不要用)
  --dry-run              只读 cursor 注入预览,不写库
  --no-recompute         只拉数据,不触发 persona/plan 重算
  --help, -h             显示本帮助

行为模式:
  首跑(sync_logs 无 cursor):等价全量,自动分 N 批,内存恒定
  后续增量:WHERE updated_date > '\${cursor}',只拉新/改的行

退出码:
  0  success / partial(有进展)
  1  bootstrap 失败(Nest 起不来)
  2  sync 失败(数据/SQL 异常)
  4  并发拦截(同 host 已有 sync 在跑)
`);
}

async function bootstrap() {
  const args = parseArgs(process.argv.slice(2));
  if (args.help || !args.dir) {
    printHelp();
    process.exit(args.help ? 0 : 1);
  }

  const logger = new Logger('sync:cli');
  logger.log(
    `Starting sync(dir=${args.dir}, dryRun=${args.dryRun}, noRecompute=${args.noRecompute}, ` +
      `full=${args.full}, cohortBatch=${args.cohortBatchSize ?? '(default/env)'})`,
  );

  const app = await NestFactory.createApplicationContext(AppModule, {
    logger: ['log', 'warn', 'error'],
  });

  try {
    const importSvc = app.get(ColdImportService);
    const result = await importSvc.importDirectory(args.dir!, {
      dryRun: args.dryRun,
      // --full 时走 legacy cold-import path(不读 cursor);否则增量(读上次 cursor)
      // 注意 PR1 后两个 mode 都写 cursor_after=run_start(为下次增量留 baseline)
      incremental: !args.full,
      cohortBatchSize: args.cohortBatchSize,
    });

    logger.log('─────────────────────────────────────────');
    logger.log(`Result:`);
    logger.log(`  runId:                 ${result.runId}`);
    logger.log(`  host:                  ${result.hostName} (${result.hostId})`);
    logger.log(`  tenants:               [${result.tenantIds.join(',')}]`);
    logger.log(`  status:                ${result.status}`);
    logger.log(`  patients upserted:     ${result.totals.patientsUpserted}`);
    logger.log(`  transactions written:  ${result.totals.transactionsWritten}`);
    logger.log(`  duplicates (idem):     ${result.totals.duplicates}`);
    logger.log(`  failed:                ${result.totals.failed}`);
    logger.log(
      `  facts:                 created=${result.totals.factsCreated} superseded=${result.totals.factsSuperseded} unchanged=${result.totals.factsUnchanged} failed=${result.totals.factsFailed}`,
    );

    if (args.dryRun || args.noRecompute) {
      logger.log('Skipping persona/plan recompute (dry-run 或 --no-recompute)');
      await app.close();
      process.exit(0);
    }

    // ─────────────────────────────────────────
    // 联动 persona 重算 — 仅受影响的 patient
    // ─────────────────────────────────────────
    const prisma = app.get(PrismaService);
    const personaSvc = app.get(PersonaService);
    const planEngine = app.get(PlanEngineService);

    // 找本次 syncLog 之后 written 的 transactions 涉及的 patient
    if (!result.syncLogId) {
      logger.warn('No syncLog id — 跳过 persona/plan(可能 dry-run)');
      await app.close();
      process.exit(0);
    }
    const syncLog = await prisma.syncLog.findUnique({ where: { id: result.syncLogId } });
    if (!syncLog) {
      logger.warn('syncLog 找不到 — 跳过');
      await app.close();
      process.exit(0);
    }
    const affectedPatients = await prisma.patientTransaction.findMany({
      where: {
        hostId: result.hostId,
        createdAt: { gte: syncLog.startedAt },
      },
      select: { patientId: true },
      distinct: ['patientId'],
    });
    const patientIds = affectedPatients.map((r) => r.patientId).filter((id): id is string => !!id);
    logger.log('─────────────────────────────────────────');
    logger.log(`Persona recompute:${patientIds.length} patients affected`);
    let pSuccess = 0;
    let pNoop = 0;
    let pFailed = 0;
    for (const patientId of patientIds) {
      try {
        const r = await personaSvc.recompute({
          patientId,
          source: `incremental-sync:${result.runId}`,
        });
        if (r.status === 'success' || r.status === 'partial') pSuccess++;
        else if (r.status === 'noop') pNoop++;
        else pFailed++;
      } catch (e) {
        pFailed++;
        logger.warn(`persona patient=${patientId} failed: ${(e as Error).message}`);
      }
    }
    logger.log(`  success=${pSuccess} noop=${pNoop} failed=${pFailed}`);

    // ─────────────────────────────────────────
    // 联动 plan 重算 — 整 host(scenario SQL 跑一次刷新所有 patient 池)
    // ─────────────────────────────────────────
    logger.log('─────────────────────────────────────────');
    logger.log(`Plan recompute:host=${result.hostName}`);
    const tenants = await prisma.patient.findMany({
      where: { hostId: result.hostId },
      distinct: ['tenantId'],
      select: { tenantId: true },
    });
    let totalPlansCreated = 0;
    let totalHits = 0;
    for (const t of tenants) {
      const r = await planEngine.runAllForHost({
        hostId: result.hostId,
        tenantId: t.tenantId,
        now: new Date(),
      });
      totalPlansCreated += r.plansCreated;
      totalHits += r.patientsHit;
      logger.log(`  tenant=${t.tenantId}: patientsHit=${r.patientsHit} plansCreated=${r.plansCreated} skipped=${r.plansSkippedAssigned}`);
    }
    logger.log(`  Total: patientsHit=${totalHits} plansCreated=${totalPlansCreated}`);
    logger.log('─────────────────────────────────────────');
  } catch (e) {
    logger.error(`Incremental sync failed: ${(e as Error).message}`);
    if ((e as Error).stack) logger.error((e as Error).stack);
    await app.close();
    process.exit(1);
  }

  await app.close();
  process.exit(0);
}

bootstrap();
