<!-- #region SearchableMultiSelect [C:3] [TYPE Component] [SEMANTICS ui, multi-select, search, combobox, filter] -->
<!-- @BRIEF Searchable multi-select combobox with dropdown, checkboxes, Select All/Clear, and selected chips. Like dataset search in translate but with multi-select. -->
<!-- @UX_STATE default -> Input with dropdown hidden -->
<!-- @UX_STATE dropdown_open -> Dropdown visible with filtered options -->
<!-- @UX_STATE empty -> No options match the search filter -->
<!-- @UX_STATE loading -> Spinner in input while fetching options -->
<!-- @RELATION CALLED_BY -> [EXT:frontend:DashboardHub] -->
<!-- @RELATION CALLED_BY -> [ValidationTaskConfig] -->
<script lang="ts">
  /**
   * Searchable multi-select combobox.
   * @prop {Array<{id: string, name: string, subtitle?: string, hint?: string}>} options - Available items
   * @prop {string[]} selected - Bindable array of selected item IDs
   * @prop {string} label - Label text above input
   * @prop {string} placeholder - Placeholder for search input
   * @prop {string} emptyMessage - Text shown when no options match
   * @prop {boolean} loading - Show spinner in input
   * @prop {string} selectAllLabel - Text for Select All button
   * @prop {string} clearLabel - Text for Clear button
   * @prop {number} debounceMs - Debounce delay for search callback (0 = no debounce)
   * @prop {(query: string) => void} onSearch - Callback when user types (for server-side)
   * @prop {boolean} disabled - Disable the input
   */
  let {
    options = [],
    selected = $bindable([]),
    label = '',
    placeholder = 'Search...',
    emptyMessage = 'No matches found',
    loading = false,
    selectAllLabel = 'Select All',
    clearLabel = 'Clear',
    debounceMs = 0,
    onSearch = undefined,
    disabled = false,
  } = $props();

  let searchQuery = $state('');
  let isOpen = $state(false);
  let inputEl = $state(null);
  let containerEl = $state(null);
  let debounceTimer = $state(null);

  let filteredOptions = $derived(
    searchQuery
      ? options.filter(
          (o) =>
            o.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
            (o.subtitle &&
              o.subtitle.toLowerCase().includes(searchQuery.toLowerCase())),
        )
      : options,
  );

  let allFilteredSelected = $derived(
    filteredOptions.length > 0 &&
      filteredOptions.every((o) => selected.includes(o.id)),
  );

  let someFilteredSelected = $derived(
    filteredOptions.some((o) => selected.includes(o.id)),
  );

  function handleInput(e) {
    searchQuery = e.target.value;
    if (debounceMs > 0 && onSearch) {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(() => onSearch(searchQuery), debounceMs);
    }
    if (!isOpen) isOpen = true;
  }

  function handleFocus() {
    if (!disabled) isOpen = true;
  }

  function handleClickOutside(e) {
    if (containerEl && !containerEl.contains(e.target)) {
      isOpen = false;
    }
  }

  function toggle(id) {
    if (selected.includes(id)) {
      selected = selected.filter((s) => s !== id);
    } else {
      selected = [...selected, id];
    }
  }

  function selectAllFiltered() {
    const filteredIds = filteredOptions.map((o) => o.id);
    if (allFilteredSelected) {
      // Deselect all filtered
      selected = selected.filter((s) => !filteredIds.includes(s));
    } else {
      // Select all filtered
      const newSelected = new Set([...selected, ...filteredIds]);
      selected = Array.from(newSelected);
    }
  }

  function clearAll() {
    selected = [];
  }

  function removeItem(id) {
    selected = selected.filter((s) => s !== id);
  }

  function getItemById(id) {
    return options.find((o) => o.id === id);
  }

  // Click outside handler
  $effect(() => {
    if (typeof document !== 'undefined') {
      document.addEventListener('click', handleClickOutside);
      return () => {
        document.removeEventListener('click', handleClickOutside);
        clearTimeout(debounceTimer);
      };
    }
  });
</script>

<div bind:this={containerEl} class="relative">
  {#if label}
    <label class="block text-sm font-medium text-gray-700 mb-1">{label}</label>
  {/if}

  <!-- Search Input -->
  <div class="relative">
    <input
      bind:this={inputEl}
      type="text"
      value={searchQuery}
      oninput={handleInput}
      onfocus={handleFocus}
      placeholder={placeholder}
      disabled={disabled}
      class="w-full px-3 py-2 pr-8 border border-gray-300 rounded-lg text-sm focus-visible:ring-2 focus-visible:ring-primary-ring focus-visible:border-primary-ring {disabled ? 'bg-gray-50 cursor-not-allowed' : ''}"
    />
    {#if loading}
      <div class="absolute right-2.5 top-2.5 animate-spin h-4 w-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
    {:else if selected.length > 0 && !isOpen}
      <div class="absolute right-2.5 top-2.5 text-xs font-medium text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded-full">
        {selected.length}
      </div>
    {/if}
  </div>

  <!-- Selected Chips -->
  {#if selected.length > 0}
    <div class="flex flex-wrap gap-1 mt-1.5">
      {#each selected as id}
        {@const item = getItemById(id)}
        {#if item}
          <span class="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-700 rounded-full text-xs font-medium border border-blue-200">
            {item.name}
            <button
              onclick={() => removeItem(id)}
              class="hover:text-blue-900 hover:bg-blue-100 rounded-full p-0.5"
              aria-label="Remove {item.name}"
            >
              <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
          </span>
        {/if}
      {/each}
      <button
        onclick={clearAll}
        class="text-xs text-gray-500 hover:text-gray-700 px-1 py-0.5"
      >
        {clearLabel}
      </button>
    </div>
  {/if}

  <!-- Dropdown -->
  {#if isOpen}
    <div class="absolute z-50 mt-1 w-full bg-white border border-gray-200 rounded-lg shadow-lg">
      <!-- Select All / Clear toolbar -->
      {#if filteredOptions.length > 0}
        <div class="flex items-center justify-between px-3 py-2 border-b border-gray-100 bg-gray-50 rounded-t-lg">
          <button
            onclick={selectAllFiltered}
            class="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
          >
            {allFilteredSelected ? 'Deselect all' : selectAllLabel}
            <span class="text-gray-400 font-normal">
              ({selected.length}/{options.length})
            </span>
          </button>
          {#if selected.length > 0}
            <button
              onclick={clearAll}
              class="text-xs text-gray-500 hover:text-gray-700 transition-colors"
            >
              {clearLabel}
            </button>
          {/if}
        </div>
      {/if}

      <!-- Options list -->
      <div class="max-h-60 overflow-y-auto" role="listbox">
        {#if loading && options.length === 0}
          <div class="px-3 py-6 text-center text-sm text-gray-400">
            <div class="inline-block animate-spin h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full mb-2"></div>
            <p>Loading...</p>
          </div>
        {:else if filteredOptions.length === 0}
          <div class="px-3 py-6 text-center text-sm text-gray-400 italic">
            {emptyMessage}
          </div>
        {:else}
          {#each filteredOptions as opt}
            <label
              class="flex items-center gap-2.5 px-3 py-2 text-sm hover:bg-blue-50 cursor-pointer transition-colors border-b border-gray-50 last:border-b-0 {selected.includes(opt.id) ? 'bg-blue-50/50' : ''}"
              role="option"
              aria-selected={selected.includes(opt.id)}
            >
              <input
                type="checkbox"
                checked={selected.includes(opt.id)}
                onchange={() => toggle(opt.id)}
                class="rounded border-gray-300 text-blue-600 focus-visible:ring-blue-500"
              />
              <span class="font-medium text-gray-900">{opt.name}</span>
              {#if opt.subtitle}
                <span class="text-gray-400 text-xs">{opt.subtitle}</span>
              {/if}
              {#if opt.hint}
                <span class="ml-auto text-xs text-gray-400">{opt.hint}</span>
              {/if}
            </label>
          {/each}
        {/if}
      </div>
    </div>
  {/if}
</div>
<!-- #endregion SearchableMultiSelect -->
