import { Injectable, Logger } from '@nestjs/common';
import type { Action } from '@pac/types';
import { PrismaService } from '../../../prisma/prisma.service';
import { ParserRegistry } from './parsers/parser.registry';
import { ClinicalSignalService } from '../../clinical-signals/clinical-signal.service';
import {
  type BulkEntry,
  type FactReject,
  FactWriter,
  FactWriteResult,
} from './fact-writer.service';
import { FactContentSchemaError } from './fact-content-schemas';

/**
 * ParserPipeline — transaction → fact 衍生编排器
 *
 * 输入:已写库的 transaction(含 id)+ 对应 canonical row
 * 流程:
 *   1. 按 transaction.action 查 ParserRegistry
 *   2. parser.parse(ctx) → FactDraft[]
 *   3. ClinicalSignalService.enrich —— 按 clinical-signals.yaml 配置的字段抽临床信号,
 *      命中则写进 draft.content.clinical_signals(**不新建衍生 fact**,见该 service 注释)
 *   4. 逐个调用 FactWriter.writeDraft
 *   5. 汇总 metrics(created / superseded / unchanged / evidence_appended)
 *
 * 信号抽取放在这一层而不是各 parser 里:源字段跨 fact 类型且由 yaml 配置
 * (既往史在 emr_record,诊断严重度在 diagnosis_record.name_zh),挂 pipeline 上加字段只改 yaml。
 *
 * 设计:
 *   - 无 parser 的 action 静默跳过(返回 zero metrics)— transaction 已落账,稍后补 parser 重放即可
 *   - 单条 transaction 的多 fact 衍生**不开外层事务** — 每 draft 一次 $transaction(FactWriter 内部)
 *     原因:外层事务跨多 subject 会放大锁范围;每 fact 独立写更适配未来 BullMQ jobName=subject_id 串行化
 *   - parser 抛错时单条 transaction 整体计 failed,不影响后续 transaction(由上层捕获)
 */
@Injectable()
export class ParserPipeline {
  private readonly logger = new Logger(ParserPipeline.name);

  constructor(
    private readonly registry: ParserRegistry,
    private readonly writer: FactWriter,
    private readonly prisma: PrismaService,
    private readonly signals: ClinicalSignalService,
  ) {}

  /**
   * @returns PipelineRunMetrics 本次 transaction 衍生统计(单条 transaction)
   */
  async runForTransaction(input: {
    transaction: {
      id: string;
      hostId: string;
      tenantId: string;
      patientId: string | null;
      action: Action;
      subjectType: string;
      subjectId: string;
      occurredAt: Date;
      clinicId: string;
    };
    canonicalRow: Record<string, unknown>;
  }): Promise<PipelineRunMetrics> {
    const { transaction, canonicalRow } = input;
    const parser = this.registry.get(transaction.action);
    const metrics: PipelineRunMetrics = {
      action: transaction.action,
      parserMatched: !!parser,
      factsCreated: 0,
      factsSuperseded: 0,
      factsUnchanged: 0,
      factsEvidenceAppended: 0,
      factsStaleSkipped: 0,
      factsFailed: 0,
      factRejects: [],
      writes: [],
    };

    if (!parser) {
      this.logger.debug(
        `no parser for action=${transaction.action};skip(transaction=${transaction.id})`,
      );
      return metrics;
    }

    if (!transaction.patientId) {
      this.logger.warn(
        `parser pipeline: transaction ${transaction.id} has no patientId;skip fact derivation`,
      );
      return metrics;
    }

    const drafts = this.signals.enrichAll(
      parser.parse({ transaction, canonicalRow }),
      transaction.occurredAt,
    );
    const sourceUpdatedAt = parseSourceUpdatedAt(canonicalRow);

    // fact 的 source_unit = 该患者的 source_unit(集团内 subject_id 撞号消歧)
    const patientSourceUnit =
      (
        await this.prisma.patient.findUnique({
          where: { id: transaction.patientId },
          select: { sourceUnit: true },
        })
      )?.sourceUnit ?? '';

    for (const draft of drafts) {
      try {
        const result = await this.writer.writeDraft({
          draft,
          hostId: transaction.hostId,
          tenantId: transaction.tenantId,
          sourceUnit: patientSourceUnit,
          patientId: transaction.patientId,
          transactionId: transaction.id,
          sourceUpdatedAt,
        });
        metrics.writes.push(result);
        switch (result.action) {
          case 'created':
            metrics.factsCreated++;
            break;
          case 'superseded':
            metrics.factsSuperseded++;
            break;
          case 'unchanged':
            metrics.factsUnchanged++;
            break;
          case 'evidence_appended':
            metrics.factsEvidenceAppended++;
            break;
          case 'stale_skipped':
            metrics.factsStaleSkipped++;
            break;
        }
      } catch (err) {
        metrics.factsFailed++;
        this.logger.error(
          `fact write failed: subjectId=${draft.subjectId} type=${draft.type} err=${
            err instanceof Error ? err.message : String(err)
          }`,
        );
      }
    }

    return metrics;
  }

  /**
   * 批量版 — N 条 transaction 一起跑 parser → 收集所有 draft → 1 次 bulk fact write
   * PR4 引入,跟 runForTransaction 行为等价,但**减少 SQL 往返 ~ N 倍**(单次 batch SELECT + bulk INSERT/UPDATE)
   *
   * 注意:本方法假设所有 transaction 同 hostId+tenantId(processSubject 满足)
   * 返回:整批的 PipelineRunMetrics(汇总)
   */
  async runForBatch(items: BatchItem[]): Promise<PipelineRunMetrics> {
    const metrics: PipelineRunMetrics = {
      action: 'BATCH',
      parserMatched: true,
      factsCreated: 0,
      factsSuperseded: 0,
      factsUnchanged: 0,
      factsEvidenceAppended: 0,
      factsStaleSkipped: 0,
      factsFailed: 0,
      factRejects: [],
      writes: [],
    };

    // fact 的 source_unit = 该患者的 source_unit(集团内 subject_id 撞号消歧)。
    // patient 先于 fact 落库,按 patientId 批量取一次。
    const patientIds = [
      ...new Set(items.map((i) => i.transaction.patientId).filter((p): p is string => !!p)),
    ];
    const suRows = await this.prisma.patient.findMany({
      where: { id: { in: patientIds } },
      select: { id: true, sourceUnit: true },
    });
    const suMap = new Map(suRows.map((p) => [p.id, p.sourceUnit]));

    // 1. 全部 tx 走 parser,收集 drafts(in-memory)
    const bulkEntries: BulkEntry[] = [];
    for (const item of items) {
      const parser = this.registry.get(item.transaction.action);
      if (!parser) {
        this.logger.debug(`no parser for action=${item.transaction.action};skip`);
        continue;
      }
      if (!item.transaction.patientId) continue;
      try {
        const drafts = this.signals.enrichAll(
          parser.parse({
            transaction: item.transaction,
            canonicalRow: item.canonicalRow,
          }),
          item.transaction.occurredAt,
        );
        const sourceUpdatedAt = parseSourceUpdatedAt(item.canonicalRow);
        for (const draft of drafts) {
          bulkEntries.push({
            draft,
            hostId: item.transaction.hostId,
            tenantId: item.transaction.tenantId,
            sourceUnit: suMap.get(item.transaction.patientId) ?? '',
            patientId: item.transaction.patientId,
            transactionId: item.transaction.id,
            sourceUpdatedAt,
          });
        }
      } catch (err) {
        metrics.factsFailed++;
        this.logger.error(
          `parser failed: tx=${item.transaction.id} action=${item.transaction.action} ` +
            `err=${err instanceof Error ? err.message : String(err)}`,
        );
      }
    }

    if (bulkEntries.length === 0) return metrics;

    // 2. 一次 bulk write,失败降级 per-entry(写一份保证收尾)
    //    注:fact.content schema 违规**不再**触发降级 —— bulkWrite 内部把违规条目隔离进 rejected,
    //    其余照常批量写。这里的 catch 只兜住真正的批级故障(DB 异常 / 事务超时)。
    //    2026-07-29 前不是这样:一行负金额就把整批打成逐行事务,200 行 3s→63.5s,
    //    直接把宿主推送卡到超时重推死循环(详见 FactWriter.bulkWrite 注释)。
    try {
      const { results, rejected } = await this.writer.bulkWrite(bulkEntries);
      for (const r of rejected) {
        metrics.factsFailed++;
        metrics.factRejects.push(r);
      }
      // 逐个 push,不用 push(...results):单资源单批已达 5 万+ 行,spread 实参压栈会炸
      // RangeError(V8 上限 ~6.5万;同型 bug 曾炸 plans 的 selectHits,见 scenario 注释)。
      for (const r of results) {
        metrics.writes.push(r);
        switch (r.action) {
          case 'created':
            metrics.factsCreated++;
            break;
          case 'superseded':
            metrics.factsSuperseded++;
            break;
          case 'unchanged':
            metrics.factsUnchanged++;
            break;
          case 'evidence_appended':
            metrics.factsEvidenceAppended++;
            break;
          case 'stale_skipped':
            metrics.factsStaleSkipped++;
            break;
        }
      }
    } catch (err) {
      this.logger.warn(
        `bulkWrite 批失败,降级 per-entry: ${err instanceof Error ? err.message : String(err)}`,
      );
      // 降级:逐条用 writeDraft(单 entry 失败不影响其他)
      for (const e of bulkEntries) {
        try {
          const r = await this.writer.writeDraft({
            draft: e.draft,
            hostId: e.hostId,
            tenantId: e.tenantId,
            sourceUnit: e.sourceUnit,
            patientId: e.patientId,
            transactionId: e.transactionId,
            sourceUpdatedAt: e.sourceUpdatedAt,
          });
          metrics.writes.push(r);
          switch (r.action) {
            case 'created':
              metrics.factsCreated++;
              break;
            case 'superseded':
              metrics.factsSuperseded++;
              break;
            case 'unchanged':
              metrics.factsUnchanged++;
              break;
            case 'evidence_appended':
              metrics.factsEvidenceAppended++;
              break;
            case 'stale_skipped':
              metrics.factsStaleSkipped++;
              break;
          }
        } catch (subErr) {
          metrics.factsFailed++;
          if (subErr instanceof FactContentSchemaError) {
            metrics.factRejects.push({
              type: subErr.factType,
              subjectId: subErr.subjectId,
              transactionId: e.transactionId,
              issues: subErr.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`),
            });
          }
          this.logger.error(
            `fallback writeDraft failed: tx=${e.transactionId} subject=${e.draft.subjectId} ` +
              `err=${subErr instanceof Error ? subErr.message : String(subErr)}`,
          );
        }
      }
    }

    return metrics;
  }
}

export interface BatchItem {
  transaction: {
    id: string;
    hostId: string;
    tenantId: string;
    patientId: string | null;
    action: Action;
    subjectType: string;
    subjectId: string;
    occurredAt: Date;
    clinicId: string;
  };
  canonicalRow: Record<string, unknown>;
}

export interface PipelineRunMetrics {
  action: string;
  parserMatched: boolean;
  factsCreated: number;
  factsSuperseded: number;
  factsUnchanged: number;
  factsEvidenceAppended: number;
  /// 乱序防护跳过数(更旧 sourceUpdatedAt 晚到,未旧覆新)
  factsStaleSkipped: number;
  factsFailed: number;
  /// 被 fact.content schema 拒收的条目明细(定位信息,无 content 原文)——
  /// 一路透到 push 回执,让宿主知道是自己哪一行有问题,而不是只看到超时。
  factRejects: FactReject[];
  writes: FactWriteResult[];
}

/// canonicalRow.updatedAt(宿主末次修改时间,string / Date)→ Date;缺失或不可解析 → null(退回到达顺序语义)。
function parseSourceUpdatedAt(canonicalRow: Record<string, unknown>): Date | null {
  const v = canonicalRow.updatedAt;
  if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
  if (typeof v === 'string' && v.trim()) {
    const d = new Date(v);
    return isNaN(d.getTime()) ? null : d;
  }
  return null;
}
