import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service';
import { QueueProducer } from './queue-producer.service';

/**
 * 凌晨兜底扫描 — 每日 02:00 跑。
 *
 * 目的:抓回主路径漏掉的"画像 stale"患者:
 *   - 死信队列没消化的(persona-recompute 重试耗尽 → 等次日补)
 *   - 应用错误期间漏入队的(部署 / panic 错过 enqueue)
 *   - cold-import 进来但没触发 enqueue 的(脚本路径)
 *
 * 判定:max(patient_transactions.event_seq) > personas.event_watermark (或 persona 不存在)
 *      → 这个 patient 的画像落后,enqueue persona-recompute(BullMQ jobId 自动去重)
 *
 * 安全:扫描走 GROUP BY + HAVING,不锁表。同一 patient 即便已在主路径队列里,
 *      jobId 去重保证不会重复算。
 */
@Injectable()
export class StaleScanService {
  private readonly logger = new Logger(StaleScanService.name);

  constructor(
    private readonly prisma: PrismaService,
    private readonly queue: QueueProducer,
  ) {}

  /**
   * Stale persona 扫描 cron — env 驱动
   *
   * PAC_STALE_SCAN_CRON:
   *   不设         → 不跑(local 默认)
   *   '0 2 * * *'  → 生产 / staging 推荐(凌晨 02:00,DW 增量同步前一会儿)
   *
   * 量级估算:5 家试点 30 万患者,扫一次约 1-5 秒,enqueue 部分(预期 < 1% stale)。
   */
  @Cron(process.env.PAC_STALE_SCAN_CRON || '0 0 31 12 *' /* never — env not set */, {
    name: 'persona-stale-scan',
    // cron 表达式按北京时间解释(容器为 UTC;不加则实际 +8h)
    timeZone: 'Asia/Shanghai',
  })
  async scanAndEnqueueStale(): Promise<void> {
    const startedAt = Date.now();
    this.logger.log('stale-scan: START');

    const stale = await this.findStalePatients();

    let enqueued = 0;
    let failed = 0;
    for (const p of stale) {
      try {
        await this.queue.enqueuePersonaRecompute({
          hostId: p.hostId,
          tenantId: p.tenantId,
          patientId: p.patientId,
          triggeredBy: 'scheduled:stale-scan-2am',
        });
        enqueued++;
      } catch (err) {
        failed++;
        this.logger.warn(
          `stale-scan: enqueue failed patient=${p.patientId}: ${err instanceof Error ? err.message : err}`,
        );
      }
    }

    const durationMs = Date.now() - startedAt;
    this.logger.log(
      `stale-scan: DONE stale=${stale.length} enqueued=${enqueued} failed=${failed} duration=${durationMs}ms`,
    );
  }

  /**
   * 找出"画像 stale"的患者:
   *   - 有 transaction 但没 persona(从来没算过)
   *   - 有 persona 但 watermark < latest event_seq(漏了更新)
   *
   * 用一句 raw SQL 兼顾两种情况;返回 (hostId, tenantId, patientId) 三元组。
   */
  private async findStalePatients(): Promise<
    Array<{ hostId: string; tenantId: string; patientId: string }>
  > {
    // Postgres:LEFT JOIN active persona,WHERE 取 watermark IS NULL 或 max_seq > watermark
    const rows = await this.prisma.$queryRaw<
      Array<{ host_id: string; tenant_id: string; patient_id: string }>
    >`
      SELECT t.host_id, t.tenant_id, t.patient_id
      FROM (
        SELECT host_id, tenant_id, patient_id, MAX(event_seq) AS max_seq
        FROM patient_transactions
        WHERE patient_id IS NOT NULL
        GROUP BY host_id, tenant_id, patient_id
      ) t
      LEFT JOIN personas p
        ON p.patient_id = t.patient_id
       AND p.superseded_at IS NULL
      WHERE p.event_watermark IS NULL OR p.event_watermark < t.max_seq
      LIMIT 5000
    `;
    return rows.map((r) => ({
      hostId: r.host_id,
      tenantId: r.tenant_id,
      patientId: r.patient_id,
    }));
  }

  /**
   * 手动触发(管理后台 / CLI 用)。
   */
  async runNow(): Promise<{ stale: number; enqueued: number; failed: number }> {
    const stale = await this.findStalePatients();
    let enqueued = 0;
    let failed = 0;
    for (const p of stale) {
      try {
        await this.queue.enqueuePersonaRecompute({
          hostId: p.hostId,
          tenantId: p.tenantId,
          patientId: p.patientId,
          triggeredBy: 'manual:stale-scan-runNow',
        });
        enqueued++;
      } catch {
        failed++;
      }
    }
    return { stale: stale.length, enqueued, failed };
  }
}
