import { Injectable, Logger } from '@nestjs/common';
import { createHash } from 'node:crypto';
import { Action, ActionSchema, SubjectType, SubjectTypeSchema } from '@pac/types';
import type { Prisma } from '@prisma/client';
import type { EmitsConfig } from '../assembler/assembler.schema';

/**
 * Transaction Synthesizer(v2.1 重塑)
 *
 * 把 canonical row + yaml emits 合成 PatientTransaction 写入对象。
 *
 * **v2.1 关键变更**(canonical-fact-layer.md §五):
 *   - 删除 resource → action / subjectType 硬映射(原 actionForResource / subjectTypeForResource)
 *   - 改读 yaml 显式声明的 `emits` 三件套(action / subjectType / occurredAtField)
 *   - 写 `rawPayload`(host 原文)—— 唯一留底,reparse 的输入
 *   - action / subjectType 仅做枚举合法性校验,值由 yaml 决定
 *
 * 三个上游入口共用:
 *   - ColdImportService(W2,文件)
 *   - PullAdapter(W5+,HTTP 周期拉)
 *   - PushReceiver(W5+,HTTP webhook)
 */
@Injectable()
export class TransactionSynthesizer {
  private readonly logger = new Logger(TransactionSynthesizer.name);

  /**
   * 把 canonical row + emits 合成为 patient_transactions 写入对象。
   *
   * @param input.rawRow         host 原生行(rawPayload 用)
   * @param input.canonicalRow   field-mapped 后的行(派生 transaction 立柱字段用;**不再留底**,见 schema 注释)
   * @param input.emits          yaml 显式声明的 action/subjectType/occurredAtField(必须)
   * @param input.resource       canonical resource key(日志 + sourceEventId 用)
   * @param input.hostId         host UUID
   * @param input.tenantId       tenant id(来自 manifest 或 token)
   * @param input.sourceContext  来源上下文,合成 sourceEventId 幂等键
   *                              (如 'cold-import:<dir>:<runId>' / 'pull:<runId>')
   */
  synthesize(input: {
    rawRow: Record<string, unknown>;
    canonicalRow: Record<string, unknown>;
    emits: EmitsConfig | undefined;
    resource: string;
    hostId: string;
    tenantId: string;
    patientIdResolver: (externalId: string) => string | undefined;
    sourceContext: string;
    /** 患者命名空间(集团模型):并进 sourceEventId,避免两品牌同 subjectId+updatedAt 撞键被去重。 */
    sourceUnit: string;
  }): Prisma.PatientTransactionUncheckedCreateInput | null {
    const { emits } = input;
    if (!emits) {
      this.logger.warn(
        `资源 ${input.resource} 的 yaml 缺 emits 段,跳过 transaction 合成 ` +
          `(只有 patient 主档允许无 emits;其他资源必须声明 action/subjectType/occurredAtField)`,
      );
      return null;
    }

    // ─── 枚举合法性校验(yaml 写错不能蒙混入库)──
    const actionParsed = ActionSchema.safeParse(emits.action);
    if (!actionParsed.success) {
      this.logger.error(
        `yaml emits.action="${emits.action}" 不在 PAC Action 封闭集中;resource=${input.resource}`,
      );
      return null;
    }
    const subjectTypeParsed = SubjectTypeSchema.safeParse(emits.subjectType);
    if (!subjectTypeParsed.success) {
      this.logger.error(
        `yaml emits.subjectType="${emits.subjectType}" 不在 PAC SubjectType 封闭集中;resource=${input.resource}`,
      );
      return null;
    }
    const action: Action = actionParsed.data;
    const subjectType: SubjectType = subjectTypeParsed.data;

    // canonical 字段提取
    const c = input.canonicalRow;
    const subjectId = String(c.externalId ?? '').trim();
    if (!subjectId) {
      this.logger.warn(
        `Missing externalId in canonical row for ${input.resource};skipping`,
      );
      return null;
    }

    const clinicId = String(c.clinicId ?? c.clinic_id ?? '').trim();
    if (!clinicId) {
      this.logger.warn(
        `Missing clinicId for ${input.resource} externalId=${subjectId};skipping(clinic 是立柱必填)`,
      );
      return null;
    }

    const patientExternalId = c.patientExternalId as string | undefined;
    const patientId = patientExternalId
      ? input.patientIdResolver(patientExternalId)
      : undefined;

    // occurredAt — 从 yaml emits.occurredAtField 指定的字段拿;
    // 缺失/不可解析时确定性回退 updatedAt → createdAt(与 sourceEventId 的 updatedAtMark 同序)。
    // 墙钟只在三者皆缺时兜底:否则同一源行每次摄入 occurredAt 抖动,
    // 下游 fact 时间锚变更检测会产生伪 supersede 版本。
    let occurredAt = this.pickOccurredAt(c, emits.occurredAtField);
    if (!occurredAt) {
      occurredAt =
        this.pickOccurredAt(c, 'updatedAt') ?? this.pickOccurredAt(c, 'createdAt');
    }
    if (!occurredAt) {
      this.logger.warn(
        `Cannot determine occurredAt from field="${emits.occurredAtField}" ` +
          `(updatedAt/createdAt 亦缺)for ${input.resource} externalId=${subjectId};using now()`,
      );
      occurredAt = new Date();
    }

    // sourceEventId 幂等键
    // 冷启同 row + 同 updatedAt 视为同一事件(re-import 时 idempotent)
    const updatedAtMark =
      (c.updatedAt as string | undefined) ?? (c.createdAt as string | undefined) ?? '';
    // source_unit 并进幂等键:集团内同 subjectId 跨品牌(瑞尔/瑞泰 encounter 1011032)是两条不同事件
    const sourceEventId = `${input.sourceContext}:${input.resource}:${input.sourceUnit}:${subjectId}:${updatedAtMark}`;

    const payloadHash = this.sha256(JSON.stringify(input.rawRow));

    return {
      hostId: input.hostId,
      tenantId: input.tenantId,
      patientId: patientId ?? null,
      sourceEventId,
      clinicId,
      action,
      subjectType,
      subjectId,
      rawPayload: input.rawRow as Prisma.InputJsonValue,
      payloadHash,
      occurredAt,
    };
  }

  /** 按 yaml 指定字段名取 occurredAt;字段值需是 ISO 字符串 / Date */
  private pickOccurredAt(
    c: Record<string, unknown>,
    fieldName: string,
  ): Date | null {
    const v = c[fieldName];
    if (typeof v === 'string' && v) {
      const d = new Date(v);
      return Number.isNaN(d.getTime()) ? null : d;
    }
    if (v instanceof Date) return v;
    return null;
  }

  private sha256(s: string): string {
    return createHash('sha256').update(s).digest('hex');
  }
}
