<script lang="ts" setup>
/**
 * 病历字段关键字气泡选择器
 * - 虚拟列表渲染（大数据量不卡顿）
 * - 诊断/医嘱非常用项可一键加常用
 * - 面板 Teleport + fixed，相对视口锚定，避免 Modal overflow 裁切/越界
 */
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';

import { LoadingOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn } from '@vueuse/core';
import { Button, Empty, Input, Modal, Spin, message } from 'ant-design-vue';

import {
  applyDictRankIfNeeded,
  ensureDictSearchRankMode,
  rankByMatchScore,
  splitHighlight,
  type HighlightPart,
} from '#/utils/dictSearchRank';
import {
  getDiseaseList,
  getDoctorOrderList,
  getTraditionalChineseMedicineAllApi,
} from '#/views/doctor/doctor-reception/api';
import {
  addDoctorMyDiseaseApi,
  createDoctorOrderApi,
  getDoctorMyDiseaseListApi,
} from '#/views/doctor/settings/api';
import { getSystemConfigByKeys } from '#/views/system/system-config/api';

import { createMedicalRecordEntry, searchMedicalRecordEntry, addMyMedicalRecordEntry } from '../api';
import { MEDICAL_RECORD_FIELD_OPTIONS } from '../config/constants';

import CommonDxOrderChips from './CommonDxOrderChips.vue';

export type EntryBubbleSource =
  | 'entry'
  | 'diagnosis'
  | 'doctor_order'
  | 'tcm_disease'
  | 'tcm_syndrome'
  | 'tcm_method';

export type MedicalRecordEntryItem = {
  id: number | string;
  title: string;
  content: string;
  remark: string;
  field_code?: string;
  store_id?: number;
  match_score?: number;
  /** 展示用：首拼(全拼) */
  pinyin_initials?: string;
  pinyin_full?: string;
  pinyin?: string;
  /** 是否已在常用（诊断/医嘱） */
  is_common?: boolean;
  /** 疾病库真实 id（加常用诊断用） */
  disease_id?: number;
};

const props = withDefaults(
  defineProps<{
    fieldCode?: string;
    source?: EntryBubbleSource;
    storeId?: number;
    placeholder?: string;
    disabled?: boolean;
    loadOnFocus?: boolean;
    /**
     * 诊断/医嘱：是否在气泡内展示「常用|我的」快捷标签
     *（由系统配置 mr_common_display_mode=bubble|both 控制）
     */
    showCommonInBubble?: boolean;
    /**
     * 贴在 label 右侧时用紧凑输入框（气泡仍按最小宽度展开，不跟随输入框变窄）
     */
    compact?: boolean;
  }>(),
  {
    fieldCode: '',
    source: 'entry',
    storeId: 0,
    placeholder: '词条关键字/首拼',
    disabled: false,
    loadOnFocus: true,
    showCommonInBubble: false,
    compact: false,
  },
);

const emit = defineEmits<{
  select: [item: MedicalRecordEntryItem];
  /** 新增/加常用后通知外层刷新标签区 */
  'common-changed': [];
}>();

/** 气泡内嵌常用标签刷新 */
const bubbleChipsTick = ref(0);
/**
 * 仅 bubble/both 时在气泡内展示常用标签；
 * tags 模式常用只走文本域下方标签，气泡内不展示
 */
const showBubbleChips = computed(
  () =>
    props.showCommonInBubble === true &&
    (props.source === 'diagnosis' ||
      props.source === 'doctor_order' ||
      (props.source === 'entry' && !!props.fieldCode)),
);

const ROW_HEIGHT = 52;
const HEAD_HEIGHT = 36;
const HINT_HEIGHT = 34;
const PANEL_PAD = 8;
const PREFERRED_PANEL = 360;

const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const inputRef = ref<{ focus?: () => void } | null>(null);
const keyword = ref('');
const loading = ref(false);
const open = ref(false);
const highlightIndex = ref(0);
const options = ref<MedicalRecordEntryItem[]>([]);
const addingId = ref<string | number | null>(null);
const scrollTop = ref(0);
const listViewportH = ref(240);
const panelStyle = ref<Record<string, string>>({});
/** 气泡内新增门店词条 */
const createOpen = ref(false);
const createLoading = ref(false);
const createForm = reactive({
  title: '',
  content: '',
  remark: '',
});
const showActionCol = computed(
  () =>
    props.source === 'diagnosis' ||
    props.source === 'doctor_order' ||
    (props.source === 'entry' && !!props.fieldCode),
);
/** 病历词条 / 我的医嘱 均可新增 */
const showCreateEntry = computed(
  () =>
    (props.source === 'entry' && !!props.fieldCode) ||
    props.source === 'doctor_order',
);
const createFieldLabel = computed(() => {
  if (props.source === 'doctor_order') return '我的医嘱';
  const hit = MEDICAL_RECORD_FIELD_OPTIONS.find((o) => o.value === props.fieldCode);
  return hit?.label || props.fieldCode;
});
const createBtnLabel = computed(() =>
  props.source === 'doctor_order' ? '新增医嘱' : '新增词条',
);
const createEmptyBtnLabel = computed(() =>
  props.source === 'doctor_order' ? '新增我的医嘱' : '新增门店词条',
);
const createModalTitle = computed(() =>
  props.source === 'doctor_order'
    ? '新增我的医嘱'
    : `新增门店词条 · ${createFieldLabel.value}`,
);

const emptyHintMap: Record<EntryBubbleSource, string> = {
  entry: '暂无词条！词条维护：字典管理-病历公共/门店词条',
  diagnosis: '暂无诊断！请到「常用诊断」或疾病库维护',
  doctor_order: '暂无医嘱！可点下方「新增我的医嘱」或到设置维护',
  tcm_disease: '暂无中医疾病！请到「字典中心-中医疾病」维护',
  tcm_syndrome: '暂无中医证候！请到「字典中心-中医证候」维护',
  tcm_method: '暂无中医治法！请到「字典中心-中医治法」维护',
};

/** 中医三典全量缓存（接诊接口一次拉齐） */
let tcmAllCache: {
  diseases?: any[];
  method?: any[];
  syndrome?: any[];
} | null = null;

/**
 * 统一拼出气泡「首拼(全拼)」展示串
 * 优先已格式化的 pinyin_display / pinyin_initials；勿单独用 abbr 的 pinyin 顶掉全拼
 */
function formatPyDisplayOf(item: Record<string, any> | null | undefined): string {
  if (!item) return '';
  const display = String(item.pinyin_display || '').trim();
  if (display) return display;
  const initials = String(item.pinyin_initials || '').trim();
  // 后端已写成 首拼(全拼) 时直接用
  if (initials.includes('(') && initials.endsWith(')')) return initials;
  const abbr = String(item.pinyin || initials || item.initials_of_pinyin || '').trim();
  const full = String(item.pinyin_full || '').trim();
  if (!abbr && !full) return '';
  if (!full || abbr.toLowerCase() === full.toLowerCase()) return abbr || full;
  if (!abbr) return `(${full})`;
  return `${abbr}(${full})`;
}

/** 匹配用拼音串：含首拼+全拼，避免只匹配到 abbr */
function pyMatchOf(item: Record<string, any> | null | undefined): string {
  if (!item) return '';
  return [
    item.pinyin_display,
    item.pinyin_initials,
    item.pinyin,
    item.pinyin_full,
    item.initials_of_pinyin,
  ]
    .map((v) => String(v || '').trim())
    .filter(Boolean)
    .join(' ');
}

const gridClass = computed(() => ({
  'has-score': !!keyword.value.trim(),
  'has-action': showActionCol.value,
}));

/** 虚拟列表：总高度 / 可见切片 / 偏移 */
const totalHeight = computed(() => options.value.length * ROW_HEIGHT);
const startIndex = computed(() =>
  Math.max(0, Math.floor(scrollTop.value / ROW_HEIGHT) - 2),
);
const endIndex = computed(() => {
  const visible = Math.ceil(listViewportH.value / ROW_HEIGHT) + 4;
  return Math.min(options.value.length - 1, startIndex.value + visible);
});
const visibleRows = computed(() => {
  const list = options.value;
  const start = startIndex.value;
  const end = endIndex.value;
  if (list.length === 0 || end < start) return [];
  return list.slice(start, end + 1).map((item, i) => ({
    item,
    index: start + i,
  }));
});
const offsetY = computed(() => startIndex.value * ROW_HEIGHT);

function matchTextOrPinyin(text: string, pinyin: string, kw: string): boolean {
  if (!kw) return true;
  const raw = String(text || '');
  const py = String(pinyin || '').toLowerCase();
  const k = kw.toLowerCase();
  if (raw.includes(kw) || raw.toLowerCase().includes(k)) return true;
  return !!py && py.includes(k);
}

/**
 * 匹配度模式：多气泡共用 ensureDictSearchRankMode，进页只打一次 get-by-keys
 */
async function loadRankModeConfig() {
  await ensureDictSearchRankMode(async () => {
    const res = await getSystemConfigByKeys(['dict_search_rank_mode']);
    return res?.dict_search_rank_mode ?? res?.data?.dict_search_rank_mode;
  });
}

/**
 * 面板 Teleport 到 body 并用 fixed 锚定输入框视口坐标
 * 解决模态框 overflow 裁切、以及相对定位导致的「超出可视区」
 */
function updatePanelPlacement() {
  const el = containerRef.value;
  if (!el) return;
  const rect = el.getBoundingClientRect();
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  const gap = 4;
  const spaceBelow = vh - rect.bottom - PANEL_PAD;
  const spaceAbove = rect.top - PANEL_PAD;
  const placeTop = spaceBelow < 200 && spaceAbove > spaceBelow;
  const avail = Math.max(160, placeTop ? spaceAbove : spaceBelow);
  const panelMax = Math.min(PREFERRED_PANEL, avail);
  listViewportH.value = Math.max(100, panelMax - HINT_HEIGHT - HEAD_HEIGHT);

  const PANEL_MIN_WIDTH = 560;
  // 在视口内尽量加宽，减少名称/词条正文被省略
  const PANEL_PREFERRED_WIDTH = Math.min(920, vw - PANEL_PAD * 2);
  const width = Math.min(
    Math.max(rect.width, PANEL_MIN_WIDTH, PANEL_PREFERRED_WIDTH),
    vw - PANEL_PAD * 2,
  );
  let left = rect.left;
  if (left + width > vw - PANEL_PAD) {
    left = vw - PANEL_PAD - width;
  }
  if (left < PANEL_PAD) left = PANEL_PAD;

  // z-index 高于 Vben/Ant Modal，避免被遮罩挡住
  const base: Record<string, string> = {
    position: 'fixed',
    zIndex: '4000',
    width: `${width}px`,
    maxHeight: `${panelMax}px`,
    left: `${left}px`,
    right: 'auto',
  };
  if (placeTop) {
    panelStyle.value = {
      ...base,
      top: 'auto',
      bottom: `${Math.max(PANEL_PAD, vh - rect.top + gap)}px`,
    };
  } else {
    panelStyle.value = {
      ...base,
      top: `${rect.bottom + gap}px`,
      bottom: 'auto',
    };
  }
}

async function fetchDiagnosis(kw: string): Promise<MedicalRecordEntryItem[]> {
  const [myList, diseaseList] = await Promise.all([
    getDoctorMyDiseaseListApi().catch(() => []),
    getDiseaseList(kw).catch(() => []),
  ]);
  const myDiseaseIds = new Set<number>();
  (myList || []).forEach((item: any) => {
    const d = item?.disease || item;
    const id = Number(d?.id ?? item?.disease_id ?? 0);
    if (id > 0) myDiseaseIds.add(id);
  });
  const seen = new Set<string>();
  const result: MedicalRecordEntryItem[] = [];
  const push = (
    id: number | string,
    name: string,
    remark: string,
    pinyin = '',
    matchScore?: number,
    isCommon = false,
    diseaseId = 0,
  ) => {
    const key = String(name || '').trim();
    if (!key || seen.has(key)) return;
    if (kw && !matchTextOrPinyin(key, pinyin, kw) && !(matchScore && matchScore > 0)) {
      return;
    }
    seen.add(key);
    result.push({
      id,
      title: key,
      content: key,
      remark,
      match_score: matchScore || 0,
      pinyin_initials: pinyin,
      is_common: isCommon,
      disease_id: diseaseId || Number(id) || 0,
    });
  };
  // tags 模式且无关键字：不在气泡里铺「默认常用」（常用已在下方标签）
  const showMyDefault = props.showCommonInBubble || !!kw;
  if (showMyDefault) {
    (myList || []).forEach((item: any) => {
      const d = item?.disease || item;
      const diseaseId = Number(d?.id ?? item?.disease_id ?? 0);
      push(
        d?.id ?? item?.id,
        d?.name || '',
        '常用',
        formatPyDisplayOf(d),
        0,
        true,
        diseaseId,
      );
    });
  }
  (diseaseList || []).forEach((item: any) => {
    const diseaseId = Number(item?.id || 0);
    const isCommon = diseaseId > 0 && myDiseaseIds.has(diseaseId);
    push(
      item?.id,
      item?.name || '',
      isCommon ? '常用' : '疾病库',
      formatPyDisplayOf(item),
      Number(item?.match_score || 0),
      isCommon,
      diseaseId,
    );
  });
  return rankByMatchScore(
    result,
    kw,
    (i) => i.content || i.title,
    (i) => i.pinyin_initials || '',
  );
}

async function fetchDoctorOrder(kw: string): Promise<MedicalRecordEntryItem[]> {
  const res = await getDoctorOrderList().catch(() => ({ my: [], common: [] }));
  const myContents = new Set<string>();
  (res?.my || []).forEach((item: any) => {
    const c = String(item?.content || '').trim();
    if (c) myContents.add(c);
  });
  const result: MedicalRecordEntryItem[] = [];
  const push = (
    id: number | string,
    text: string,
    remark: string,
    pinyin = '',
    isCommon = false,
  ) => {
    const key = String(text || '').trim();
    if (!key) return;
    if (kw && !matchTextOrPinyin(key, pinyin, kw)) return;
    result.push({
      id,
      title: key.length > 24 ? `${key.slice(0, 24)}…` : key,
      content: key,
      remark,
      pinyin_initials: pinyin,
      is_common: isCommon,
    });
  };
  // tags 模式且无关键字：气泡不铺「我的/公共」默认常用（已在下方标签展示）
  const showDefaultCommons = props.showCommonInBubble || !!kw;
  if (showDefaultCommons) {
    (res?.my || []).forEach((item: any) => {
      push(
        item?.id ?? `my_${item?.content}`,
        item?.content || '',
        '我的',
        formatPyDisplayOf(item),
        true,
      );
    });
    (res?.common || []).forEach((item: any) => {
      const text = item?.name || item?.content || '';
      const isCommon = myContents.has(String(text).trim());
      push(
        item?.id ?? `c_${item?.name}`,
        text,
        isCommon ? '我的' : '公共',
        formatPyDisplayOf(item),
        isCommon,
      );
    });
  }
  return applyDictRankIfNeeded(
    result,
    kw,
    (i) => i.content || i.title,
    (i) => i.pinyin_initials || '',
    'frontend',
  );
}

/**
 * 加载中医三典全量（内存缓存，避免每次按键打接口）
 */
async function loadTcmAll() {
  if (tcmAllCache) return tcmAllCache;
  const res = await getTraditionalChineseMedicineAllApi();
  const data = res?.data || res || {};
  tcmAllCache = {
    diseases: data.diseases || [],
    method: data.method || [],
    syndrome: data.syndrome || [],
  };
  return tcmAllCache;
}

/**
 * 中医疾病 / 证候 / 治法：本地过滤 + 匹配度排序
 * 与诊断一致用逗号拼接选用，故 content=名称
 */
async function fetchTcmDict(
  type: 'diseases' | 'syndrome' | 'method',
  kw: string,
  remark: string,
): Promise<MedicalRecordEntryItem[]> {
  const all = await loadTcmAll();
  const rows = all?.[type] || [];
  const result: MedicalRecordEntryItem[] = [];
  const k = kw.toLowerCase();
  for (const item of rows) {
    const name = String(item?.name || '').trim();
    if (!name) continue;
    const py = pyMatchOf(item);
    const alias = String(item?.alias || '');
    if (
      kw &&
      !matchTextOrPinyin(name, py, kw) &&
      !(alias && (alias.includes(kw) || alias.toLowerCase().includes(k)))
    ) {
      continue;
    }
    result.push({
      id: item?.id ?? name,
      title: name,
      content: name,
      remark,
      pinyin_initials: formatPyDisplayOf(item),
      pinyin_full: String(item?.pinyin_full || ''),
    });
  }
  // 无关键字时只展示前 80 条，避免一次铺几千行
  const sliced = kw ? result : result.slice(0, 80);
  return applyDictRankIfNeeded(
    sliced,
    kw,
    (i) => i.content || i.title,
    (i) => i.pinyin_initials || '',
    'frontend',
  );
}

async function fetchList(kw = '') {
  if (props.source === 'entry' && !props.fieldCode) {
    options.value = [];
    open.value = false;
    return;
  }
  loading.value = true;
  open.value = true;
  scrollTop.value = 0;
  updatePanelPlacement();
  try {
    let list: MedicalRecordEntryItem[] = [];
    if (props.source === 'diagnosis') {
      list = await fetchDiagnosis(kw.trim());
    } else if (props.source === 'doctor_order') {
      list = await fetchDoctorOrder(kw.trim());
    } else if (props.source === 'tcm_disease') {
      list = await fetchTcmDict('diseases', kw.trim(), '中医疾病');
    } else if (props.source === 'tcm_syndrome') {
      list = await fetchTcmDict('syndrome', kw.trim(), '中医证候');
    } else if (props.source === 'tcm_method') {
      list = await fetchTcmDict('method', kw.trim(), '中医治法');
    } else {
      const raw = await searchMedicalRecordEntry({
        field_code: props.fieldCode,
        keyword: kw.trim() || undefined,
        store_id: props.storeId || undefined,
      });
      const rows = (raw || []).map((item: any) => ({
        id: Number(item.id),
        title: item.title || '',
        content: item.content || item.title || '',
        remark: item.remark || '',
        field_code: item.field_code,
        store_id: item.store_id,
        match_score: Number(item.match_score || 0),
        pinyin_initials: formatPyDisplayOf(item),
        pinyin_full: String(item.pinyin_full || item.pinyin || ''),
      }));
      const modeFromApi = raw?.[0]?.rank_mode;
      list = applyDictRankIfNeeded(
        rows,
        kw.trim(),
        (i) => `${i.title} ${i.content}`,
        (i) => i.pinyin_initials || '',
        modeFromApi,
      );
    }
    options.value = list;
    highlightIndex.value = options.value.length > 0 ? 0 : -1;
    await nextTick();
    updatePanelPlacement();
    scrollHighlightIntoView();
  } catch {
    options.value = [];
    highlightIndex.value = -1;
  } finally {
    loading.value = false;
  }
}

function partsOf(text: string): HighlightPart[] {
  return splitHighlight(text, keyword.value);
}

/** 字典搜索防抖：略加长，减少拼音连敲时的频繁请求 */
const debouncedFetch = useDebounceFn(() => fetchList(keyword.value), 500);

function handleInput() {
  debouncedFetch();
}

function handleFocus() {
  if (props.disabled) return;
  if (props.loadOnFocus || keyword.value.trim() || options.value.length > 0) {
    fetchList(keyword.value);
  }
}

function selectItem(item?: MedicalRecordEntryItem) {
  const target =
    item ||
    (highlightIndex.value >= 0 ? options.value[highlightIndex.value] : undefined);
  if (!target) return;
  emit('select', target);
  keyword.value = '';
  open.value = false;
  highlightIndex.value = -1;
  options.value = [];
}

/**
 * 非常用诊断/医嘱加入常用（不关闭气泡，仅更新当前行状态）
 */
async function handleAddCommon(item: MedicalRecordEntryItem, e?: Event) {
  e?.preventDefault();
  e?.stopPropagation();
  if (item.is_common || addingId.value != null) return;
  addingId.value = item.id;
  try {
    if (props.source === 'diagnosis') {
      const diseaseId = Number(item.disease_id || item.id);
      if (!diseaseId) {
        message.warning('无法识别疾病ID');
        return;
      }
      await addDoctorMyDiseaseApi(diseaseId);
      item.is_common = true;
      item.remark = '常用';
      message.success('已加入常用诊断');
      bubbleChipsTick.value += 1;
      emit('common-changed');
    } else if (props.source === 'doctor_order') {
      await createDoctorOrderApi(item.content);
      item.is_common = true;
      item.remark = '我的';
      message.success('已加入常用医嘱');
      bubbleChipsTick.value += 1;
      emit('common-changed');
    } else if (props.source === 'entry' || (!props.source && props.fieldCode)) {
      const entryId = Number(item.id);
      if (!entryId) {
        message.warning('无法识别词条ID');
        return;
      }
      await addMyMedicalRecordEntry(entryId);
      item.is_common = true;
      message.success('已加入常用词条');
      bubbleChipsTick.value += 1;
      emit('common-changed');
    }
  } catch (err: any) {
    message.error(err?.message || '添加失败');
  } finally {
    addingId.value = null;
  }
}

function onListScroll(e: Event) {
  scrollTop.value = (e.target as HTMLElement).scrollTop;
}

function moveHighlight(delta: number) {
  if (options.value.length === 0) return;
  const len = options.value.length;
  let next = highlightIndex.value;
  if (next < 0) next = 0;
  else next = (next + delta + len) % len;
  highlightIndex.value = next;
  scrollHighlightIntoView();
}

/** 键盘高亮时滚动虚拟列表视口 */
function scrollHighlightIntoView() {
  const listEl = listRef.value;
  if (!listEl || highlightIndex.value < 0) return;
  const idx = highlightIndex.value;
  const top = idx * ROW_HEIGHT;
  const bottom = top + ROW_HEIGHT;
  const viewTop = listEl.scrollTop;
  const viewBottom = viewTop + listViewportH.value;
  if (top < viewTop) {
    listEl.scrollTop = top;
    scrollTop.value = top;
  } else if (bottom > viewBottom) {
    const next = bottom - listViewportH.value;
    listEl.scrollTop = next;
    scrollTop.value = next;
  }
}

function handleKeydown(e: KeyboardEvent) {
  if (!open.value) {
    if (e.key === 'ArrowDown' || e.key === 'Enter') {
      e.preventDefault();
      fetchList(keyword.value);
    }
    return;
  }
  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault();
      moveHighlight(1);
      break;
    case 'ArrowUp':
      e.preventDefault();
      moveHighlight(-1);
      break;
    case 'ArrowRight':
      e.preventDefault();
      moveHighlight(1);
      break;
    case 'ArrowLeft':
      e.preventDefault();
      moveHighlight(-1);
      break;
    case 'Enter':
      // Ctrl+Enter 留给病历面板做模块焦点切换，此处不拦截
      if (e.ctrlKey) return;
      e.preventDefault();
      selectItem();
      break;
    case 'Escape':
      e.preventDefault();
      open.value = false;
      highlightIndex.value = -1;
      break;
  }
}

/**
 * 打开新增弹窗：病历词条→门店词条；医嘱→我的常用医嘱
 */
function openCreateEntry(e?: Event) {
  e?.preventDefault();
  e?.stopPropagation();
  if (props.source === 'doctor_order') {
    const seed = keyword.value.trim();
    createForm.title = seed;
    createForm.content = seed;
    createForm.remark = '';
    createOpen.value = true;
    return;
  }
  if (!props.fieldCode) {
    message.warning('缺少字段编码');
    return;
  }
  if (!props.storeId || props.storeId <= 0) {
    message.warning('缺少门店信息，无法新增门店词条');
    return;
  }
  const seed = keyword.value.trim();
  createForm.title = seed;
  createForm.content = seed;
  createForm.remark = '';
  createOpen.value = true;
}

/**
 * 提交新增：门店词条 或 我的医嘱（createDoctorOrder）
 */
async function submitCreateEntry() {
  if (props.source === 'doctor_order') {
    const content = (createForm.content.trim() || createForm.title.trim());
    if (!content) {
      message.warning('请填写医嘱内容');
      return Promise.reject(new Error('empty content'));
    }
    createLoading.value = true;
    try {
      await createDoctorOrderApi(content);
      message.success('已加入我的医嘱');
      createOpen.value = false;
      bubbleChipsTick.value += 1;
      emit('common-changed');
      await fetchList(keyword.value);
    } catch (err: any) {
      message.error(err?.message || '新增失败');
      return Promise.reject(err);
    } finally {
      createLoading.value = false;
    }
    return;
  }
  const title = createForm.title.trim();
  if (!title) {
    message.warning('请填写词条名称');
    return Promise.reject(new Error('empty title'));
  }
  if (!props.storeId || props.storeId <= 0) {
    message.warning('缺少门店信息');
    return Promise.reject(new Error('no store'));
  }
  createLoading.value = true;
  try {
    await createMedicalRecordEntry({
      scope: 'store',
      store_id: props.storeId,
      field_code: props.fieldCode,
      title,
      content: createForm.content.trim() || title,
      remark: createForm.remark.trim(),
      status: 1,
      sort: 0,
    });
    message.success('门店词条已新增');
    createOpen.value = false;
    await fetchList(keyword.value);
  } catch (err: any) {
    message.error(err?.message || '新增失败');
    return Promise.reject(err);
  } finally {
    createLoading.value = false;
  }
}

function handleClickOutside(e: MouseEvent) {
  if (!open.value || createOpen.value) return;
  const t = e.target as Node;
  // 面板已 Teleport 到 body，需同时判断输入容器与面板
  if (containerRef.value?.contains(t) || panelRef.value?.contains(t)) return;
  open.value = false;
}

function onWinChange() {
  if (open.value) updatePanelPlacement();
}

watch(
  () => [props.fieldCode, props.source, props.storeId] as const,
  () => {
    options.value = [];
    open.value = false;
  },
);

watch(open, async (v) => {
  if (v) {
    await nextTick();
    updatePanelPlacement();
  }
});

onMounted(() => {
  document.addEventListener('click', handleClickOutside);
  window.addEventListener('resize', onWinChange);
  window.addEventListener('scroll', onWinChange, true);
  loadRankModeConfig();
});

onUnmounted(() => {
  document.removeEventListener('click', handleClickOutside);
  window.removeEventListener('resize', onWinChange);
  window.removeEventListener('scroll', onWinChange, true);
});

/**
 * 供病历面板 Ctrl+Enter 焦点切换：聚焦搜索框 / 关闭气泡
 */
defineExpose({
  focus() {
    open.value = false;
    nextTick(() => {
      const el = inputRef.value as any;
      el?.focus?.();
      const native = containerRef.value?.querySelector(
        'input',
      ) as HTMLInputElement | null;
      native?.focus();
    });
  },
  close() {
    open.value = false;
  },
});
</script>
<template>
  <div
    ref="containerRef"
    class="entry-keyword-bubble"
    :class="{ 'entry-keyword-bubble--compact': compact }"
  >
    <Input
      ref="inputRef"
      v-model:value="keyword"
      :placeholder="placeholder"
      :disabled="disabled"
      allow-clear
      size="small"
      @input="handleInput"
      @focus="handleFocus"
      @keydown="handleKeydown"
    >
      <template #prefix>
        <LoadingOutlined v-if="loading" class="text-muted-foreground" />
        <SearchOutlined v-else class="text-muted-foreground" />
      </template>
    </Input>
    <Teleport to="body">
      <div
        v-if="open"
        ref="panelRef"
        class="entry-keyword-bubble__panel"
        :style="panelStyle"
      >
        <div class="entry-keyword-bubble__hint">
          <span>
            ↑↓←→ 切换 · Enter 选用 · Esc 关闭
            <template v-if="keyword.trim()"> · 按匹配度排序</template>
          </span>
          <span class="entry-keyword-bubble__hint-right">
            <Button
              v-if="showCreateEntry"
              type="link"
              size="small"
              class="entry-keyword-bubble__create-btn"
              @mousedown.stop.prevent="openCreateEntry"
            >
              <template #icon><PlusOutlined /></template>
              {{ createBtnLabel }}
            </Button>
            <span class="entry-keyword-bubble__count">
              <template v-if="keyword.trim()">搜到 {{ options.length }} 条</template>
              <template v-else>共 {{ options.length }} 条</template>
            </span>
          </span>
        </div>
        <Spin :spinning="loading">
          <CommonDxOrderChips
            v-if="showBubbleChips"
            :key="`bubble_chips_${source}_${fieldCode}_${bubbleChipsTick}`"
            class="entry-keyword-bubble__chips"
            :source="
              source === 'diagnosis'
                ? 'diagnosis'
                : source === 'doctor_order'
                  ? 'doctor_order'
                  : 'entry'
            "
            :field-code="fieldCode"
            :disabled="disabled"
            @select="
              (text) =>
                selectItem({
                  id: text,
                  title: text,
                  content: text,
                  remark: '常用',
                  is_common: true,
                })
            "
          />
          <Empty
            v-if="!loading && options.length === 0"
            :image="Empty.PRESENTED_IMAGE_SIMPLE"
            :description="emptyHintMap[source]"
            class="py-4"
          >
            <Button
              v-if="showCreateEntry"
              type="primary"
              size="small"
              @mousedown.stop.prevent="openCreateEntry"
            >
              {{ createEmptyBtnLabel }}
            </Button>
          </Empty>
          <template v-else>
            <div class="entry-keyword-bubble__head" :class="gridClass">
              <span class="col-title">名称</span>
              <span class="col-py">首拼(全拼)</span>
              <span class="col-content">内容</span>
              <span class="col-remark">{{ source === 'entry' ? '临床意义' : '来源' }}</span>
              <span v-if="keyword.trim()" class="col-score">匹配度</span>
              <span v-if="showActionCol" class="col-action">操作</span>
            </div>
            <div
              ref="listRef"
              class="entry-keyword-bubble__list"
              :style="{ height: `${listViewportH}px` }"
              @scroll="onListScroll"
            >
              <div
                class="entry-keyword-bubble__phantom"
                :style="{ height: `${totalHeight}px` }"
              />
              <div
                class="entry-keyword-bubble__virtual"
                :style="{ transform: `translateY(${offsetY}px)` }"
              >
                <div
                  v-for="{ item, index } in visibleRows"
                  :key="`${item.id}_${index}`"
                  class="entry-keyword-bubble__row"
                  :class="[{ 'is-active': index === highlightIndex }, gridClass]"
                  :style="{ height: `${ROW_HEIGHT}px` }"
                  @mousedown.prevent="selectItem(item)"
                  @mouseenter="highlightIndex = index"
                >
                  <span class="col-title" :title="item.title">
                    <template v-for="(p, pi) in partsOf(item.title)" :key="`t_${pi}`">
                      <em v-if="p.hit" class="hit">{{ p.text }}</em>
                      <template v-else>{{ p.text }}</template>
                    </template>
                  </span>
                  <span
                    class="col-py"
                    :title="item.pinyin_initials || item.pinyin_full || item.pinyin || ''"
                  >
                    <template v-if="!(item.pinyin_initials || '').trim()">-</template>
                    <template v-else>
                      <template
                        v-for="(p, pi) in partsOf(item.pinyin_initials || '')"
                        :key="`py_${pi}`"
                      >
                        <em v-if="p.hit" class="hit">{{ p.text }}</em>
                        <template v-else>{{ p.text }}</template>
                      </template>
                    </template>
                  </span>
                  <span class="col-content" :title="item.content">
                    <template
                      v-for="(p, pi) in partsOf(item.content)"
                      :key="`c_${pi}`"
                    >
                      <em v-if="p.hit" class="hit">{{ p.text }}</em>
                      <template v-else>{{ p.text }}</template>
                    </template>
                  </span>
                  <span class="col-remark" :title="item.remark">
                    {{ item.remark || '-' }}
                  </span>
                  <span v-if="keyword.trim()" class="col-score">
                    {{ Number(item.match_score || 0) }}%
                  </span>
                  <span
                    v-if="showActionCol"
                    class="col-action"
                    @mousedown.stop.prevent
                  >
                    <Button
                      v-if="!item.is_common"
                      type="link"
                      size="small"
                      :loading="addingId === item.id"
                      @click.stop="handleAddCommon(item, $event)"
                    >
                      <template #icon><PlusOutlined /></template>
                      加常用
                    </Button>
                    <span v-else class="is-common-tag">已常用</span>
                  </span>
                </div>
              </div>
            </div>
          </template>
        </Spin>
      </div>
    </Teleport>
    <Modal
      v-model:open="createOpen"
      :title="createModalTitle"
      :confirm-loading="createLoading"
      ok-text="保存"
      cancel-text="取消"
      destroy-on-close
      :z-index="5000"
      @ok="submitCreateEntry"
    >
      <div class="entry-create-form">
        <template v-if="source === 'doctor_order'">
          <div class="entry-create-form__tip">
            将保存到「我的医嘱」，与设置页新增常用医嘱相同
          </div>
          <div class="entry-create-form__item">
            <div class="entry-create-form__label">医嘱内容</div>
            <Input.TextArea
              v-model:value="createForm.content"
              :rows="4"
              placeholder="请输入医嘱内容（必填）"
            />
          </div>
        </template>
        <template v-else>
          <div class="entry-create-form__tip">
            将保存为当前门店词条（store_id={{ storeId }}），仅本店可用
          </div>
          <div class="entry-create-form__item">
            <div class="entry-create-form__label">名称</div>
            <Input v-model:value="createForm.title" placeholder="词条名称（必填）" />
          </div>
          <div class="entry-create-form__item">
            <div class="entry-create-form__label">正文</div>
            <Input.TextArea
              v-model:value="createForm.content"
              :rows="3"
              placeholder="选中后写入文本区的内容，默认与名称相同"
            />
          </div>
          <div class="entry-create-form__item">
            <div class="entry-create-form__label">临床意义</div>
            <Input v-model:value="createForm.remark" placeholder="可选" />
          </div>
        </template>
      </div>
    </Modal>
  </div>
</template>
<style scoped>
.entry-keyword-bubble {
  position: relative;
  width: 100%;
}
.entry-keyword-bubble--compact {
  width: 220px;
  max-width: 100%;
  flex: 1 1 180px;
}
.entry-keyword-bubble__panel {
  /* position/尺寸由 updatePanelPlacement 以 fixed 写入，避免被 Modal overflow 裁切 */
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border-radius: 8px;
  border: 1px solid hsl(var(--border));
  background: hsl(var(--popover, var(--background)));
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.entry-keyword-bubble__chips {
  flex-shrink: 0;
  padding: 6px 10px 2px;
  border-bottom: 1px solid hsl(var(--border));
  max-height: 88px;
  overflow: auto;
}
.entry-keyword-bubble__hint {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 12px;
  font-size: 12px;
  color: hsl(var(--muted-foreground));
  border-bottom: 1px solid hsl(var(--border));
  background: hsl(var(--muted) / 0.35);
}
.entry-keyword-bubble__hint-right {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}
.entry-keyword-bubble__create-btn {
  padding-inline: 4px;
  height: auto;
}
.entry-keyword-bubble__count {
  flex-shrink: 0;
  font-weight: 600;
  color: hsl(var(--foreground));
}
.entry-create-form__tip {
  margin-bottom: 12px;
  font-size: 12px;
  color: hsl(var(--muted-foreground));
}
.entry-create-form__item {
  margin-bottom: 12px;
}
.entry-create-form__label {
  margin-bottom: 4px;
  font-size: 13px;
  font-weight: 500;
}
.entry-keyword-bubble__list {
  position: relative;
  overflow-y: auto;
  overflow-x: hidden;
}
.entry-keyword-bubble__phantom {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: -1;
  pointer-events: none;
}
.entry-keyword-bubble__virtual {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
}
.entry-keyword-bubble__head,
.entry-keyword-bubble__row {
  display: grid;
  /* 名称/拼音/正文加宽，备注收窄，减少词条正文被裁切 */
  grid-template-columns: minmax(130px, 1.1fr) minmax(150px, 1.2fr) minmax(200px, 2.2fr) minmax(88px, 0.9fr);
  gap: 8px;
  padding: 6px 12px;
  font-size: 13px;
  align-items: center;
  box-sizing: border-box;
}
.entry-keyword-bubble__head.has-score,
.entry-keyword-bubble__row.has-score {
  grid-template-columns: minmax(120px, 1fr) minmax(140px, 1.1fr) minmax(180px, 2fr) minmax(72px, 0.8fr) 52px;
}
.entry-keyword-bubble__head.has-action,
.entry-keyword-bubble__row.has-action {
  grid-template-columns: minmax(120px, 1fr) minmax(140px, 1.1fr) minmax(180px, 2fr) minmax(72px, 0.8fr) 76px;
}
.entry-keyword-bubble__head.has-score.has-action,
.entry-keyword-bubble__row.has-score.has-action {
  grid-template-columns: minmax(110px, 0.95fr) minmax(130px, 1fr) minmax(160px, 1.9fr) minmax(64px, 0.7fr) 48px 72px;
}
.entry-keyword-bubble__head {
  flex-shrink: 0;
  font-weight: 600;
  color: hsl(var(--muted-foreground));
  background: hsl(var(--popover, var(--background)));
  border-bottom: 1px solid hsl(var(--border));
}
.entry-keyword-bubble__row {
  cursor: pointer;
  border-bottom: 1px solid hsl(var(--border) / 0.6);
}
.entry-keyword-bubble__row:hover,
.entry-keyword-bubble__row.is-active {
  background: hsl(var(--accent));
  color: hsl(var(--accent-foreground));
}
.col-title,
.col-remark,
.col-py {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.col-title {
  font-weight: 500;
}
/* 词条正文：最多两行，配合加宽面板尽量多露字 */
.col-content {
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  white-space: normal;
  word-break: break-word;
  line-height: 1.35;
}
.col-py {
  font-size: 12px;
  color: hsl(var(--muted-foreground));
  text-transform: lowercase;
}
.col-remark {
  color: hsl(var(--muted-foreground));
}
.col-score {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  color: #d4380d;
  white-space: nowrap;
}
.col-action {
  text-align: right;
  white-space: nowrap;
}
.is-common-tag {
  font-size: 12px;
  color: hsl(var(--muted-foreground));
}
.entry-keyword-bubble__row.is-active .col-remark,
.entry-keyword-bubble__row.is-active .col-py,
.entry-keyword-bubble__row.is-active .is-common-tag {
  color: inherit;
  opacity: 0.85;
}
.entry-keyword-bubble__row.is-active .col-score {
  color: inherit;
}
.hit {
  font-style: normal;
  color: #cf1322;
  background: rgba(255, 214, 102, 0.55);
  border-radius: 2px;
  padding: 0 1px;
}
</style>
