import { Injectable, Logger } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bullmq';
import { randomUUID } from 'node:crypto';
import type { Queue } from 'bullmq';
import { QueueName, QueueDefaults } from './queue-names';
import type {
  PersonaRecomputeJob,
  PlanRecomputeJob,
  PlanAssetGenerateJob,
  ExecutionCallbackJob,
  ColdImportJob,
} from './job-payloads';

/**
 * QueueProducer — 给业务模块(sync / persona / plan)统一的入队接口。
 *
 * 设计点:
 *   - `jobName` 是 BullMQ 的去重 / 串行键 ⭐
 *     · persona / plan recompute  → jobName = patientId(同 patient 串行)
 *     · plan-asset-generate       → jobName = planId(同 plan 串行)
 *   - 同 jobName 在队列里只保留 1 个 waiting 的 job(用 deduplication)
 *     防止 1 秒内来 10 个 txn 队列里堆 10 个重复 job
 *   - 失败重试 / 退避 由 QueueDefaults 统一管理,模块不自定义
 */
@Injectable()
export class QueueProducer {
  private readonly logger = new Logger(QueueProducer.name);

  constructor(
    @InjectQueue(QueueName.PERSONA_RECOMPUTE) private readonly personaQ: Queue,
    @InjectQueue(QueueName.PLAN_RECOMPUTE) private readonly planQ: Queue,
    @InjectQueue(QueueName.PLAN_ASSET_GENERATE) private readonly assetQ: Queue,
    @InjectQueue(QueueName.EXECUTION_CALLBACK) private readonly callbackQ: Queue,
    @InjectQueue(QueueName.COLD_IMPORT) private readonly coldImportQ: Queue,
  ) {}

  async enqueuePersonaRecompute(
    job: PersonaRecomputeJob,
    opts?: {
      /**
       * 去抖模式(push 摄入路径用)—— 磨平存量/增量差异的关键:
       * 存量按批重放时同一患者被几十个批次连续 touch,立即入队会重算几十遍(每遍读全量历史,
       * Node-CPU-bound)。去抖 = delay 一个时间窗 + 窗内按 (patientId, 时间桶) 去重:
       * 窗内多批合并成一次重算(job 到期跑时读的是当时 DB 全量,晚落的批自动包含);
       * 增量只多付一个窗的延迟(召回是日级节奏,无感)。
       * 非 push 路径(单患者刷新等要即时反馈)不传,行为不变。
       */
      debounce?: boolean;
    },
  ): Promise<void> {
    if (opts?.debounce) {
      const windowMs = resolvePersonaDebounceMs();
      // 时间桶 jobId:同患者同桶内的重复 add 被 BullMQ jobId 去重吃掉,只留第一个(delayed);
      // 跨桶产生新 job —— 最坏每患者每窗 1 次重算,50 批连续推 → 2~3 次而非 50 次。
      const bucket = Math.floor(Date.now() / windowMs);
      await this.personaQ.add(job.patientId, job, {
        jobId: `persona_${job.patientId}_${bucket}`,
        delay: windowMs,
        ...QueueDefaults[QueueName.PERSONA_RECOMPUTE],
      });
      this.logger.debug(
        `enqueue persona-recompute(debounced ${windowMs}ms) patient=${job.patientId} triggeredBy=${job.triggeredBy}`,
      );
      return;
    }
    // jobId 用 _ 分隔(BullMQ 不允许 :);同 patient 的 waiting job 自动去重
    await this.personaQ.add(job.patientId, job, {
      jobId: `persona_${job.patientId}`,
      ...QueueDefaults[QueueName.PERSONA_RECOMPUTE],
    });
    this.logger.debug(
      `enqueue persona-recompute patient=${job.patientId} triggeredBy=${job.triggeredBy}`,
    );
  }

  async enqueuePlanRecompute(job: PlanRecomputeJob): Promise<void> {
    await this.planQ.add(job.patientId, job, {
      jobId: `plan_${job.patientId}`,
      ...QueueDefaults[QueueName.PLAN_RECOMPUTE],
    });
    this.logger.debug(
      `enqueue plan-recompute patient=${job.patientId} triggeredBy=${job.triggeredBy}`,
    );
  }

  async enqueuePlanAssetGenerate(job: PlanAssetGenerateJob): Promise<void> {
    await this.assetQ.add(job.planId, job, {
      jobId: `asset_${job.planId}`,
      ...QueueDefaults[QueueName.PLAN_ASSET_GENERATE],
    });
    this.logger.debug(
      `enqueue plan-asset-generate plan=${job.planId.slice(0, 8)} triggeredBy=${job.triggeredBy}`,
    );
  }

  /**
   * 整包 cold-import 入队。返回 jobId 作前端轮询句柄(不落 syncLog —— importDirectory 自己会写
   * 自己的 syncLog 供审计/最近事件;前端进度直接查 BullMQ job 的 state + returnvalue)。
   * concurrency=1 串行 + importDirectory 自带 running 锁 → 同 host 不会并发跑。
   */
  async enqueueColdImport(job: ColdImportJob): Promise<{ jobId: string }> {
    const jobId = randomUUID();
    await this.coldImportQ.add(job.hostName, job, {
      jobId,
      ...QueueDefaults[QueueName.COLD_IMPORT],
    });
    this.logger.log(
      `enqueue cold-import job=${jobId} host=${job.hostName} triggeredBy=${job.triggeredBy}`,
    );
    return { jobId };
  }

  /** 查一次 cold-import job 进度(前端轮询用)。job 不存在返回 null(可能已被清理)。 */
  async getColdImportJob(jobId: string): Promise<{
    state: string;
    hostId: string | null;
    returnValue: unknown;
    failedReason: string | null;
  } | null> {
    const job = await this.coldImportQ.getJob(jobId);
    if (!job) return null;
    const state = await job.getState();
    return {
      state,
      hostId: (job.data as { hostId?: string } | undefined)?.hostId ?? null,
      returnValue: job.returnvalue ?? null,
      failedReason: job.failedReason ?? null,
    };
  }

  async enqueueExecutionCallback(job: ExecutionCallbackJob): Promise<void> {
    // jobId = executionId(append-only 唯一)→ 同一 execution 不会重复入队
    await this.callbackQ.add(job.executionId, job, {
      jobId: `callback_${job.executionId}`,
      ...QueueDefaults[QueueName.EXECUTION_CALLBACK],
    });
    this.logger.debug(
      `enqueue execution-callback execution=${job.executionId.slice(0, 8)} triggeredBy=${job.triggeredBy}`,
    );
  }
}

/// persona 去抖窗口(push 摄入路径):PAC_PUSH_PERSONA_DEBOUNCE_MS,默认 5 分钟。
/// 太短(<1min)合并不了存量批次;太长增量画像延迟大。下限 10s 防误配成 0 把去抖变空转。
function resolvePersonaDebounceMs(): number {
  const raw = Number(process.env.PAC_PUSH_PERSONA_DEBOUNCE_MS);
  if (Number.isFinite(raw) && raw >= 10_000) return raw;
  return 5 * 60_000;
}
