<script setup lang="ts">
/**
 * 门店多选搜索（对齐开方关键字气泡交互）
 * - Teleport + fixed 锚定，避免搜索栏 overflow 裁切导致「气泡不见」
 * - 多选：点选后不关闭面板，已选项从列表剔除并以 Tag 展示
 * - Enter / ↑↓ 键盘选用，Esc 关闭
 */
import {
  computed,
  nextTick,
  onMounted,
  onUnmounted,
  ref,
  watch,
} from 'vue';

import { useUserStore } from '@vben/stores';

import { LoadingOutlined, SearchOutlined } from '@ant-design/icons-vue';
import { useDebounceFn, useVModel } from '@vueuse/core';
import { Empty, Input, Spin, Tag } from 'ant-design-vue';

import { searchStoreOption } from '#/views/system/store/api';

defineOptions({
  name: 'StoreMultiSearch',
  inheritAttrs: false,
});

export type StoreSearchItem = {
  id: number;
  name: string;
  type?: number;
  shouzimu?: string;
};

const props = withDefaults(
  defineProps<{
    value?: number[];
    /** 0诊所 1药店；不传则诊所+药店 */
    storeType?: 0 | 1;
    placeholder?: string;
    disabled?: boolean;
    initialItems?: StoreSearchItem[];
  }>(),
  {
    value: () => [],
    storeType: undefined,
    placeholder: '输入名称或拼音首拼搜索',
    disabled: false,
    initialItems: () => [],
  },
);

/**
 * 模块级搜索结果缓存（所有实例共享）：key = `${userId}|${storeType}|${keyword}`
 * 门店数据变更低频，TTL 内相同关键词直接命中缓存零等待，避免每敲一个字都等接口转圈。
 * key 必须拼登录用户 id：不同账号数据权限不同（店管只能搜到本店可见门店），
 * 切换账号后不允许在 TTL 内命中上一账号的结果，拼 id 后 key 天然隔离
 */
const searchResultCache = new Map<
  string,
  { at: number; items: StoreSearchItem[] }
>();
const SEARCH_CACHE_TTL = 5 * 60 * 1000;
const SEARCH_CACHE_MAX = 200;

function readSearchCache(key: string): null | StoreSearchItem[] {
  const hit = searchResultCache.get(key);
  if (!hit) return null;
  if (Date.now() - hit.at > SEARCH_CACHE_TTL) {
    searchResultCache.delete(key);
    return null;
  }
  return hit.items;
}

function writeSearchCache(key: string, items: StoreSearchItem[]) {
  // 简单容量控制：超限整体清空（低频触发，无需 LRU）
  if (searchResultCache.size >= SEARCH_CACHE_MAX) {
    searchResultCache.clear();
  }
  searchResultCache.set(key, { at: Date.now(), items });
}

const emits = defineEmits<{
  'update:value': [value: number[]];
}>();

const userStore = useUserStore();

/** 当前登录用户 id（缓存隔离维度；后端返回的 admin 行主键是 id，兜底 vben 的 userId） */
function currentUserId(): number | string {
  const info = userStore.userInfo as null | Record<string, any>;
  return info?.id ?? info?.userId ?? 0;
}

const mValue = useVModel(props, 'value', emits, {
  passive: true,
  defaultValue: [],
});

const containerRef = ref<HTMLElement | null>(null);
const panelRef = ref<HTMLElement | null>(null);
const listRef = ref<HTMLElement | null>(null);
const searchKeyword = ref('');
const loading = ref(false);
const showDropdown = ref(false);
const highlightIndex = ref(-1);
const options = ref<StoreSearchItem[]>([]);
const selectedMap = ref<Record<number, StoreSearchItem>>({});
const panelStyle = ref<Record<string, string>>({});

const selectedIds = computed(() =>
  Array.isArray(mValue.value) ? mValue.value.map(Number) : [],
);

const selectedItems = computed(() =>
  selectedIds.value.map((id) => {
    const cached = selectedMap.value[id];
    return cached || { id, name: `门店${id}` };
  }),
);

/** 下拉中过滤掉已选，方便连续多选 */
const visibleOptions = computed(() =>
  options.value.filter((item) => !selectedIds.value.includes(item.id)),
);

function mergeInitialItems(items: StoreSearchItem[]) {
  if (!items?.length) return;
  const next = { ...selectedMap.value };
  for (const item of items) {
    if (item?.id) next[item.id] = item;
  }
  selectedMap.value = next;
}

/**
 * 相对输入框视口坐标放置面板（与 EntryKeywordBubble 同思路）
 */
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 pad = 8;
  const spaceBelow = vh - rect.bottom - pad;
  const spaceAbove = rect.top - pad;
  const placeTop = spaceBelow < 200 && spaceAbove > spaceBelow;
  const avail = Math.max(160, placeTop ? spaceAbove : spaceBelow);
  const maxH = Math.min(320, avail);
  const width = Math.min(Math.max(rect.width, 360), vw - pad * 2);
  let left = rect.left;
  if (left + width > vw - pad) left = vw - pad - width;
  if (left < pad) left = pad;

  const base: Record<string, string> = {
    position: 'fixed',
    zIndex: '4000',
    width: `${width}px`,
    maxHeight: `${maxH}px`,
    left: `${left}px`,
    right: 'auto',
  };
  panelStyle.value = placeTop
    ? {
        ...base,
        top: 'auto',
        bottom: `${Math.max(pad, vh - rect.top + gap)}px`,
      }
    : {
        ...base,
        top: `${rect.bottom + gap}px`,
        bottom: 'auto',
      };
}

async function openPanel() {
  showDropdown.value = true;
  await nextTick();
  updatePanelPlacement();
}

/** 请求序号：只采纳最后一次请求的响应，避免快速输入时慢响应覆盖新结果 */
let fetchSeq = 0;

async function fetchOptions() {
  const keyword = searchKeyword.value.trim();
  if (!keyword) {
    // 递增序号让在途请求作废，避免清空后慢响应又把旧结果写回来
    fetchSeq += 1;
    loading.value = false;
    options.value = [];
    showDropdown.value = false;
    highlightIndex.value = -1;
    return;
  }

  const cacheKey = `${currentUserId()}|${props.storeType ?? 'all'}|${keyword}`;
  const cachedItems = readSearchCache(cacheKey);
  if (cachedItems) {
    // 命中缓存：直接渲染，不发请求不转 loading（体感「秒出」）
    fetchSeq += 1;
    loading.value = false;
    options.value = cachedItems;
    await openPanel();
    highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1;
    await nextTick();
    updatePanelPlacement();
    return;
  }

  const seq = (fetchSeq += 1);
  loading.value = true;
  await openPanel();
  try {
    const res = await searchStoreOption({
      keyword,
      ...(props.storeType === 0 || props.storeType === 1
        ? { type: props.storeType }
        : {}),
      limit: 20,
    });
    if (seq !== fetchSeq) return;
    const items = res?.items ?? [];
    writeSearchCache(cacheKey, items);
    options.value = items;
    highlightIndex.value = visibleOptions.value.length > 0 ? 0 : -1;
    await nextTick();
    updatePanelPlacement();
  } catch {
    if (seq !== fetchSeq) return;
    options.value = [];
    highlightIndex.value = -1;
  } finally {
    if (seq === fetchSeq) {
      loading.value = false;
    }
  }
}

const debouncedFetch = useDebounceFn(fetchOptions, 300);

function handleInput() {
  debouncedFetch();
}

function handleFocus() {
  if (searchKeyword.value.trim() && options.value.length > 0) {
    openPanel();
    if (highlightIndex.value < 0 && visibleOptions.value.length > 0) {
      highlightIndex.value = 0;
    }
  }
}

/**
 * 多选：追加 ID，保留关键词与面板，不关闭气泡
 */
function selectOption(item: StoreSearchItem) {
  if (selectedIds.value.includes(item.id)) return;
  selectedMap.value = { ...selectedMap.value, [item.id]: item };
  mValue.value = [...selectedIds.value, item.id];
  // 高亮下一项（列表已剔除刚选的）
  nextTick(() => {
    if (visibleOptions.value.length === 0) {
      highlightIndex.value = -1;
      return;
    }
    highlightIndex.value = Math.min(
      Math.max(highlightIndex.value, 0),
      visibleOptions.value.length - 1,
    );
    updatePanelPlacement();
    scrollHighlightIntoView();
  });
}

function removeSelected(id: number) {
  mValue.value = selectedIds.value.filter((x) => x !== id);
  const next = { ...selectedMap.value };
  delete next[id];
  selectedMap.value = next;
}

function closePanel() {
  showDropdown.value = false;
  highlightIndex.value = -1;
}

/** 仅改 list 的 scrollTop，表头固定不动 */
function scrollHighlightIntoView() {
  nextTick(() => {
    const listEl = listRef.value;
    if (!listEl || highlightIndex.value < 0) return;
    const el = listEl.querySelector(
      `[data-store-opt-index="${highlightIndex.value}"]`,
    ) as HTMLElement | null;
    if (!el) return;
    const listRect = listEl.getBoundingClientRect();
    const elRect = el.getBoundingClientRect();
    const deltaTop = elRect.top - listRect.top;
    const deltaBottom = elRect.bottom - listRect.bottom;
    if (deltaTop < 0) {
      listEl.scrollTop += deltaTop;
    } else if (deltaBottom > 0) {
      listEl.scrollTop += deltaBottom;
    }
  });
}

function handleKeydown(e: KeyboardEvent) {
  if (!showDropdown.value || visibleOptions.value.length === 0) {
    if (e.key === 'Escape') closePanel();
    return;
  }
  const len = visibleOptions.value.length;
  switch (e.key) {
    case 'ArrowDown':
      e.preventDefault();
      // 头尾循环：最后一项再按下 → 回到第一项
      highlightIndex.value =
        highlightIndex.value < 0 ? 0 : (highlightIndex.value + 1) % len;
      scrollHighlightIntoView();
      break;
    case 'ArrowUp':
      e.preventDefault();
      // 头尾循环：第一项再按上 → 跳到最后一项
      highlightIndex.value =
        highlightIndex.value <= 0 ? len - 1 : highlightIndex.value - 1;
      scrollHighlightIntoView();
      break;
    case 'Enter':
      e.preventDefault();
      {
        const idx = highlightIndex.value >= 0 ? highlightIndex.value : 0;
        const item = visibleOptions.value[idx];
        if (item) selectOption(item);
      }
      break;
    case 'Escape':
      e.preventDefault();
      closePanel();
      break;
  }
}

function handleClickOutside(e: MouseEvent) {
  if (!showDropdown.value) return;
  const t = e.target as Node;
  if (containerRef.value?.contains(t) || panelRef.value?.contains(t)) return;
  closePanel();
}

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

onMounted(() => {
  document.addEventListener('mousedown', handleClickOutside);
  window.addEventListener('resize', onWinChange);
  window.addEventListener('scroll', onWinChange, true);
  mergeInitialItems(props.initialItems || []);
});

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

watch(
  () => props.storeType,
  () => {
    options.value = [];
    closePanel();
    searchKeyword.value = '';
  },
);

watch(
  () => props.initialItems,
  (items) => mergeInitialItems(items || []),
  { deep: true },
);

watch(visibleOptions, (list) => {
  if (!showDropdown.value) return;
  if (list.length === 0) {
    highlightIndex.value = -1;
    return;
  }
  if (highlightIndex.value >= list.length) {
    highlightIndex.value = list.length - 1;
  }
});
</script>

<template>
  <div ref="containerRef" class="store-multi-search">
    <Input
      v-model:value="searchKeyword"
      :placeholder="placeholder"
      :disabled="disabled"
      allow-clear
      @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="showDropdown"
        ref="panelRef"
        class="store-multi-search__panel"
        :style="panelStyle"
      >
        <div class="store-multi-search__hint">
          <span>↑↓ 循环切换 · Enter 选用 · Esc 关闭 · 可连续多选</span>
          <span class="store-multi-search__count">
            剩 {{ visibleOptions.length }} 条可选
          </span>
        </div>
        <div class="store-multi-search__body">
          <Spin :spinning="loading">
            <Empty
              v-if="!loading && visibleOptions.length === 0"
              :image="Empty.PRESENTED_IMAGE_SIMPLE"
              description="暂无匹配门店（或已全部选中）"
              class="py-3"
            />
            <div ref="listRef" class="store-multi-search__list">
              <button
                v-for="(item, index) in visibleOptions"
                :key="item.id"
                type="button"
                class="store-multi-search__option"
                :class="{ 'is-active': index === highlightIndex }"
                :data-store-opt-index="index"
                @mousedown.prevent="selectOption(item)"
              >
                <div class="store-multi-search__option-name">
                  {{ item.name }}【{{ item.id }}】
                </div>
                <div v-if="item.shouzimu" class="store-multi-search__option-sub">
                  首拼：{{ item.shouzimu }}
                </div>
              </button>
            </div>
          </Spin>
        </div>
      </div>
    </Teleport>

    <div v-if="selectedItems.length > 0" class="store-multi-search__tags">
      <Tag
        v-for="item in selectedItems"
        :key="item.id"
        closable
        color="blue"
        @close="() => removeSelected(item.id)"
      >
        <!-- 文本单独包一层才能做省略：antd Tag 的内容是裸文本节点，无法直接限宽 -->
        <span
          class="store-multi-search__tag-text"
          :title="`${item.name}【${item.id}】`"
        >
          {{ item.name }}【{{ item.id }}】
        </span>
      </Tag>
    </div>
  </div>
</template>

<style scoped lang="scss">
.store-multi-search {
  position: relative;
  width: 100%;
}

.store-multi-search__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;

  /* 单个超长门店名不能撑破容器：限宽 + 省略号，完整名走 title 悬停提示 */
  :deep(.ant-tag) {
    display: inline-flex;
    align-items: center;
    max-width: 100%;
    min-width: 0;
    /* 间距交给 gap 控制，去掉 antd 自带右外边距避免换行时右侧不齐 */
    margin-inline-end: 0;
  }

  :deep(.ant-tag-close-icon) {
    flex-shrink: 0;
  }
}

.store-multi-search__tag-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>

<style lang="scss">
/* Teleport 到 body，非 scoped，避免气泡被裁切/丢样式 */
.store-multi-search__panel {
  overflow: hidden;
  display: flex;
  flex-direction: column;
  border-radius: 10px;
  background: hsl(var(--card, var(--background)));
  border: 1px solid hsl(var(--border));
  box-shadow:
    0 8px 24px hsl(0 0% 0% / 0.12),
    0 2px 8px hsl(0 0% 0% / 0.06);
}

.store-multi-search__hint {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  flex-shrink: 0;
  padding: 8px 12px;
  font-size: 12px;
  color: hsl(var(--muted-foreground));
  border-bottom: 1px solid hsl(var(--border));
  background: hsl(var(--muted) / 0.35);
}

.store-multi-search__count {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}

/* 表头固定，仅 body/list 滚动 */
.store-multi-search__body {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.store-multi-search__body .ant-spin-nested-loading,
.store-multi-search__body .ant-spin-container {
  flex: 1;
  min-height: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.store-multi-search__list {
  flex: 1;
  min-height: 0;
  overflow-x: hidden;
  overflow-y: auto;
  padding: 6px;
}

.store-multi-search__option {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  background: transparent;
  transition: background 0.15s;
}

.store-multi-search__option:hover,
.store-multi-search__option.is-active {
  background: hsl(var(--accent) / 0.85);
}

.store-multi-search__option-name {
  font-size: 13px;
  font-weight: 500;
  color: hsl(var(--foreground));
  line-height: 1.4;
}

.store-multi-search__option-sub {
  margin-top: 2px;
  font-size: 12px;
  color: hsl(var(--muted-foreground));
}
</style>
