import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import type { ClinicalSignal } from '@pac/types';
import type { FactDraft } from '../sync/pipeline/parsers/parser.interface';
import {
  buildScanIndex,
  loadSignalDict,
  type SignalDict,
} from './signal-dict';
import { extractSignalsFromContent } from './signal-extractor';

/**
 * ClinicalSignalService —— 把抽取挂进 fact 衍生管线的唯一接口。
 *
 * 【为什么挂在 ParserPipeline 而不是各个 parser 里】
 *   源字段要跨 fact 类型且可配置(既往史在 emr_record,严重度在 diagnosis_record.name_zh) ——
 *   挂在 parser 里意味着每加一个源字段就要改一个 parser;挂在 pipeline 上只改 yaml 一行。
 *   parser 的职责是"把宿主行翻译成 PAC 标准 fact",信号抽取是对**已标准化内容**的再解读,
 *   本来就该在 parser 之后。
 *
 * 【为什么写回源 fact 的 content 而不新建 fact 类型】
 *   FactType 的设计纪律:「fact 只装源事件,不做衍生 fact 派生 — 衍生信息留在源 fact 的
 *   content JSONB 里」。信号跟着源病历的生命周期走(病历订正 → 重放 → 信号自动重抽;
 *   病历作废 → status 变更,信号随之失效),不需要额外维护一致性。
 *
 * 【为什么没命中就不写这个 key】
 *   content hash 变了就会 supersede 出一个新 fact 版本。生产 256 万条 emr_record 里
 *   绝大多数是「无特殊陈述」这类无信号文本 —— 给它们都塞一个 `clinical_signals: []`
 *   会凭空掀起 256 万条版本波。只给真有命中的写。
 */
@Injectable()
export class ClinicalSignalService implements OnModuleInit {
  private readonly logger = new Logger(ClinicalSignalService.name);
  private dict!: SignalDict;
  private scanIndex!: Map<string, string[]>;

  onModuleInit(): void {
    // 启动即加载并强校验 —— 字典写错宁可起不来,也不要静默把禁忌判反。
    this.dict = loadSignalDict();
    this.scanIndex = buildScanIndex(this.dict);
    this.logger.log(
      `clinical-signals 字典已加载:${this.dict.version} ` +
        `(${this.dict.signals.length} 信号 / ${this.dict.scan_fields.length} 类源 fact)`,
    );
  }

  get version(): string {
    return this.dict.version;
  }

  /** 该 fact 类型是否在扫描范围内 */
  scannedFields(factType: string): string[] | undefined {
    return this.scanIndex.get(factType);
  }

  /**
   * 抽取一条 draft 的信号。命中则返回带 `clinical_signals` 的新 draft,否则原样返回。
   *
   * 不抛错:抽取失败绝不能拖垮 fact 落库 —— 病历本身是主数据,信号只是旁注。
   * 失败只 warn,下次重放或全量重扫会自愈。
   */
  enrich(draft: FactDraft, occurredAt: Date | null): FactDraft {
    const fields = this.scanIndex.get(draft.type);
    if (!fields) return draft;
    // FactDraft.content 是 Prisma.InputJsonValue(联合类型);parser 产出的实际都是对象,
    // 非对象形态直接放行(没字段可扫)。
    const content = draft.content;
    if (typeof content !== 'object' || content === null || Array.isArray(content)) return draft;
    try {
      const signals = extractSignalsFromContent(
        content as Record<string, unknown>,
        fields,
        occurredAt ?? draft.occurredAt ?? null,
        this.dict,
      );
      if (signals.length === 0) return draft;
      return { ...draft, content: { ...content, clinical_signals: signals } };
    } catch (err) {
      this.logger.warn(
        `clinical-signals 抽取失败 subjectId=${draft.subjectId}: ${(err as Error).message}`,
      );
      return draft;
    }
  }

  /** 批量版 —— 供 ParserPipeline 的 bulk 路径与全量重扫 CLI 共用 */
  enrichAll(drafts: FactDraft[], occurredAt: Date | null): FactDraft[] {
    return drafts.map((d) => this.enrich(d, occurredAt));
  }

  /** 直接对一份 content 抽取(全量重扫 CLI 用,不经过 parser) */
  extractForContent(
    factType: string,
    content: Record<string, unknown>,
    occurredAt: Date | null,
  ): ClinicalSignal[] | null {
    const fields = this.scanIndex.get(factType);
    if (!fields) return null;
    return extractSignalsFromContent(content, fields, occurredAt, this.dict);
  }
}
