<template>
  <div>
    <div
      ref="root"
      :class="mergedGroupClass"
    >
    <button
      :id="buttonId"
      ref="buttonRef"
      type="button"
      class="peer relative w-full border bg-white pl-3 pr-10 py-1 text-left outline-none"
      :class="[
          isReadonly ? '' : 'grow-height',
          isReadonly ? '' : 'focus-visible:border-m-primary',
          hasError
            ? isOpen
              ? openDirection === 'down'
                ? 'rounded-b-none !border !border-m-danger !border-b-transparent'
                : 'rounded-t-none !border !border-m-danger !border-t-transparent'
              : 'border-m-danger'
            : hasSuccess
              ? isOpen
                ? openDirection === 'down'
                  ? 'rounded-b-none !border !border-m-success !border-b-transparent'
                  : 'rounded-t-none !border !border-m-success !border-t-transparent'
                : 'border-m-success'
              : isReadonly
                ? 'border-black'
                : isOpen
                  ? openDirection === 'down'
                    ? 'rounded-b-none !border !border-m-primary !border-b-transparent'
                    : 'rounded-t-none !border !border-m-primary !border-t-transparent'
                  : isOptionSelected
                    ? 'border-black'
                    : 'border-m-muted',
          disabled ? 'cursor-not-allowed border-m-muted text-black/60' : isReadonly ? 'cursor-default' : 'cursor-pointer',
          label ? 'min-h-[40px]' : 'h-[40px] py-0',
          rounded,
          textField,
          keyboardFocused
            ? (isOpen
              ? (openDirection === 'down' ? 'm-combo-ring-top' : 'm-combo-ring-bottom')
              : 'm-focus-ring-kbd')
            : '',
  ]"
      :aria-expanded="isOpen"
      :aria-controls="listboxId"
      :aria-activedescendant="!isOpen ? undefined : (activeIndex === -1 ? selectAllId : (activeIndex >= 0 ? optionId(activeIndex) : undefined))"
      :aria-invalid="hasError"
      :aria-describedby="describedBy"
      :aria-required="required || undefined"
      :aria-readonly="isReadonly || undefined"
      :disabled="disabled"
      @click="toggle"
      @keydown="onKeydown"
      @focus="onKbdFocus"
      @blur="onKbdBlur"
    >
      <label
        v-if="label"
        class="floating-label pointer-events-none absolute left-3 inline-block origin-left transition-transform duration-150 font-medium"
        :class="[
          isOpen ? 'top-2  z-30' : 'top-2',
          hasError
            ? 'text-m-danger'
            : hasSuccess
              ? 'text-m-success'
              : disabled
                ? 'text-m-muted'
                : isReadonly
                  ? isOptionSelected
                    ? 'text-black'
                    : 'text-m-muted'
                  : isOpen
                    ? 'text-m-primary'
                    : isOptionSelected
                      ? 'text-black'
                      : 'text-m-muted',
          textLabel,
        ]"
        :style="labelTransformStyle"
      >
        {{ label }}<MalioRequiredMark v-if="required" />
      </label>

      <div
        v-if="displayTags && selectedOptions.length > 0"
        class="flex flex-wrap items-center justify-start gap-1"
        :class="[label ? 'pt-1' : '']"
      >
        <span
          v-for="option in visibleTags"
          :key="String(option.value)"
          class="inline-flex max-w-full items-center rounded-[20px] px-2 pt-[2px] pb-0 text-lg font-medium leading-[normal]"
          :class="[
            option.color ? '' : 'bg-m-bg',
            disabled ? 'text-black/60' : 'text-black',
          ]"
          :style="tagStyle(option)"
        >
          <span class="truncate pb-[2px]">{{ option.label }}</span>
        </span>
        <span
          v-if="hiddenTagsCount > 0"
          data-test="tags-overflow"
          class="inline-flex items-center rounded-[20px] bg-m-bg px-2 pt-[2px] pb-0 text-lg font-medium leading-[normal]"
          :class="disabled ? 'text-black/60' : 'text-black'"
        >
          <span class="pb-[2px]">+{{ hiddenTagsCount }}</span>
        </span>
      </div>

      <span
        v-else-if="displayTag && emptyOptionLabel"
        class="block truncate text-right"
        :class="[
          textValue,
          label ? 'pl-24' : '',
          'text-m-muted'
        ]"
      >
        {{ emptyOptionLabel }}
      </span>

      <span
        v-if="!displayTag"
        class="block truncate text-right"
        :class="[
          textValue,
          label ? 'pl-24' : '',
          disabled ? 'text-black/60' : isOptionSelected ? 'text-black' : 'text-m-muted'
        ]"
      >
        {{ selectionSummary }}
      </span>

      <span
        v-if="!disabled"
        data-test="chevron"
        class="absolute right-3 top-1/2 -translate-y-1/2"
        :class="[
          hasError
            ? 'text-m-danger'
            : hasSuccess
              ? 'text-m-success'
              : disabled
                ? 'text-m-muted'
                : isReadonly
                  ? isOptionSelected
                    ? 'text-black'
                    : 'text-m-muted'
                  : isOpen
                    ? 'text-m-primary'
                    : isOptionSelected
                      ? 'text-black'
                      : 'text-m-muted'
        ]"
      >
      <slot name="icon">
        <IconifyIcon
          icon="mdi:chevron-down"
          width="20"
          class="transition-transform duration-300"
          :class="isOpen ? 'rotate-180' : 'rotate-0'"
        />
      </slot>
    </span>
    </button>

    <ul
      v-if="isOpen"
      :id="listboxId"
      ref="listRef"
      role="listbox"
      :aria-labelledby="buttonId"
      class="absolute left-0 right-0 z-20 max-h-60 w-full overflow-auto border bg-white"
      :class="[
        openDirection === 'down'
          ? 'top-[calc(100%-4px)] rounded-b-md border-t-0'
          : 'bottom-[calc(100%-4px)] rounded-t-md border-b-0',
        hasError
          ? 'select-scrollbar-error'
          : hasSuccess
            ? 'select-scrollbar-success'
            : 'select-scrollbar-primary',
        hasError
          ? 'border-m-danger'
          : hasSuccess
            ? 'border-m-success'
            : 'border-m-primary',
        keyboardFocused
          ? (openDirection === 'down' ? 'm-combo-ring-bottom' : 'm-combo-ring-top')
          : '',
      ]"
    >
      <li
        v-if="normalizedOptions.length === 0"
        class="px-3 py-2 text-m-muted"
        data-test="no-options-text"
      >
        {{ noOptionsText }}
      </li>
      <li
        v-if="displaySelectAll && normalizedOptions.length > 0"
        :id="selectAllId"
        role="option"
        :aria-selected="allSelected"
        class="cursor-pointer border-b border-m-muted/30 px-3 py-2"
        :class="[activeIndex === -1 ? 'bg-m-muted/10' : '']"
        @mouseenter="activeIndex = -1"
        @mousedown.prevent
        @click="toggleAll"
      >
        <Checkbox
          :model-value="allSelected"
          :label="selectAllLabel"
          :disabled="disabled"
          group-class="!mt-0 pointer-events-none"
          label-class="option-checkbox w-full font-semibold"
          tabindex="-1"
          :reserve-message-space="false"
        />
      </li>
      <li
        v-for="(opt, index) in normalizedOptions"
        :id="optionId(index)"
        :key="String(opt.value)"
        role="option"
        :aria-selected="isChecked(opt.value)"
        class="cursor-pointer px-3 py-2"
        :class="[
            index === activeIndex ? 'bg-m-muted/10' : '',
            isChecked(opt.value) ? 'bg-m-muted/10 font-semibold' : '',
            'text-black'
          ]"
        @mouseenter="activeIndex = index"
        @mousedown.prevent
        @click="toggleOption(opt.value)"
      >
        <Checkbox
          :model-value="isChecked(opt.value)"
          :label="opt.label || '\u00A0'"
          :disabled="disabled"
          group-class="!mt-0 pointer-events-none"
          label-class="option-checkbox w-full"
          tabindex="-1"
          :reserve-message-space="false"
        />
      </li>
    </ul>
    </div>
    <p
      v-if="reserveMessageSpace || hint || error || success"
      :id="`${buttonId}-describedby`"
      :class="[
        hasError
          ? 'text-m-danger'
          : hasSuccess
            ? 'text-m-success'
            : 'text-m-muted',
        'mt-1 ml-[2px] text-xs',
        reserveMessageSpace ? 'min-h-[1rem]' : '',
      ]"
    >
      {{ error || success || hint }}
    </p>
  </div>
</template>

<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, useId, nextTick, watch} from 'vue'
import {Icon as IconifyIcon} from '@iconify/vue'
import {twMerge} from 'tailwind-merge'
import Checkbox from '../checkbox/Checkbox.vue'
import MalioRequiredMark from '../shared/RequiredMark.vue'
import {useKbdFocusRing} from '../shared/useKbdFocusRing'

defineOptions({name: 'MalioSelectCheckbox', inheritAttrs: false})

const {keyboardFocused, onFocus: onKbdFocus, onBlur: onKbdBlur} = useKbdFocusRing()

type Option = {
  label: string;
  value: string | number;
  color?: string;
  textColor?: string
}
const props = withDefaults(defineProps<{
  modelValue?: Array<string | number>
  options?: Option[]
  emptyOptionLabel?: string
  label?: string
  hint?: string
  error?: string
  success?: string
  textField?: string
  textValue?: string
  textLabel?: string
  rounded?: string
  displayTag?: boolean
  maxTags?: number
  displaySelectAll?: boolean
  selectAllLabel?: string
  disabled?: boolean
  readonly?: boolean
  groupClass?: string
  noOptionsText?: string
  required?: boolean
  reserveMessageSpace?: boolean
}>(), {
  modelValue: () => [],
  options: () => [],
  emptyOptionLabel: '',
  label: '',
  hint: '',
  error: '',
  success: '',
  textField: 'text-lg',
  textValue: 'text-lg',
  textLabel: 'text-sm',
  rounded: 'rounded-md',
  displayTag: false,
  maxTags: 0,
  displaySelectAll: false,
  selectAllLabel: 'Tout sélectionner',
  disabled: false,
  readonly: false,
  groupClass: '',
  noOptionsText: 'Aucune option disponible',
  required: false,
  reserveMessageSpace: true,
})

const emit = defineEmits<{
  (e: 'update:modelValue', v: Array<string | number>): void
}>()
const root = ref<HTMLElement | null>(null)
const buttonRef = ref<HTMLButtonElement | null>(null)
const isOpen = ref(false)
const activeIndex = ref(-1)
const openDirection = ref<'down' | 'up'>('down')
const uid = useId()
const buttonId = `custom-select-btn-${uid}`
const listboxId = `custom-select-listbox-${uid}`
const selectAllId = `custom-select-all-${uid}`
// Index actif le plus bas : -1 cible la ligne « tout sélectionner » quand elle est affichée
const lowestIndex = computed(() => (props.displaySelectAll && normalizedOptions.value.length > 0 ? -1 : 0))
const listRef = ref<HTMLElement | null>(null)
const listHeight = ref(0)
const normalizedOptions = computed<Option[]>(() => props.options)
const mergedGroupClass = computed(() =>
  twMerge('relative w-full h-12 flex items-center', props.groupClass),
)
const hasError = computed(() => !!props.error)
const hasSuccess = computed(() => !!props.success && !hasError.value)
const isOptionSelected = computed(() =>
  props.modelValue.length > 0
)
const isReadonly = computed(() => props.readonly && !props.disabled)
const selectedOptions = computed(() =>
  normalizedOptions.value.filter(option => props.modelValue.includes(option.value)),
)
const displayTags = computed(() =>
  props.displayTag && selectedOptions.value.length > 0,
)
const visibleTags = computed(() =>
  props.maxTags > 0 ? selectedOptions.value.slice(0, props.maxTags) : selectedOptions.value,
)
const hiddenTagsCount = computed(() =>
  props.maxTags > 0 ? Math.max(selectedOptions.value.length - props.maxTags, 0) : 0,
)
function tagStyle(option: Option) {
  const style: Record<string, string> = {}
  if (option.color) style.backgroundColor = option.color
  // En disabled on garde le gris hérité (text-black/60), on n'applique pas la couleur custom
  if (option.textColor && !props.disabled) style.color = option.textColor
  return Object.keys(style).length ? style : undefined
}
const shouldFloatLabel = computed(() =>
  isReadonly.value ? isOptionSelected.value : (isOpen.value || displayTags.value)
)
const selectionSummary = computed(() =>
  `${props.modelValue.length}/${normalizedOptions.value.length}`
)
const allSelected = computed(() =>
  normalizedOptions.value.length > 0
  && normalizedOptions.value.every(opt => props.modelValue.includes(opt.value)),
)
const describedBy = computed(() =>
  (hasError.value || hasSuccess.value || !!props.hint) ? `${buttonId}-describedby` : undefined,
)

function optionId(index: number) {
  return `custom-select-opt-${uid}-${index}`
}

function updateOpenDirection() {
  if (!root.value) return

  const rect = root.value.getBoundingClientRect()
  const estimatedListHeight = Math.min(normalizedOptions.value.length * 40, 240)
  const spaceBelow = window.innerHeight - rect.bottom
  const spaceAbove = rect.top

  openDirection.value =
    spaceBelow >= estimatedListHeight || spaceBelow >= spaceAbove
      ? 'down'
      : 'up'
}

function open() {
  if (props.disabled || props.readonly) return
  updateOpenDirection()
  isOpen.value = true

  const selectedIndex = normalizedOptions.value.findIndex(o => props.modelValue.includes(o.value))
  activeIndex.value = selectedIndex >= 0 ? selectedIndex : 0

  nextTick(() => {
    if (openDirection.value === 'up' && listRef.value) {
      listHeight.value = listRef.value.offsetHeight
    } else {
      listHeight.value = 0
    }
  })
}

const labelTransformStyle = computed(() => {
  // label non flottant
  if (!shouldFloatLabel.value) {
    return {}
  }

  // fermé ou ouverture vers le bas : comportement classique
  if (!isOpen.value || openDirection.value === 'down') {
    return {
      transform: 'translateY(-1.15rem) scale(0.9)',
    }
  }

  // ouverture vers le haut : on remonte en fonction de la hauteur de la liste
  const extraOffset = 8 // marge visuelle au-dessus de la liste en px
  const total = 4 +listHeight.value + extraOffset
  // 18 ≈ 1.15rem pour garder la même base que votre flottant actuel

  return {
    transform: `translateY(-${total}px) scale(0.9)`,
  }
})

function close() {
  isOpen.value = false
}

function toggle() {
  if (props.disabled || props.readonly) return
  if (isOpen.value) {
    close()
    return
  }
  open()
}

function isChecked(value: string | number) {
  return props.modelValue.includes(value)
}

function toggleOption(value: string | number) {
  if (isChecked(value)) {
    emit('update:modelValue', props.modelValue.filter(item => item !== value))
  } else {
    emit('update:modelValue', [...props.modelValue, value])
  }
  nextTick(() => buttonRef.value?.focus())
}

function toggleAll() {
  if (allSelected.value) {
    emit('update:modelValue', [])
  } else {
    emit('update:modelValue', normalizedOptions.value.map(opt => opt.value))
  }
  nextTick(() => buttonRef.value?.focus())
}

// Garde l'option active visible quand on navigue au clavier
watch(activeIndex, async (index) => {
  if (!isOpen.value) return
  await nextTick()
  const id = index === -1 ? selectAllId : (index >= 0 ? optionId(index) : null)
  if (id) document.getElementById(id)?.scrollIntoView({block: 'nearest'})
})

function onKeydown(e: KeyboardEvent) {
  if (props.disabled || props.readonly) return

  // Tab : laisse le focus partir mais ferme la liste
  if (e.key === 'Tab') {
    if (isOpen.value) close()
    return
  }

  // Liste fermée : ouverture au clavier
  if (!isOpen.value) {
    if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
      e.preventDefault()
      open()
    }
    return
  }

  // Liste ouverte (multi-select : Entrée/Espace togglent et la liste reste ouverte)
  if (e.key === 'Escape') {
    e.preventDefault()
    close()
    return
  }
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault()
    // -1 = ligne « tout sélectionner »
    if (activeIndex.value === -1) {
      toggleAll()
      return
    }
    const opt = normalizedOptions.value[activeIndex.value]
    if (opt) toggleOption(opt.value)
    return
  }
  if (e.key === 'ArrowDown') {
    e.preventDefault()
    activeIndex.value = Math.min(activeIndex.value + 1, normalizedOptions.value.length - 1)
    return
  }
  if (e.key === 'ArrowUp') {
    e.preventDefault()
    activeIndex.value = Math.max(activeIndex.value - 1, lowestIndex.value)
    return
  }
  if (e.key === 'Home') {
    e.preventDefault()
    activeIndex.value = lowestIndex.value
    return
  }
  if (e.key === 'End') {
    e.preventDefault()
    activeIndex.value = normalizedOptions.value.length - 1
  }
}

function onClickOutside(e: MouseEvent) {
  if (!root.value) return
  if (!root.value.contains(e.target as Node)) close()
}

onMounted(() => document.addEventListener('mousedown', onClickOutside))
onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
</script>

<style scoped>
.floating-label {
  background: white;
  padding: 0 0.25rem;
}

.grow-height {
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

@media (prefers-reduced-motion: reduce) {
  .grow-height {
    transition: none;
  }
}

:deep(ul[role="listbox"]) {
  scrollbar-width: auto;
  scrollbar-gutter: stable;
}

:deep(.select-scrollbar-primary) {
  scrollbar-color: rgb(var(--m-primary)) transparent;
}

:deep(.select-scrollbar-error) {
  scrollbar-color: #000000 transparent;
}

:deep(.select-scrollbar-success) {
  scrollbar-color: #000000 transparent;
}

</style>
