import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { z } from 'zod';

/**
 * 字典 include —— assembler yaml 的 `dict_includes:` 解析与合并。
 *
 * 动机(多宿主字典分层):enum/keyword 映射词分两类 ——
 *   ① 临床通用知识(慢性牙髓炎→K04、开髓→pulp_extirpation):跟宿主无关,拷贝会漂移
 *     (实证:jvs-dw 自己的 treatment_planned 字典已是 treatment_actual 的陈旧拷贝);
 *   ② 宿主怪癖(价格表项名、标点残留、host 用词判例):共用会互相污染。
 * → ① 抽到 data/_shared/dict/*.yaml,宿主 yaml 用 `dict_includes:` 引入;② 留宿主本地。
 *
 * 用法(assembler yaml 顶层):
 *   dict_includes:
 *     - ../../_shared/dict/diagnosis-kcode.yaml     # 路径相对本 yaml 文件
 *
 * 合并语义(⚠️ 三条规则,变更需同步 pac-docs data-ingestion 文档):
 *   - enum_mapping   :同字段逐 key 合并;include 按声明序先铺,本地条目**覆盖**同 key
 *                     (宿主可用本地 `词: ""` 墓碑禁用共享词条 —— "" 与 _default 空值同义)。
 *   - keyword_mapping:同字段规则数组拼接,**本地规则在前**(引擎首个命中即用 → 宿主优先),
 *                     include 按声明序殿后兜底。
 *   - keyword_strip_clauses:去重并集,本地在前(引擎对全列表做 startsWith,序无语义)。
 *   - `_default` 约定留宿主本地声明(共享文件禁止携带,schema 强校验拦截)。
 *
 * 解析发生在 zod 校验【之前】(loadAllAssemblers),合并产物 = 现行 AssemblerConfig 形状
 * → 引擎(assembler-engine)与 schema 零改动;不写 dict_includes 的 yaml 行为分毫不变。
 */

const EnumMapSchema = z.record(z.string(), z.record(z.string(), z.string()));
const KeywordRuleSchema = z.object({
  value: z.string(),
  any: z.array(z.string().min(1)).min(1),
  none: z.array(z.string().min(1)).optional(),
});
const KeywordMapSchema = z.record(z.string(), z.array(KeywordRuleSchema));

/// 共享字典文件形状:只允许三个映射段(strict:打错字段名/夹带 field_mapping 等 fail-fast)。
const DictIncludeFileSchema = z
  .object({
    enum_mapping: EnumMapSchema.optional(),
    keyword_mapping: KeywordMapSchema.optional(),
    keyword_strip_clauses: z.array(z.string().min(1)).optional(),
  })
  .strict()
  .superRefine((file, ctx) => {
    // _default 是宿主决策(丢弃 or 空码兜底),共享层禁止携带 —— 防止共享文件悄悄改宿主兜底行为
    for (const [field, m] of Object.entries(file.enum_mapping ?? {})) {
      if (Object.prototype.hasOwnProperty.call(m, '_default')) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: `共享字典不得含 _default(enum_mapping.${field});_default 由宿主 yaml 本地声明`,
        });
      }
    }
  });
export type DictIncludeFile = z.infer<typeof DictIncludeFileSchema>;

interface RawAssemblerYaml {
  dict_includes?: unknown;
  enum_mapping?: Record<string, Record<string, string>>;
  keyword_mapping?: Record<string, unknown[]>;
  keyword_strip_clauses?: string[];
  [k: string]: unknown;
}

/**
 * 解析 yaml 原始对象里的 dict_includes 并合并,返回可直接过 AssemblerConfigSchema 的对象。
 * 无 dict_includes → 原样返回(零开销,老 yaml 完全不走新逻辑)。
 * @param raw       yaml.load 的原始产物
 * @param yamlPath  本 yaml 的绝对路径(include 相对路径的锚点;也用于报错定位)
 */
export function resolveDictIncludes(raw: unknown, yamlPath: string): unknown {
  if (raw === null || typeof raw !== 'object') return raw;
  const cfg = raw as RawAssemblerYaml;
  if (cfg.dict_includes === undefined) return raw;

  const incPaths = z.array(z.string().min(1)).min(1).safeParse(cfg.dict_includes);
  if (!incPaths.success) {
    throw new Error(`${yamlPath}: dict_includes 须为非空字符串数组`);
  }

  // 1. 逐个加载 include 文件(相对本 yaml;strict 校验)
  const includes: DictIncludeFile[] = incPaths.data.map((rel) => {
    const abs = path.resolve(path.dirname(yamlPath), rel);
    if (!fs.existsSync(abs)) {
      throw new Error(`${yamlPath}: dict include 不存在: ${rel} → ${abs}`);
    }
    const parsed = yaml.load(fs.readFileSync(abs, 'utf-8'));
    const r = DictIncludeFileSchema.safeParse(parsed);
    if (!r.success) {
      throw new Error(
        `共享字典 ${rel} 校验失败(由 ${path.basename(yamlPath)} 引入):\n` +
          JSON.stringify(r.error.issues, null, 2),
      );
    }
    return r.data;
  });

  // 2. enum_mapping:include 按序先铺(后者覆盖前者),本地最后覆盖
  const mergedEnum: Record<string, Record<string, string>> = {};
  for (const inc of includes) {
    for (const [field, m] of Object.entries(inc.enum_mapping ?? {})) {
      mergedEnum[field] = { ...mergedEnum[field], ...m };
    }
  }
  for (const [field, m] of Object.entries(cfg.enum_mapping ?? {})) {
    mergedEnum[field] = { ...mergedEnum[field], ...m };
  }

  // 3. keyword_mapping:本地规则在前,include 按序殿后
  const mergedKeyword: Record<string, unknown[]> = {};
  for (const [field, rules] of Object.entries(cfg.keyword_mapping ?? {})) {
    mergedKeyword[field] = [...rules];
  }
  for (const inc of includes) {
    for (const [field, rules] of Object.entries(inc.keyword_mapping ?? {})) {
      mergedKeyword[field] = [...(mergedKeyword[field] ?? []), ...rules];
    }
  }

  // 4. keyword_strip_clauses:本地在前去重并集
  const strip = [
    ...(cfg.keyword_strip_clauses ?? []),
    ...includes.flatMap((i) => i.keyword_strip_clauses ?? []),
  ];
  const mergedStrip = [...new Set(strip)];

  // 5. 拼回(剥掉 dict_includes;空段不落 key,与"没写该段"的老形状一致)
  const { dict_includes: _drop, ...rest } = cfg;
  return {
    ...rest,
    ...(Object.keys(mergedEnum).length > 0 ? { enum_mapping: mergedEnum } : {}),
    ...(Object.keys(mergedKeyword).length > 0 ? { keyword_mapping: mergedKeyword } : {}),
    ...(mergedStrip.length > 0 ? { keyword_strip_clauses: mergedStrip } : {}),
  };
}
