import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import {
  classifyPopulation,
  type Skill,
  type SkillMatchContext,
  type ScriptTier,
} from './skill.types';
import type { DraftPlanScriptInput } from './input.types';
import { resolveBaseCommonPath, resolveBaseFormatPath } from './skill-registry.service';
import { forbiddenWordsBlock } from './safety-rules';

/**
 * SkillComposer — 纯函数式,把 input + 全 skills → matched skills + system prompt。
 *
 * 流程:
 *   1. 从 input 派生 SkillMatchContext(scenario / diagnosisCodes / age / population / relationship)
 *   2. 全 skills 过滤 applies match
 *   3. 跨维度排除(如 child + K08 → drop K08;allowedPopulation 列表非空时必须包含当前 population)
 *   4. 按 priority 升序排
 *   5. 拼装:base-system.md + matched skills body
 *   6. 算 composeHash(给 promptVersion 用,版本归因 SQL `GROUP BY promptVersion` 看效果)
 *
 * Composer 不持有状态,所有 input 走参数 — 易测、可 inline 在 call.ts 跑。
 */

export interface ComposedSystem {
  /** 拼好的完整 system prompt(base + 各 skill body) */
  systemPrompt: string;
  /** 匹配到的 skills(已按 priority 升序);测试 + agent_invocations 落账 + user prompt 末尾清单都用 */
  matchedSkills: Skill[];
  /** match context(给 user prompt build 用,避免重算) */
  context: SkillMatchContext;
  /** composeHash 给 promptVersion suffix 用,16 hex */
  composeHash: string;
}

/** lazy load base —— common(共性,三档一份)+ format(每档一份),按档缓存 */
let cachedCommon: string | null = null;
const cachedFormat: Partial<Record<ScriptTier, string>> = {};
function loadBase(tier: ScriptTier): string {
  if (cachedCommon === null) {
    cachedCommon = readFileSync(resolveBaseCommonPath(), 'utf-8').trim();
  }
  if (cachedFormat[tier] === undefined) {
    cachedFormat[tier] = readFileSync(resolveBaseFormatPath(tier), 'utf-8').trim();
  }
  // 顺序:共性定位/铁律 → 本档输出格式 → 禁词(单一源)
  return `${cachedCommon}\n\n${cachedFormat[tier]}\n\n${forbiddenWordsBlock()}`;
}

/**
 * 从 input 派生 match context — orchestrator 已在 reason 上直传 scenario raw key + subKey + dxCode,
 * composer 这里不做文本推断,纯字段映射。
 */
export function deriveContext(input: DraftPlanScriptInput): SkillMatchContext {
  const reasons = input.plan.reasons ?? [];
  const scenario = input.plan.primaryScenarioKey ?? null;
  const diagnosisCodes = Array.from(
    new Set(reasons.map((r) => r.dxCode).filter((c): c is string => !!c)),
  );
  const age = input.patient.age ?? null;
  return {
    scenario,
    diagnosisCodes,
    age,
    population: classifyPopulation(age),
    relationship: input.clinicalContext.completedTreatmentCount === 0 ? 'new' : 'returning',
  };
}

/** 一个 skill 是否匹配当前 context */
export function skillApplies(skill: Skill, ctx: SkillMatchContext): boolean {
  const a = skill.frontmatter.applies;
  if (a.scenario && a.scenario !== ctx.scenario) return false;
  if (a.diagnosisCodePrefix) {
    const hit = ctx.diagnosisCodes.some((c) =>
      c.startsWith(a.diagnosisCodePrefix!),
    );
    if (!hit) return false;
  }
  // 年龄未知(null):按业务规格"未知 → 默认成人模板"——通过 ageMin(成人/成人以上),
  //   但仍被 ageMax 排除(child 的 ageMax 命中 null → 不匹配),所以 null 只落成人。
  if (a.ageMin !== undefined && ctx.age !== null && ctx.age < a.ageMin) return false;
  if (a.ageMax !== undefined && (ctx.age === null || ctx.age > a.ageMax)) return false;
  if (a.relationship && a.relationship !== ctx.relationship) return false;
  // allowedPopulation 跨维度排除:非空数组时,当前 population 必须在列表内
  if (
    skill.frontmatter.allowedPopulation.length > 0 &&
    (ctx.population === null ||
      !skill.frontmatter.allowedPopulation.includes(ctx.population))
  ) {
    return false;
  }
  return true;
}

/** skill 是否适用该档:tiers 空 = 全档共用(如人群共性知识);非空需包含当前档 */
export function skillTierOk(skill: Skill, tier: ScriptTier): boolean {
  const t = skill.frontmatter.tiers;
  return t.length === 0 || t.includes(tier);
}

/**
 * Compose 主入口(tier-aware)。
 *   base = common(共性) + format(该档) + 禁词(单一源);
 *   skills = applies 命中 且 该档适用(tiers 过滤)。
 */
export function composeSystem(
  input: DraftPlanScriptInput,
  allSkills: readonly Skill[],
  tier: ScriptTier = 'stable',
): ComposedSystem {
  const context = deriveContext(input);
  const matched = allSkills
    .filter((s) => skillApplies(s, context) && skillTierOk(s, tier))
    .sort(
      (a, b) =>
        (a.frontmatter.priority ?? 50) - (b.frontmatter.priority ?? 50),
    );

  const base = loadBase(tier);
  // 只拼 body — 内部 skill name/version 不进提示词(版本归因走 composeHash,见下)
  const skillsBlock = matched.map((s) => s.body).join('\n\n---\n\n');

  const systemPrompt = skillsBlock
    ? `${base}\n\n# 本次适用知识 / 模板\n\n${skillsBlock}`
    : base;

  // composeHash = sha256(tier + matched.name+version join)前 16 hex
  const hashSrc = [tier, ...matched.map((s) => `${s.frontmatter.name}@${s.frontmatter.version}`)].join('|');
  const composeHash = createHash('sha256').update(hashSrc).digest('hex').slice(0, 16);

  return { systemPrompt, matchedSkills: matched, context, composeHash };
}
