import { z } from 'zod';
import {
  CanonicalResourceSchemas,
  ActionSchema,
  SubjectTypeSchema,
  FactKindSchema,
} from '@pac/types';

/**
 * AssemblerConfig — 一份 yaml,描述"把 host 若干裸表投影成一个 canonical 资源"。
 *
 * 一个 host × N 个 canonical resource = N 份 yaml。
 * 同一份 host 的所有 yaml 放一个目录(如 `configs/assemblers/friday/*.yaml`)。
 *
 * 思路:
 *   - host 导出原始表(`SELECT * FROM tb_xxx`)→ 一张表一份 CSV/JSON
 *   - 这份 yaml 告诉 AssemblerEngine:
 *       primary:        主表是谁 + 主键字段名(每行 = 一个 canonical resource)
 *       field_mapping:  主表字段名 → canonical 字段名
 *       join_arrays:    哪些 1:N 子表要嵌套到主表(diagnoses[] / treatments[] / items[]等)
 *       enum_mapping:   host 编码值 → PAC 枚举值(A01 → endodontic)
 *
 * 不放在 yaml 的:
 *   - amountUnit / timezone(放 manifest 顶层,所有资源共用)
 *   - PAC canonical 自身的形状(zod schema 已在 @pac/types,出口处校验)
 */

const fieldMapping = z.record(z.string(), z.string());
const enumMappingForField = z.record(z.string(), z.string());

/// 关键词分类规则(有序数组 = 优先级,从前到后首个命中即用)。
/// 仅在精确 enum_mapping 未命中时启用,作为 _default 之前的一层兜底。
/// normalized 文本(剥离 keyword_strip_clauses 段后)含 `any` 中任一词 → 赋 `value`(PAC enum)。
/// 匹配算法在引擎(切段 / 含词 / 按序裁决,host 无关);规则词在 yaml(host 术式差异)。
const KeywordRuleSchema = z.object({
  /// 命中后赋的 PAC enum 值(如 'endodontic'); '' 表示丢弃(等价 _default 空)
  value: z.string(),
  /// 含任一词即命中(子串 includes)
  any: z.array(z.string().min(1)).min(1),
  /// 排除词:命中 any 但也含 none 中任一 → 不算命中(语义反转,如 龋 any=[龋] none=[风险,可疑])
  none: z.array(z.string().min(1)).optional(),
});
export type KeywordRule = z.infer<typeof KeywordRuleSchema>;
const keywordMappingForField = z.array(KeywordRuleSchema);

/// 子数组:host 的 1:N 关联表 → canonical 资源里的数组字段
export const JoinArraySchema = z.object({
  /// 子表名(对应文件里 raw_tables[<这里>])
  table: z.string().min(1),
  /// 外键字段(子表里指向主表主键的列)
  foreign_key: z.string().min(1),
  /// 子表字段 → 数组元素 canonical 字段名
  field_mapping: fieldMapping,
  /// 数组元素的枚举映射(key 是数组元素的 canonical 字段名)
  enum_mapping: z.record(z.string(), enumMappingForField).optional(),
});
export type JoinArrayConfig = z.infer<typeof JoinArraySchema>;

/**
 * emits — yaml 显式声明该 canonical 投影出来的 transaction 应该带的"三件套":
 *   action       — PAC Action 封闭枚举(yaml 必须从枚举里挑)
 *   subjectType  — PAC SubjectType 封闭枚举
 *   occurredAt   — canonical row 哪个字段拿来作为 transaction.occurredAt
 *   kind         — fact.kind(actual / planned)— **可选**,仅 treatment 这种 actual/planned
 *                  共用 canonical resource 时需要;parser 也可自己从 row 推
 *
 * **v2.1 关键变更**:废除 TransactionSynthesizer 的 resource → action 硬映射。
 *   原因:同一 canonical 资源在不同 yaml(不同 host)下可能映射到不同 action
 *   (例:host A 没有 treatment_planned,只投 treatment_completed;host B 两个都有)。
 *   yaml 声明 emits 后,synthesizer 只做枚举合法性校验,不再硬编码。
 */
export const EmitsSchema = z.object({
  action: ActionSchema,
  subjectType: SubjectTypeSchema,
  /// canonical row 字段名(经 field_mapping 后的)— 该字段值作为 transaction.occurredAt
  occurredAtField: z.string().min(1),
  /// 可选;parser 可据此知道产 actual 还是 planned fact
  kind: FactKindSchema.optional(),
});
export type EmitsConfig = z.infer<typeof EmitsSchema>;

export const AssemblerConfigSchema = z.object({
  /// 目标 canonical 资源(必须是 CanonicalResourceSchemas 注册过的 key)
  canonical: z.string().refine(
    (k) => Object.prototype.hasOwnProperty.call(CanonicalResourceSchemas, k),
    'canonical must be a registered CanonicalResourceKey',
  ),

  /// 该资源投出来 transaction 时使用的 action/subjectType/occurredAt
  /// patient assembler 不需要(走 upsert 不进 transaction);其他都必填
  emits: EmitsSchema.optional(),

  primary: z.object({
    /// 主表名
    table: z.string().min(1),
    /// 主键字段(供子表 foreign_key 关联用)
    key: z.string().min(1),
    /// **炸行兜底**:若 DW ETL 把 1:N 子表(影像 / 结算明细等)写成 LEFT JOIN 没 groupArray,
    /// 主表 id 会被复制 N 次。配 dedup_by=id 时 AssemblerEngine 按 id 取首行,其他重复行丢弃。
    /// 详见 apps/pac-docs/content/docs/architecture/data-ingestion.mdx §3.5 附录 B 第 3 条。
    dedup_by: z.string().optional(),
  }),

  /// 主表字段 → canonical 字段映射(canonical key ← host field)
  field_mapping: fieldMapping,

  /// 主表字段的枚举映射(key 是 canonical 字段名)
  enum_mapping: z.record(z.string(), enumMappingForField).optional(),

  /// 关键词分类兜底(key 是 canonical 字段名)— 精确 enum_mapping 未命中时按序匹配。
  /// 治本长尾:host 术式 free-text(2万+ distinct)精确字典覆盖不全,关键词一条吃一类。
  keyword_mapping: z.record(z.string(), keywordMappingForField).optional(),

  /// 关键词匹配前剥离的"条件/未来从句"前缀(按 , ; 。切段,丢以这些词开头的段)。
  /// 例:actual 治疗 "充填,必要时根管治疗" → 丢 "必要时根管治疗" → 只剩 "充填" → restorative。
  /// planned 计划不配(计划本就以"建议/拟"措辞,不能剥)。host/语义差异在此声明,引擎通用。
  keyword_strip_clauses: z.array(z.string().min(1)).optional(),

  /// 1:N 子表嵌套(canonical 数组字段名 → 子表 + 字段映射)
  join_arrays: z.record(z.string(), JoinArraySchema).optional(),
});
export type AssemblerConfig = z.infer<typeof AssemblerConfigSchema>;
