import { VISIT_RECENCY_BUCKETS } from './visit-recency';

/**
 * 画像标签筛选字典 — 列表/选人侧"按画像圈人"的可筛维度(单一真理源)。
 *
 * 与 PERSONA_FEATURE_SPECS(标签卡全集)的关系:这里是**精选的可筛子集**,
 * 每项声明它在 persona_features.data 里的结构化取值位置(dataPath)+ code↔中文。
 * 未收录:acquisition_channel(取值随宿主,非稳定枚举)、discount_anchor(数值型非标签)。
 * code 必须与对应 feature extractor 写入 data 的值一致(rfm.feature.ts 等);
 * 改 extractor 枚举时同步改这里(CI 不强校验,靠 PR review)。
 *
 * 筛选语义(plan.service 实现):同一维度多选 = OR;跨维度 = AND;
 * 匹配的是患者**当前版**画像(personas.supersededAt IS NULL)。
 *
 * ⚠️ **加维度必须同步补库索引** —— 每个维度的 (key, dataPath) 在
 * `prisma/migrations/20260728020000_persona_features_tag_filter_indexes` 里各有一条偏索引
 * (数组维度走 GIN、标量维度走 btree 且第二列盖 persona_id)。漏建的维度会退化成
 * 「按 key 捞全量特征行 + 回堆过滤 jsonb」:生产实测禁忌维度 190,210 行里只中 2,156 行,
 * 读了 1.3GB 堆、单次筛人 20+ 秒(rfm 在测试服 57 秒)。这不是能忍的慢,是不可用。
 */
export interface PersonaTagOption {
  /** data 里的结构化取值(英文 code) */
  value: string;
  zh: string;
  /**
   * 一句话说明 —— 筛选面板上 hover 该选项时显示。
   *
   * 「重要保持」「待激活」「重要发展」这类分群名是算法口径的产物,光看名字猜不出边界
   * (重要保持 vs 重要挽留差在哪?待激活 vs 沉睡客几个月分界?),客服只能凭感觉勾。
   * 这里用大白话给出**判定条件**,不解释算法。省略 = 名字本身够自明(性别之类)。
   */
  hint?: string;
}

export interface PersonaTagFilterDim {
  /**
   * 筛选串(`id:value`)里用的**维度标识**,必须全局唯一。
   *
   * 不填 = 等于 key(绝大多数维度:一个特征只出一个可筛字段,两者同名最省事)。
   * 必须填的场景:**同一个 persona 特征出多个可筛字段** —— 如 visit_recency 同时提供
   * 上次时间(bucket)/ 上次医生(lastDoctor)/ 偏好医生(preferredDoctor),
   * 三者 key 相同、dataPath 不同,只靠 key 分不开。
   */
  id?: string;
  /** persona_features.key */
  key: string;
  nameZh: string;
  /** data 里取值的字段名(单层) */
  dataPath: string;
  /** true = data[dataPath] 是 string[](用 array_contains);false = 标量 equals */
  isArray?: boolean;
  /**
   * true = **前端筛选面板不展示**,但维度本身仍然有效。
   *
   * 刻意做成"隐藏"而不是"删除":
   *   · API / MCP 仍接受该维度 —— 已保存的筛选链接、机器人调用不会突然失效;
   *   · persona_features 照常写这些特征、偏索引(迁移 20260728020000)照常在,复活只需删这一行;
   *   · 业务口径反复过不止一次,删了重写的成本远高于留一个 flag。
   */
  hidden?: boolean;
  /**
   * true = 取值是**开集**(如医生姓名),不做闭集校验。
   *
   * 校验仍在:维度 id 必须已知,取值走参数化 equals(无注入面);只是不再要求
   * 取值出现在 options 里。options 此时留空,由前端从 /plans/doctors 之类的接口拉。
   * ⚠️ 开集维度**必须走等值**(不是模糊匹配)—— 等值才能吃到偏索引;
   *   模糊匹配会退化成全分区扫,就是 2026-07-29 那个「圈人 20 秒」。
   *   "模糊"体验交给前端:输入框在已加载的候选列表里做客户端匹配,选中后发精确值。
   */
  dynamic?: boolean;
  /**
   * 取值从哪张表来。默认 'persona'(persona_features.data[dataPath])。
   *
   * 'patient' = 直接查 patient_profiles 的列(经 patients.profile 关系) —— 用于**事实派生属性**(上次到诊 / 上次医生 / 偏好医生)。
   * 它们刻意**不做成画像特征**:「上次到诊 2022-06-19」是原始事实,而画像该是
   * 「重要价值 / 流失客 / 禁忌」这类判断,混在一起会污染画像标签抽屉和画像摘要 LLM
   * (2026-07-29 第一版就是那么做的,当天被否)。
   * 附带好处:普通列 + btree,不用穿 patients → personas → persona_features 两层 EXISTS + JSONB;
   * 卡片展示与筛选读**同一处**,不会出现「卡片显示 A、筛选按 B 匹配」。
   */
  source?: 'persona' | 'patient';
  /** source='patient' 时:patient_profiles 上的字段名(Prisma 字段名) */
  patientField?: 'lastVisitAt' | 'lastVisitDoctor' | 'preferredDoctor';
  options: PersonaTagOption[];
}

/// 维度的筛选标识(`id:value` 串里用的);未显式给 id 的维度回落到 key
export function personaTagDimId(dim: PersonaTagFilterDim): string {
  return dim.id ?? dim.key;
}

export const PERSONA_TAG_FILTER_DIMS: PersonaTagFilterDim[] = [
  // ══════════════ Tier1 · 治疗维度 —— 客服最先问的是「这人有什么可谈」 ══════════════
  {
    key: 'potential_treatment',
    nameZh: '潜在治疗',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'implant', zh: '种植', hint: '诊断缺牙,还没种' },
      { value: 'ortho', zh: '正畸', hint: '建议矫正,还没做(13-40 岁)' },
      { value: 'early_ortho', zh: '早矫', hint: '建议儿童早期矫治(3-12 岁)' },
      { value: 'endo', zh: '根管', hint: '诊断牙髓问题,还没做根管' },
      { value: 'perio', zh: '牙周', hint: '诊断牙周问题,还没治' },
      // ⚠️ 「充填」不是「补牙」—— 业务 2026-07-29 定的措辞,矩阵/卡片渲染走
      //   `potentialTreatmentItemName`(labels.ts)。这里曾漏改,于是助手嘴里是「补牙」、
      //   界面上是「充填」,主管看着像两件事。⛔ 改措辞两处必须一起改(有测试锁)。
      { value: 'filling', zh: '充填', hint: '诊断龋齿,还没补' },
      { value: 'restoration', zh: '修复', hint: '需要冠桥 / 贴面,还没做' },
      { value: 'extraction', zh: '拔牙', hint: '残根残冠等需拔,还没拔' },
    ],
  },
  {
    key: 'treatment_history',
    nameZh: '治疗史',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'implant_history', zh: '种植史', hint: '种过牙' },
      { value: 'ortho_history', zh: '正畸史', hint: '做过矫正' },
      { value: 'prostho_history', zh: '修复史', hint: '做过冠桥 / 贴面 / 嵌体' },
      { value: 'perio_history', zh: '牙周治疗史', hint: '做过洁牙 / 刮治 / 牙周序列治疗' },
    ],
  },

  // ══════════════ Tier2 · 行为维度 ══════════════
  {
    id: 'last_visit_bucket',
    key: '',
    source: 'patient',
    patientField: 'lastVisitAt',
    nameZh: '上次时间',
    dataPath: '',
    options: VISIT_RECENCY_BUCKETS.map((b) => ({ value: b.value, zh: b.zh, hint: b.hint })),
  },
  // 「上次医生 / 偏好医生」是**开集维度**(单宿主几百个医生),取值不做闭集校验,
  // options 留空 —— 前端从 GET /pac/v1/plans/doctors 拉本 scope 的医生名单,
  // 输入框做客户端模糊匹配,**选中后发精确姓名**(等值查询才吃得到偏索引,见 dynamic 注释)。
  // 注:preferredDoctor 现在是拿「主治医生」(病历里出现频次最高的医生)代替 ——
  //   DW fact_client_out.favor_doctor_id 才是真·偏好医生,PAC 尚未摄入,接入后换值即可,前端不用改。
  {
    id: 'visit_doctor_last',
    key: '',
    source: 'patient',
    patientField: 'lastVisitDoctor',
    nameZh: '上次医生',
    dataPath: '',
    dynamic: true,
    options: [],
  },
  {
    id: 'visit_doctor_preferred',
    key: '',
    source: 'patient',
    patientField: 'preferredDoctor',
    nameZh: '偏好医生',
    dataPath: '',
    dynamic: true,
    options: [],
  },

  // ══════════════ Tier3 · 属性维度 —— 定语气与话术分寸,不决定要不要打 ══════════════
  {
    key: 'rfm',
    nameZh: '价值分群',
    dataPath: 'segment',
    options: [
      { value: 'important_value', zh: '重要价值', hint: '消费高、来得勤,最近还在来' },
      { value: 'important_retain', zh: '重要保持', hint: '消费高、来得勤,但有段时间没来了' },
      { value: 'important_develop', zh: '重要发展', hint: '消费高、最近来过,但来的次数少' },
      { value: 'important_winback', zh: '重要挽留', hint: '消费高,但很久没来了' },
      { value: 'general_value', zh: '一般价值', hint: '来得勤、最近还在来,消费一般' },
      { value: 'general_retain', zh: '一般保持', hint: '来得勤但有段时间没来,消费一般' },
      { value: 'general_develop', zh: '一般发展', hint: '最近来过但次数少,消费一般' },
      { value: 'low_active', zh: '低活跃', hint: '很久没来,或三项都偏低' },
    ],
  },
  {
    key: 'urgency_level',
    nameZh: '紧迫度',
    dataPath: 'level',
    options: [
      { value: 'urgent', zh: '紧急', hint: '有待做的项目,且 90 天以上没来' },
      { value: 'high', zh: '高', hint: '有待做的项目,30 到 90 天没来' },
      { value: 'mid', zh: '中', hint: '有待做的项目,30 天内来过或刚发现' },
    ],
  },
  {
    key: 'age_bracket',
    nameZh: '年龄段',
    dataPath: 'bracket',
    options: [
      { value: 'infant', zh: '婴幼儿', hint: '0-2 岁' },
      { value: 'preschool', zh: '学龄前', hint: '3-6 岁' },
      { value: 'mixed_dentition', zh: '替牙期', hint: '7-11 岁,换牙阶段' },
      { value: 'adolescent', zh: '青少年', hint: '12-17 岁' },
      { value: 'youth', zh: '青年', hint: '18-25 岁' },
      { value: 'young_adult', zh: '中青年', hint: '26-30 岁' },
      { value: 'middle_aged', zh: '中年', hint: '31-45 岁' },
      { value: 'pre_senior', zh: '中老年', hint: '46-54 岁' },
      { value: 'senior', zh: '老年', hint: '55 岁以上' },
    ],
  },
  {
    key: 'entitlement_status',
    nameZh: '权益身份',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'high_insurance', zh: '高端保险直付', hint: '用过 Bupa、万欣和等商业保险直付' },
      { value: 'bank_vip', zh: '银行私行权益', hint: '用过私人银行 / 白金卡等银行权益' },
      { value: 'stored_value', zh: '储值会员', hint: '有储值或预存记录' },
      { value: 'pedo_member', zh: '儿牙会员', hint: '买过儿牙会员卡 / 涂氟年卡' },
      { value: 'medical', zh: '医保客户', hint: '用过医保结算' },
    ],
  },
  {
    /**
     * 获客渠道 —— ⭐ 2026-08-12 补进来，为的是**分配时的画像亮点 + 圈人**：
     *   「这一格里口碑客户是平时的 2.6 倍」这类结论要能算，就得先能取到它的分布。
     * ⚠️ `hidden`：前端筛选面板暂不展示（面板已经不短了），但工具可以点名用 ——
     *   hidden 的语义是「面板不展示」，⛔ 不是「不能用」。
     * ⚠️ 值域取自 persona-feature-specs 的 `acquisition_channel.labelValues`，
     *   ⛔ 别在这里另立一套中文：两份必然漂。
     */
    key: 'acquisition_channel',
    nameZh: '获客渠道',
    dataPath: 'channel',
    hidden: true,
    options: [
      { value: 'walk_in', zh: '走入', hint: '直接上门' },
      { value: 'word_of_mouth', zh: '口碑客户', hint: '老客户推荐来的' },
      { value: 'group_sales', zh: '集团销售渠道' },
      { value: 'group_marketing', zh: '集团营销渠道' },
      { value: 'regional_marketing', zh: '地区营销渠道' },
      { value: 'ecommerce', zh: '电商平台' },
      { value: 'social_media', zh: '自媒体网络' },
      { value: 'internal_staff', zh: '内部员工' },
      { value: 'other', zh: '其他' },
    ],
  },
  {
    key: 'referral_champion',
    nameZh: '转介绍达人',
    dataPath: 'type',
    options: [
      { value: 'family', zh: '家庭型', hint: '推荐 3 人以上并带来成交,带的是家里人' },
      { value: 'social', zh: '社交型', hint: '推荐 3 人以上并带来成交,带的是家人以外的人' },
    ],
  },
  {
    key: 'family_structure',
    nameZh: '家庭构成',
    dataPath: 'structure',
    options: [
      { value: 'multigen', zh: '多代之家', hint: '院内登记有长辈(父母 / 祖辈)' },
      { value: 'nuclear', zh: '多口之家', hint: '院内登记有子女' },
      { value: 'couple', zh: '两口之家', hint: '院内登记有配偶' },
      { value: 'single', zh: '单身家庭', hint: '只有旁系或朋友关系' },
    ],
  },
  {
    key: 'gender',
    nameZh: '性别',
    dataPath: 'gender',
    options: [
      { value: 'male', zh: '男' },
      { value: 'female', zh: '女' },
    ],
  },

  // ══════════════ 已隐藏(hidden: true)—— 面板不展示,维度仍有效 ══════════════
  {
    key: 'lifecycle_stage',
    hidden: true, // 业务 2026-07-29 下线:与召回决策关系不大,面板不展示(API/MCP 仍可用)
    nameZh: '生命周期',
    dataPath: 'stage',
    options: [
      { value: 'prospect', zh: '潜客', hint: '还没到过诊,只约过或咨询过' },
      { value: 'new', zh: '新客', hint: '首诊半年内,就诊不超过 3 次' },
      { value: 'growth', zh: '成长客', hint: '还在来,近一年消费明显上涨' },
      { value: 'mature', zh: '成熟客', hint: '还在来,消费平稳' },
      { value: 'reactivate', zh: '待激活', hint: '半年到一年半没来' },
      { value: 'dormant', zh: '沉睡客', hint: '一年半到两年没来' },
      { value: 'churned', zh: '流失客', hint: '两年以上没来' },
    ],
  },
  {
    key: 'treatment_sensitivity',
    hidden: true, // 业务 2026-07-29 下线:与召回决策关系不大,面板不展示(API/MCP 仍可用)
    nameZh: '治疗敏感',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'dental_fear', zh: '看牙恐惧', hint: '病历里提过害怕看牙' },
      { value: 'needle_faint', zh: '晕针', hint: '病历里提过晕针 / 怕打针' },
      { value: 'blood_faint', zh: '晕血', hint: '病历里提过晕血' },
      { value: 'claustrophobia', zh: '密闭恐惧', hint: '病历里提过幽闭 / 长时间张口不适' },
    ],
  },
  {
    key: 'special_attention',
    hidden: true, // 业务 2026-07-29 下线:与召回决策关系不大,面板不展示(API/MCP 仍可用)
    nameZh: '特殊关注',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'frequent_no_show', zh: '屡次爽约', hint: '近一年至少 3 次预约,履约不到一半' },
      { value: 'often_late', zh: '经常迟到', hint: '近一年多次晚到 15 分钟以上' },
      { value: 'do_not_disturb', zh: '免打扰', hint: '已标记不接受主动联系,不要外呼' },
      { value: 'cannot_wait', zh: '不可等候', hint: '病历或备注提过赶时间 / 不能等' },
    ],
  },
  {
    key: 'time_preference',
    hidden: true, // 业务 2026-07-29 下线:与召回决策关系不大,面板不展示(API/MCP 仍可用)
    nameZh: '时间偏好',
    dataPath: 'types',
    isArray: true,
    options: [
      { value: 'weekday', zh: '工作日', hint: '近两年预约多在工作日' },
      { value: 'weekend', zh: '周末', hint: '近两年预约多在周末' },
      { value: 'morning', zh: '上午', hint: '多约在 8-12 点' },
      { value: 'afternoon', zh: '下午', hint: '多约在 12-18 点' },
      { value: 'evening', zh: '晚间', hint: '多约在 18-21 点' },
    ],
  },
  {
    key: 'contraindication',
    hidden: true, // 业务 2026-07-29 下线:与召回决策关系不大,面板不展示(API/MCP 仍可用)
    nameZh: '禁忌',
    /**
     * 按【治疗域】筛,不按病因 —— data.domains 而非 data.types。
     *
     * 客服圈人时想的是「哪些人不能做种植」,不是「哪些人在吃抗骨吸收药」;
     * 病因(抗凝药/放疗史/双膦酸盐…)是算法口径,列成筛选项等于把内部实现摊给业务看。
     * 病因照常在关键事实卡和画像详情里露出(带医生原话),要查"所有放疗史患者"是另一个
     * 场景(风险排查),需要时再加一个独立维度,别混进这里。
     *
     * 不列「拍片禁忌」:唯一来源是妊娠,而妊娠同时禁种植和手术 —— 勾那两项就已经覆盖,
     * 单列只会让面板变长。data.domains 里仍有 xray,详情页照常显示。
     *
     * 核查项(糖尿病/高血压/骨质疏松/吸烟)不在这里 —— 它们落 data.checks 而非 data.domains,
     * 按定义就不是禁忌,放进来会让客服把「问一句」当成「不能治」。
     */
    dataPath: 'domains',
    isArray: true,
    options: [
      { value: 'surgery', zh: '手术禁忌', hint: '拔牙 / 翻瓣 / 植骨 —— 抗凝药、凝血障碍、抗骨吸收药、放疗史、妊娠期' },
      { value: 'implant', zh: '种植禁忌', hint: '未满 19 岁,或放疗史 / 抗骨吸收药 / 抗凝药 / 心血管手术史 / 妊娠哺乳' },
      { value: 'anesthesia', zh: '麻醉禁忌', hint: '局麻药物过敏;哮喘为相对禁忌' },
      { value: 'orthodontic', zh: '正畸禁忌', hint: '诊断为重度牙周炎 —— 牙周控制后可矫治,属相对禁忌' },
    ],
  },

];

/** "key:value" 串(query 上线格式)解析;非法项丢弃。 */
export function parsePersonaTags(raw: string): Array<{ key: string; value: string }> {
  return raw
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean)
    .map((s) => {
      const i = s.indexOf(':');
      if (i <= 0) return null;
      return { key: s.slice(0, i), value: s.slice(i + 1) };
    })
    .filter((x): x is { key: string; value: string } => x !== null);
}
