import { z } from 'zod';

/**
 * Canonical resource schemas — PAC AssemblerEngine 经 yaml 配置投影 + 类型归一化后期望看到的形态。
 *
 * 三个数据入口共用(对应 docs/host-integration.md §3.4):
 *   - ColdImportService(W2,CSV/JSON 文件)
 *   - PullAdapter(W5+,HTTP 周期拉)
 *   - PushReceiver(W5+,HTTP webhook)
 *
 * 字段命名跟 PAC canonical 一致(camelCase);所有 schema 用 `.passthrough()` —
 * 允许 host 透传额外字段(进 rawPayload 留底,canonical 关键字段被 zod 强约束)。
 *
 * **金额字段(amount / totalAmount / amount items[].amount)统一为分整数**(int)—
 * yuan 输入由 FieldMapper.normalize() 在 zod 前 ×100 转换。
 *
 * 失配 → ContractDriftError(SyncLog status=failed,drift detector 拾起)。
 *
 * 参考:docs/host-integration.md §3.4 / §7.4
 */

// ─────────────────────────────────────────────────────────
// Datetime 工具 schema — ISO 8601 字符串(经 normalize 后必带 offset)
// ─────────────────────────────────────────────────────────

const isoDateTime = z.string().min(1).describe('ISO 8601 datetime with timezone offset');
/// CSV 入口空字段是 `""`,统一接受空串等同于"未提供"
const optionalIsoDateTime = z.union([isoDateTime, z.literal(''), z.null()]).optional()
  .transform((v) => (v === '' || v === null ? null : v));
const optionalDate = z.union([
  z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  z.literal(''),
  z.null(),
]).optional().transform((v) => (v === '' || v === null ? null : v))
  .describe('YYYY-MM-DD');

// ─────────────────────────────────────────────────────────
// 3.4.1 patients
// ─────────────────────────────────────────────────────────

/// patient canonical — 主档(身份)+ 副表字段(profile)合并形态。
/// PAC 内部入库时拆:identity 进 patients 主表,合规/标签等进 patient_profiles 副表。
export const PatientCanonicalSchema = z
  .object({
    // ── 主表(身份)──
    externalId: z.string().min(1),
    name: z.string().optional().nullable(),
    phone: z.string().optional().nullable(),
    gender: z.string().optional().nullable(), // free text per §3.4.1
    birthDate: optionalDate,
    /// 病历号(host 病历主键,如 "BA43016");跟 externalId 互补 — 客服沟通用。
    medicalRecordNumber: z.string().optional().nullable(),
    preferences: z.record(z.string(), z.unknown()).optional().nullable(),
    /// 'active' / 'archived'(v2:'merged' 已删) — host raw 值,PAC 入库时映射成 active boolean
    status: z.string().optional().default('active'),
    createdAt: optionalIsoDateTime,
    updatedAt: optionalIsoDateTime,

    // ── 副表(profile)──
    /// 默认 false — host 不提供时不阻挡入库;合规需要时由 host 显式 true
    doNotContact: z.boolean().optional().default(false),
    doNotContactReason: z.string().optional().nullable(),
    deceased: z.boolean().optional().default(false),
    /// 宿主侧正在跟进该患者(true → 召回排除闸)。**刻意可空且无默认值**:
    /// null = 宿主未提供该信号(如 jvs-dw 未接入)→ 不启用该闸;给 false 会把未接入宿主
    /// 的患者误判成"宿主确认没在跟",语义不同。判定一律用 IS NOT TRUE,不要 = false。
    ///
    /// ⚠️ `.catch(null)` 是**刻意的防护,不是偷懒**:本字段是可选的召回闸信号,
    /// 而它所在的 patient 是**主数据**。若宿主推来无法识别的值(如 "Y2"、"待定"),
    /// 不加 catch 会让整条患者主档被 zod 拒收 —— 姓名/电话/生日全部丢失,
    /// 为一个 nice-to-have 的闸门信号赔上主数据,代价完全不成比例。
    /// 脏值降级成 null(= 信号未提供 → 不启用闸),失败方向朝"照常召回"这一侧,
    /// 而不是"静默把人挡在池外"。0/1/true/false/y/n 等常见写法由 booleanFields 先行 coerce。
    hostFollowUpActive: z.boolean().optional().nullable().catch(null),
    /// 产品收集(回访中心信息字段.docx):多源标签数组
    tags: z.array(z.string()).optional().default([]),
    /// 产品收集:host 备注 / 客户描述自由文本
    notes: z.string().optional().nullable(),
    /// 获客渠道(初诊来源,A.2.1)— PAC 立柱标准枚举(host 值经 assembler enum_mapping 归一)
    acquisitionChannel: z.string().optional().nullable(),
    /// 二级渠道(host 原值透传)
    acquisitionSub: z.string().optional().nullable(),
    /// 转介绍(B.1.2):推荐人数 / 带来转化总额(元,入库转分);DW 预聚合
    referralCount: z.coerce.number().int().optional().nullable(),
    referralAmount: z.coerce.number().optional().nullable(),
  })
  .passthrough();
export type PatientCanonical = z.infer<typeof PatientCanonicalSchema>;

/// patient_relation canonical — 患者-患者关系边(联系人/亲属/转介绍人)。
/// upsert 形态(无 emits,同 patient)。关系人本身也是 patient(FHIR RelatedPerson/link)。
export const PatientRelationCanonicalSchema = z
  .object({
    /// 本人 host id(referee.patient_id)
    patientExternalId: z.string().min(1),
    /// 关系人 host id(referee.referee_patient_id)
    relatedExternalId: z.string().min(1),
    /// 关系类型(已归一:mother/father/grandparent/spouse/child/sibling/friend/other)
    relationship: z.string().min(1),
  })
  .passthrough();
export type PatientRelationCanonical = z.infer<typeof PatientRelationCanonicalSchema>;

/// patient_return_visit canonical — 诊所回访任务记录(展示用,5 试点)。upsert 形态(无 emits)。
export const PatientReturnVisitCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().optional().nullable(),
    taskDate: z.string().optional().nullable(),
    type: z.string().optional().nullable(),
    status: z.string().optional().nullable(),
    taskStatus: z.string().optional().nullable(),
    treatmentItems: z.string().optional().nullable(),
    followContent: z.string().optional().nullable(),
    result: z.string().optional().nullable(),
  })
  .passthrough();
export type PatientReturnVisitCanonical = z.infer<typeof PatientReturnVisitCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// 3.4.2 appointments
// ─────────────────────────────────────────────────────────

export const AppointmentCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    scheduledAt: isoDateTime,
    // 8 态细分(跟 fact-content AppointmentRecordContent.status / jvs-dw enum_mapping 对齐)
    status: z.enum([
      'scheduled',
      'rescheduled',
      'cancelled',
      'arrived',
      'in_treatment',
      'completed',
      'no_show',
      'walk_in',
    ]),
    appointmentType: z.string().optional().nullable(),
    doctorId: z.string().optional().nullable(),
    complaintCategory: z.string().optional().nullable(),
    durationMinutes: z.coerce.number().optional().nullable(),
    arrivedAt: optionalIsoDateTime,
    createdAt: optionalIsoDateTime,
    updatedAt: optionalIsoDateTime,
  })
  .passthrough();
export type AppointmentCanonical = z.infer<typeof AppointmentCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// 3.4.3 clinical_encounters(召回核心)
// ─────────────────────────────────────────────────────────

export const DiagnosisItemSchema = z
  .object({
    code: z.string().optional().nullable(),
    name: z.string().optional().nullable(),
    toothPosition: z.string().optional().nullable(),
  })
  .passthrough();

/// CSV 入口字段全是字符串 — 数值字段统一用 coerce 接受 number 或 数字字符串
const coerceIntOptional = z.union([
  z.coerce.number().int(),
  z.literal(''),
  z.null(),
]).optional().transform((v) => (v === '' || v === null || v === undefined ? null : v));

const coerceInt = z.coerce.number().int();

export const TreatmentItemSchema = z
  .object({
    category: z.string().min(1),
    subtype: z.string().optional().nullable(),
    toothPosition: z.string().optional().nullable(),
    status: z.enum(['in_progress', 'completed']),
    expectedNextStep: z.string().optional().nullable(),
    expectedNextStepWindowDays: coerceIntOptional,
    relatedTreatmentSubjectId: z.string().optional().nullable(),
  })
  .passthrough();

/**
 * v2.1 拆分后:encounter 只需身份 + 时间 + 元数据
 * diagnoses / treatments 拆出独立 canonical 资源(见 DiagnosisCanonicalSchema / TreatmentCanonicalSchema)
 * 仍保留 diagnoses[] / treatments[] 字段以兼容老格式 host(都是 optional)
 */
export const ClinicalEncounterCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    startedAt: isoDateTime,
    doctorId: z.string().optional().nullable(),
    diagnoses: z.array(DiagnosisItemSchema).optional().default([]),
    treatments: z.array(TreatmentItemSchema).optional().default([]),
    notes: z.string().optional().nullable(),
    relatedTreatmentSubjectId: z.string().optional().nullable(),
    relatedTreatmentCategory: z.string().optional().nullable(),
  })
  .passthrough();
export type ClinicalEncounterCanonical = z.infer<typeof ClinicalEncounterCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// 3.4.4 charge_orders / payments / refunds
// ─────────────────────────────────────────────────────────

/// 医嘱 item — 技耗分离(治疗 service / 耗材 consumable / 药品 medication)
export const OrderItemSchema = z
  .object({
    name: z.string().min(1),
    /// 技耗分离:'treatment'(治疗服务) / 'consumable'(耗材) / 'medication'(药品) / 其他
    type: z.string().optional().nullable(),
    treatmentCategory: z.string().optional().nullable(),
    /// **value semantics = cents int**(yuan 输入由 FieldMapper.normalize 转换)
    amount: coerceInt,
    currency: z.string().optional().default('CNY'),
  })
  .passthrough();

/// 医嘱记录(治疗 / 耗材 / 药品的统一单据)
export const OrderCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    createdAt: isoDateTime,
    items: z.array(OrderItemSchema).min(1),
    totalAmount: coerceInt, // cents
    status: z.enum(['submitted', 'paid', 'partial_paid', 'cancelled']),
    updatedAt: optionalIsoDateTime,
  })
  .passthrough();
export type OrderCanonical = z.infer<typeof OrderCanonicalSchema>;

export const PaymentCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    /// 关联医嘱 order_record(技耗分离 — 按 order_record.items 各类目分别收款)
    orderExternalId: z.string().optional().nullable(),
    clinicId: z.string().min(1),
    paidAt: isoDateTime,
    amount: coerceInt, // cents(实付;2026-07 业务定调,与 refund 同口径)
    /// 应收(原价,cents,可空)— 折扣率分母 / 权益客识别;moneyFields 已登记同口径归一
    receivableAmount: coerceInt.optional().nullable(),
    currency: z.string().optional().default('CNY'),
    method: z.string().optional().nullable(),
  })
  .passthrough();
export type PaymentCanonical = z.infer<typeof PaymentCanonicalSchema>;

export const RefundCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    paymentExternalId: z.string().optional().nullable(),
    clinicId: z.string().min(1),
    refundedAt: isoDateTime,
    amount: coerceInt, // cents
    reason: z.string().optional().nullable(),
  })
  .passthrough();
export type RefundCanonical = z.infer<typeof RefundCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// 3.4.5 其他资源(consultation / visit_registration / visit_reception / emr / image)
//   doc 仅列了必填轮廓,起步保守 — 必填核心 + passthrough 其他
// ─────────────────────────────────────────────────────────

export const ConsultationCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime,
    channel: z.string().optional().nullable(),
    notes: z.string().optional().nullable(),
  })
  .passthrough();
export type ConsultationCanonical = z.infer<typeof ConsultationCanonicalSchema>;

export const VisitRegistrationCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime,
    appointmentExternalId: z.string().optional().nullable(),
  })
  .passthrough();
export type VisitRegistrationCanonical = z.infer<typeof VisitRegistrationCanonicalSchema>;

export const VisitReceptionCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime,
    receptionistId: z.string().optional().nullable(),
  })
  .passthrough();
export type VisitReceptionCanonical = z.infer<typeof VisitReceptionCanonicalSchema>;

export const EmrDocumentCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    submittedAt: isoDateTime,
    encounterExternalId: z.string().optional().nullable(),
    qualityStatus: z.string().optional().nullable(),
  })
  .passthrough();
export type EmrDocumentCanonical = z.infer<typeof EmrDocumentCanonicalSchema>;

export const ImageStudyCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    uploadedAt: isoDateTime,
    modality: z.string().optional().nullable(),
  })
  .passthrough();
export type ImageStudyCanonical = z.infer<typeof ImageStudyCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// v2.1 拆出:diagnosis / treatment / recommendation 独立 canonical 资源
//   transaction 层只校验"身份 + 时间 + 诊所"三件套;
//   临床字段(code / tooth_position / category 等)由 fact-content-schemas 在 fact 层校验。
// ─────────────────────────────────────────────────────────

/// 诊断(v2.1 独立)— 一行 host 数据投影出来的一条诊断记录
export const DiagnosisCanonicalSchema = z
  .object({
    externalId: z.string().min(1), // 诊断业务 id(host 给 / 合成均可)
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime, // 诊断时间(通常 = encounter.startedAt)
    /// 反查源接诊
    sourceEncounterExternalId: z.string().optional().nullable(),
    /// 临床字段(parser 取出来塞 fact.content,本层不强校验)
    code: z.string().optional().nullable(),
    name: z.string().optional().nullable(),
    toothPosition: z.string().optional().nullable(),
    severity: z.string().optional().nullable(),
    onsetDate: z.string().optional().nullable(),
    /// 显式来源声明(如影像 AI 'image_ai');parser 优先用它,否则按 stdCode 推断 std_code/name_map
    codeSource: z.string().optional().nullable(),
  })
  .passthrough();
export type DiagnosisCanonical = z.infer<typeof DiagnosisCanonicalSchema>;

/// 治疗(v2.1 独立)— actual / planned 由 emits.kind 区分
export const TreatmentCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime, // actual = startedAt/completedAt;planned = plannedFor
    sourceEncounterExternalId: z.string().optional().nullable(),
    relatedDiagnosisExternalId: z.string().optional().nullable(),
    category: z.string().optional().nullable(),
    subtype: z.string().optional().nullable(),
    toothPosition: z.string().optional().nullable(),
    status: z.string().optional().nullable(),
    startedAt: optionalIsoDateTime,
    completedAt: optionalIsoDateTime,
    reviewWindowDays: coerceIntOptional,
    /// W4 末加:PAC 标准 step enum 数组(host 字面值已在 yaml enum_mapping 翻译);
    /// host 没填 → undefined,chain-composer fallback subtype 词根匹配
    treatStages: z.array(z.string()).optional().nullable(),
  })
  .passthrough();
export type TreatmentCanonical = z.infer<typeof TreatmentCanonicalSchema>;

/// 医生建议(v2.1 独立,Layer C 抽取产物)
export const RecommendationCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    occurredAt: isoDateTime,
    /// 推荐码(IMPLANT_RECOMMENDED 等),由 parser 翻译到 PAC 字典
    code: z.string().optional().nullable(),
    toothPosition: z.string().optional().nullable(),
    extractedBy: z.string().optional().nullable(), // rule / llm / doctor_input
    confidence: z.number().optional().nullable(),
    sourceFactSubjectId: z.string().optional().nullable(),
    windowDays: coerceIntOptional,
  })
  .passthrough();
export type RecommendationCanonical = z.infer<typeof RecommendationCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// 产品收集(回访中心信息字段.docx)— 3 个新资源
// ─────────────────────────────────────────────────────────

/// 充值/储值/福利卡办理(中国民营牙科核心商业模式)
/// 产品收集
export const RechargeCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    rechargedAt: isoDateTime,
    /// 充值金额(cents,yuan 输入由 normalize 转换)
    amount: coerceInt,
    currency: z.string().optional().default('CNY'),
    /// 福利卡 / 储值卡 / 项目预存等
    rechargeType: z.string().optional().nullable(),
    /// host 自定义福利内容(JSON,如赠送项目 / 抵扣比例)
    benefits: z.record(z.string(), z.unknown()).optional().nullable(),
  })
  .passthrough();
export type RechargeCanonical = z.infer<typeof RechargeCanonicalSchema>;

/// 投诉记录(合规营销避雷的关键信号)
/// 产品收集
export const ComplaintCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    patientExternalId: z.string().min(1),
    clinicId: z.string().min(1),
    filedAt: isoDateTime,
    /// 投诉内容自由文本
    content: z.string().optional().nullable(),
    /// 关联治疗(如有)— 引用 treatment_record subject_id 或 host 治疗 id
    relatedTreatmentSubjectId: z.string().optional().nullable(),
    /// 投诉类别(host 自定义,如 service / clinical / billing)
    category: z.string().optional().nullable(),
    /// 处理状态(open / resolved / pending)— host 编码,assembler.yaml enum_mapping 翻译
    status: z.string().optional().default('open'),
    resolvedAt: optionalIsoDateTime,
  })
  .passthrough();
export type ComplaintCanonical = z.infer<typeof ComplaintCanonicalSchema>;

/// 转介绍记录(老客带新)
/// 产品收集
/// referral_record:fact 挂在**被推荐人**(referee = patientExternalId),
/// 推荐人(referrer)信息进 content 快照(PAC 不建推荐人主数据表,
/// referrer 可能是别 host 患者 / 员工,放 content 更通用)。
export const ReferralCanonicalSchema = z
  .object({
    externalId: z.string().min(1),
    /// 被推荐人(referee)= fact 挂的 patient
    patientExternalId: z.string().optional().nullable(),
    /// clinic 可空:referral 来自 patient 主档时无明确 clinic
    clinicId: z.string().optional().nullable(),
    recordedAt: optionalIsoDateTime,
    /// 推荐人(referrer)信息 — 快照,不依赖 referrer 是否在 PAC patient 库
    referrerExternalId: z.string().optional().nullable(),
    referrerName: z.string().optional().nullable(),
    referrerChartNumber: z.string().optional().nullable(),
    /// 推荐人类型:patient(老带新)/ staff(员工)/ partner / other
    referrerType: z.string().optional().nullable(),
    /// 渠道:wechat / face_to_face / event / ...(host 自定义)
    channel: z.string().optional().nullable(),
  })
  .passthrough();
export type ReferralCanonical = z.infer<typeof ReferralCanonicalSchema>;

// ─────────────────────────────────────────────────────────
// Registry — resource key → schema
// ─────────────────────────────────────────────────────────

/**
 * resource key 跟 manifest.tables[*].table / assembler.canonical 严格对齐;
 * `patient` 用单数(对应 PAC patients 主档,跟 SubjectType 不重叠)。
 *
 * **金额字段 + 时间字段**:normalize 阶段按 resource 查这份注册表的元信息,知道
 * 该资源里哪些 key 是 money(yuan → cents)、哪些是 datetime(无 tz → 加 offset)。
 */
export const CanonicalResourceSchemas = {
  patient: PatientCanonicalSchema,
  patient_relation: PatientRelationCanonicalSchema,
  patient_return_visit: PatientReturnVisitCanonicalSchema,
  consultation: ConsultationCanonicalSchema,
  appointment: AppointmentCanonicalSchema,
  visit_registration: VisitRegistrationCanonicalSchema,
  visit_reception: VisitReceptionCanonicalSchema,
  encounter: ClinicalEncounterCanonicalSchema,
  // v2.1 拆出
  diagnosis: DiagnosisCanonicalSchema,
  treatment: TreatmentCanonicalSchema,
  recommendation: RecommendationCanonicalSchema,
  emr: EmrDocumentCanonicalSchema,
  order: OrderCanonicalSchema,
  payment: PaymentCanonicalSchema,
  refund: RefundCanonicalSchema,
  image: ImageStudyCanonicalSchema,
  // 产品收集
  recharge: RechargeCanonicalSchema,
  complaint: ComplaintCanonicalSchema,
  referral: ReferralCanonicalSchema,
} as const;

export type CanonicalResourceKey = keyof typeof CanonicalResourceSchemas;

/**
 * 资源 → (money fields, datetime fields, top-level boolean fields)元信息。
 * FieldMapper.normalize() 用这份元信息按需做类型归一化。
 *
 * money 字段 = 该字段值在 amountUnit=yuan 时需要 ×100 转分。
 * datetime 字段 = 无 tz 时按 timezone 配置补 offset。
 * boolean 字段 = host 用 0/1/"Y"/"N"/"true"/"false" 都接收,统一转 bool。
 */
export const CanonicalResourceMeta: Record<
  CanonicalResourceKey,
  {
    moneyFields: readonly string[];
    moneyArrayFields: readonly { array: string; field: string }[]; // 数组里嵌套 money 字段
    datetimeFields: readonly string[];
    booleanFields: readonly string[];
  }
> = {
  patient: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['createdAt', 'updatedAt'],
    booleanFields: ['doNotContact', 'deceased', 'hostFollowUpActive'],
  },
  patient_relation: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: [],
    booleanFields: [],
  },
  patient_return_visit: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: [],
    booleanFields: [],
  },
  consultation: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt'],
    booleanFields: [],
  },
  appointment: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['scheduledAt', 'arrivedAt', 'createdAt', 'updatedAt'],
    booleanFields: [],
  },
  visit_registration: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt'],
    booleanFields: [],
  },
  visit_reception: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt'],
    booleanFields: [],
  },
  encounter: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['startedAt'],
    booleanFields: [],
  },
  diagnosis: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt'],
    booleanFields: [],
  },
  treatment: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt', 'startedAt', 'completedAt'],
    booleanFields: [],
  },
  recommendation: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['occurredAt'],
    booleanFields: [],
  },
  emr: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['submittedAt'],
    booleanFields: [],
  },
  order: {
    moneyFields: ['totalAmount'],
    moneyArrayFields: [{ array: 'items', field: 'amount' }],
    datetimeFields: ['createdAt', 'updatedAt'],
    booleanFields: [],
  },
  payment: {
    // 折扣三件套(诊所可控折扣额)跟 amount 同口径按 amountUnit 归一成分,
    // 避免 parser 自己 ×100 硬编码 yuan(fen 宿主会双转)。passthrough 字段,normalizer 按 `k in out` 命中才转。
    // receivableAmount = 应收(原价)— 2026-07 业务定调:amount 切实付后,应收留作折扣率分母/参考。
    moneyFields: ['amount', 'receivableAmount', 'discountDept', 'discountCompany', 'discountCard'],
    moneyArrayFields: [],
    datetimeFields: ['paidAt'],
    booleanFields: [],
  },
  refund: {
    moneyFields: ['amount'],
    moneyArrayFields: [],
    datetimeFields: ['refundedAt'],
    booleanFields: [],
  },
  image: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['uploadedAt'],
    booleanFields: [],
  },
  // ── 产品收集 ──
  recharge: {
    moneyFields: ['amount'],
    moneyArrayFields: [],
    datetimeFields: ['rechargedAt'],
    booleanFields: [],
  },
  complaint: {
    moneyFields: [],
    moneyArrayFields: [],
    datetimeFields: ['filedAt', 'resolvedAt'],
    booleanFields: [],
  },
  referral: {
    moneyFields: ['conversionAmount'],
    moneyArrayFields: [],
    datetimeFields: ['recordedAt'],
    booleanFields: [],
  },
};

// Host raw → PAC canonical 投影由 AssemblerEngine + 文件级 assembler.yaml 配置驱动,
// 不再在 @pac/types 这里提供 FieldMappingShape(v1 已删)。
// 看 apps/pac-service/src/modules/sync/assembler/assembler.schema.ts
