<template>
  <div ref="rootRef" class="vm-role-picker">
    <button
      ref="triggerRef"
      type="button"
      class="vm-role-picker__trigger"
      :class="{ 'is-open': open, 'is-disabled': disabled }"
      :disabled="disabled"
      @click="togglePanel"
    >
      <span v-if="displayText" class="vm-role-picker__value">{{ displayText }}</span>
      <span v-else class="vm-role-picker__ph">{{ placeholder ?? crudLabel('pickRole') }}</span>
      <i
        v-if="clearable && picked.size"
        class="ri-close-circle-fill vm-role-picker__clear"
        @click.stop="clear"
      />
      <i class="ri-arrow-down-s-line vm-role-picker__caret" />
    </button>

    <Teleport to="body">
      <div
        v-if="open"
        ref="panelRef"
        class="vm-role-picker__panel vm-dialog-float"
        :style="panelStyle"
      >
        <div v-if="loading" class="vm-role-picker__state">{{ crudLabel('loading') }}</div>
        <div v-else-if="!options.length" class="vm-role-picker__state">{{ crudLabel('noRoles') }}</div>
        <div v-else class="vm-role-picker__list vm-scroll">
          <label
            v-for="opt in options"
            :key="String(opt.id)"
            class="vm-role-picker__item"
          >
            <Checkbox
              :model-value="picked.has(Number(opt.id))"
              :disabled="disabled"
              @update:model-value="(v) => toggle(Number(opt.id), Boolean(v))"
            />
            <span class="vm-role-picker__name">{{ opt.name }}</span>
            <span v-if="opt.label" class="vm-role-picker__label">{{ opt.label }}</span>
          </label>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { crudFormat, crudLabel } from '../crud/style'
import { useVome } from '../hooks/useVome'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { POPUP_TRIGGER_GAP } from '../lib/dialog-float'
defineOptions({ name: 'vm-role-picker' })

const props = withDefaults(
  defineProps<{
    modelValue?: number[]
    disabled?: boolean
    clearable?: boolean
    placeholder?: string
    /** base：后台角色；user：前端角色 */
    source?: 'base' | 'user'
  }>(),
  {
    modelValue: () => [],
    disabled: false,
    clearable: true,
    placeholder: undefined,
    source: 'base',
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: number[]]
}>()

const { service } = useVome()
const rootRef = ref<HTMLElement | null>(null)
const triggerRef = ref<HTMLElement | null>(null)
const panelRef = ref<HTMLElement | null>(null)
const open = ref(false)
const panelStyle = ref<Record<string, string>>({})
const loading = ref(false)
const options = ref<Array<{ id: number; name: string; label?: string }>>([])

const picked = computed(() => new Set(props.modelValue))

const displayText = computed(() => {
  const ids = props.modelValue || []
  if (!ids.length) return ''
  const names = ids
    .map((id) => options.value.find((o) => o.id === id)?.name || String(id))
    .filter(Boolean)
  if (!names.length) return crudFormat(crudLabel('selectedCount'), ids.length)
  if (names.length === 1) return names[0]!
  return `${names[0]} +${names.length - 1}`
})

async function loadRoles() {
  loading.value = true
  try {
    // EPS prefix：/admin/base/role → service.base.role；/admin/user/role → service.user.role
    const api =
      props.source === 'user' ? service.user?.role : service.base?.role
    if (!api?.list) {
      options.value = []
      return
    }
    const list = (await api.list({ status: 1 })) as Array<
      Record<string, unknown>
    >
    options.value = list
      .filter((r) => r.id != null)
      .map((r) => ({
        id: Number(r.id),
        name: String(r.name ?? ''),
        label: r.label ? String(r.label) : undefined,
      }))
  } catch (e) {
    console.error(e)
    options.value = []
  } finally {
    loading.value = false
  }
}

function toggle(id: number, on: boolean) {
  if (props.disabled) return
  const next = new Set(picked.value)
  if (on) next.add(id)
  else next.delete(id)
  emit('update:modelValue', [...next])
}

function clear() {
  emit('update:modelValue', [])
}

function syncPanelPos() {
  const el = triggerRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  const width = Math.max(r.width, 220)
  // 贴触发框左侧 + 底部
  panelStyle.value = {
    position: 'fixed',
    top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
    left: `${r.left}px`,
    width: `${width}px`,
    zIndex: '200',
  }
}

async function togglePanel() {
  if (props.disabled) return
  open.value = !open.value
  if (open.value) {
    await nextTick()
    syncPanelPos()
  }
}

function isInside(el: Node | null) {
  if (!el) return false
  if (rootRef.value?.contains(el)) return true
  if (panelRef.value?.contains(el)) return true
  return el instanceof Element && Boolean(el.closest('.vm-role-picker__panel'))
}

function onDocPointer(e: Event) {
  const t = e.target as Node | null
  if (!isInside(t)) open.value = false
}

function onWinChange() {
  if (open.value) syncPanelPos()
}

watch(() => props.source, () => void loadRoles())

onMounted(() => {
  void loadRoles()
  document.addEventListener('pointerdown', onDocPointer, true)
  window.addEventListener('resize', onWinChange)
  window.addEventListener('scroll', onWinChange, true)
})
onUnmounted(() => {
  document.removeEventListener('pointerdown', onDocPointer, true)
  window.removeEventListener('resize', onWinChange)
  window.removeEventListener('scroll', onWinChange, true)
})
</script>

<style lang="scss">
.vm-role-picker {
  position: relative;
  width: 100%;
}

.vm-role-picker__trigger {
  display: flex;
  width: 100%;
  height: var(--vm-control-height);
  min-height: var(--vm-control-height);
  align-items: center;
  gap: 6px;
  padding: 0 var(--vm-control-padding-x) 0 12px;
  border: 1px solid var(--input);
  border-radius: var(--vm-control-radius);
  background: var(--background);
  color: var(--foreground);
  font-size: var(--vm-control-font-size);
  text-align: left;
  cursor: pointer;
  transition:
    border-color 0.15s ease,
    box-shadow 0.15s ease;

  outline: none;

  &:hover:not(.is-disabled) {
    border-color: color-mix(in srgb, var(--brand) 45%, var(--input));
  }

  /* 弹层触发：不展示打开/聚焦描边 */

  &.is-disabled {
    cursor: not-allowed;
    opacity: 0.55;
  }
}

.vm-role-picker__value {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.vm-role-picker__ph {
  flex: 1;
  color: var(--muted-foreground);
}

.vm-role-picker__clear {
  flex-shrink: 0;
  color: var(--muted-foreground);
  font-size: 14px;
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

.vm-role-picker__caret {
  flex-shrink: 0;
  opacity: 0.45;
  font-size: 16px;
}

.vm-role-picker__panel {
  max-height: min(280px, 45vh);
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
  box-shadow: var(--shadow-soft);
}

.vm-role-picker__list {
  display: grid;
  gap: 2px;
  max-height: min(280px, 45vh);
  padding: 6px;
  overflow: auto;
  overscroll-behavior: contain;
}

.vm-role-picker__item {
  display: flex;
  min-height: 32px;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-radius: 8px;
  cursor: pointer;

  &:hover {
    background: var(--muted);
  }
}

.vm-role-picker__name {
  font-size: 13px;
  color: var(--foreground);
}

.vm-role-picker__label {
  margin-left: auto;
  font-size: 11px;
  color: var(--muted-foreground);
}

.vm-role-picker__state {
  padding: 16px;
  font-size: 13px;
  color: var(--muted-foreground);
  text-align: center;
}
</style>
