import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { load as yamlLoad } from 'js-yaml';
import { z } from 'zod';
import { SignalSeveritySchema, TreatmentDomainSchema } from '@pac/types';

/**
 * clinical-signals.yaml 的加载与校验。
 *
 * 路径用 process.cwd() 而非 __dirname —— 与 sync.service.ts / skill-registry 同一个理由:
 * SWC dev 与 tsc prod 的输出目录结构不同,__dirname 跨态不稳;cwd 在 dev/prod 都是
 * apps/pac-service 根(Dockerfile:47 把 data/ 复制到镜像同一相对位置)。
 */

const TtlSchema = z.union([
  z.literal('lifelong'),
  z.object({ days: z.number().int().positive() }).strict(),
  z.object({ review_days: z.number().int().positive() }).strict(),
]);
export type SignalTtl = z.infer<typeof TtlSchema>;

const SignalDefSchema = z.object({
  code: z.string().min(1),
  label: z.string().min(1),
  any: z.array(z.string().min(1)).min(1),
  /**
   * 排除词 —— 命中位置若落在这些更长的词里,该次命中作废。
   * 中文没有词边界,`any` 的短词必然会被更长的词包住(「孕期」⊂「备孕期」),
   * 靠加长 `any` 治不了(变体无穷),必须有一条显式的反向清单。
   */
  not_any: z.array(z.string().min(1)).optional(),
  contraindicates: z.array(TreatmentDomainSchema),
  severity: SignalSeveritySchema,
  ttl: TtlSchema,
  note: z.string().optional(),
});
export type SignalDef = z.infer<typeof SignalDefSchema>;

const ScanFieldSchema = z.object({
  fact_type: z.string().min(1),
  fields: z.array(z.string().min(1)).min(1),
});

const NegationSchema = z.object({
  sentence_delimiters: z.array(z.string().min(1)).min(1),
  markers: z.array(z.string().min(1)).min(1),
  reassert: z.array(z.string().min(1)).min(1),
  undecidable_patterns: z.array(z.string().min(1)),
});

export const SignalDictSchema = z.object({
  version: z.string().min(1),
  scan_fields: z.array(ScanFieldSchema).min(1),
  negation: NegationSchema,
  signals: z.array(SignalDefSchema).min(1),
});
export type SignalDict = z.infer<typeof SignalDictSchema>;

export function resolveSignalDictPath(): string {
  const override = process.env.PAC_CLINICAL_SIGNALS_DICT;
  if (override) return override;
  return resolve(process.cwd(), 'data/_shared/dict/clinical-signals.yaml');
}

/**
 * 加载并校验字典。校验失败**直接抛** —— 与 fact-writer 的 zod 闸同一个态度:
 * 不允许"宽松降级",一个写错的字典会静默把禁忌判反,那比启动失败危险得多。
 */
export function loadSignalDict(path = resolveSignalDictPath()): SignalDict {
  if (!existsSync(path)) {
    throw new Error(`clinical-signals 字典不存在:${path}(cwd=${process.cwd()})`);
  }
  const dict = SignalDictSchema.parse(yamlLoad(readFileSync(path, 'utf8')));

  const dupes = dict.signals
    .map((s) => s.code)
    .filter((c, i, arr) => arr.indexOf(c) !== i);
  if (dupes.length > 0) {
    throw new Error(`clinical-signals 字典存在重复 code:${[...new Set(dupes)].join(', ')}`);
  }
  // absolute/relative 必须声明治疗域,否则这条信号打了标签却不知道禁什么 —— 是配置错,不是空集。
  for (const s of dict.signals) {
    if (s.severity !== 'check' && s.contraindicates.length === 0) {
      throw new Error(`信号 ${s.code} severity=${s.severity} 却未声明 contraindicates`);
    }
    if (s.severity === 'check' && s.contraindicates.length > 0) {
      throw new Error(`信号 ${s.code} severity=check 不应声明 contraindicates(核查项不产禁忌)`);
    }
  }
  return dict;
}

/** (factType → 要扫的字段名)索引。抽取器按 fact 类型 O(1) 取。 */
export function buildScanIndex(dict: SignalDict): Map<string, string[]> {
  const idx = new Map<string, string[]>();
  for (const sf of dict.scan_fields) idx.set(sf.fact_type, sf.fields);
  return idx;
}
