<script lang="ts">
  import { appRpc } from './rpc.ts';

  interface Props {
    open: boolean;
    onClose: () => void;
    onNavigate?: (resultType: string, id: string) => void;
  }

  let { open, onClose, onNavigate }: Props = $props();

  interface SearchResult {
    resultType: string;
    id: string;
    title: string;
    snippet: string;
    score: number;
  }

  let query = $state('');
  let results = $state<SearchResult[]>([]);
  let loading = $state(false);
  let selectedIndex = $state(0);
  let inputEl: HTMLInputElement | undefined = $state();
  let searchError = $state<string | null>(null);

  // Debounce timer
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
  // Fix #7: Request counter to discard stale results
  let requestToken = 0;

  // Group results by type
  let grouped = $derived(() => {
    const groups: Record<string, SearchResult[]> = {};
    for (const r of results) {
      const key = r.resultType;
      if (!groups[key]) groups[key] = [];
      groups[key].push(r);
    }
    return groups;
  });

  let groupLabels: Record<string, string> = {
    message: 'Messages',
    task: 'Tasks',
    btmsg: 'Communications',
  };

  function handleInput() {
    if (debounceTimer) clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => doSearch(), 300);
  }

  async function doSearch() {
    const q = query.trim();
    if (!q) {
      results = [];
      searchError = null;
      return;
    }
    loading = true;
    searchError = null;
    const token = ++requestToken;
    try {
      const res = await appRpc.request['search.query']({ query: q, limit: 20 });
      // Fix #7: Discard stale results if a newer request was issued
      if (token !== requestToken) return;
      if (res.error) {
        searchError = res.error;
        results = [];
      } else {
        results = res.results ?? [];
      }
      selectedIndex = 0;
    } catch (err) {
      if (token !== requestToken) return;
      console.error('[search]', err);
      results = [];
      searchError = 'Search failed';
    } finally {
      if (token === requestToken) loading = false;
    }
  }

  function handleKeydown(e: KeyboardEvent) {
    if (e.key === 'Escape') {
      e.preventDefault();
      onClose();
    } else if (e.key === 'ArrowDown') {
      e.preventDefault();
      if (selectedIndex < results.length - 1) selectedIndex++;
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      if (selectedIndex > 0) selectedIndex--;
    } else if (e.key === 'Enter' && results.length > 0) {
      e.preventDefault();
      const item = results[selectedIndex];
      if (item) {
        onNavigate?.(item.resultType, item.id);
        onClose();
      }
    }
  }

  function selectResult(idx: number) {
    const item = results[idx];
    if (item) {
      onNavigate?.(item.resultType, item.id);
      onClose();
    }
  }

  /**
   * Fix #4: Render snippet as plain text, highlight query matches client-side with <mark>.
   * Strips any HTML from snippet (from FTS5 <b> tags), then highlights matches safely.
   */
  function highlightQuery(snippet: string, q: string): string {
    // Strip existing HTML tags (FTS5 returns <b>...</b>)
    const plain = snippet.replace(/<[^>]*>/g, '');
    // Escape HTML entities
    const escaped = plain
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
    if (!q.trim()) return escaped;
    // Escape regex special chars in query
    const safeQ = q.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    const re = new RegExp(`(${safeQ})`, 'gi');
    return escaped.replace(re, '<mark>$1</mark>');
  }

  // Focus input when opened
  $effect(() => {
    if (open && inputEl) {
      requestAnimationFrame(() => inputEl?.focus());
    }
    if (!open) {
      query = '';
      results = [];
      selectedIndex = 0;
    }
  });
</script>

<!-- Fix #11: display toggle instead of {#if} to avoid DOM add/remove during click events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="overlay-backdrop" style:display={open ? 'flex' : 'none'} onclick={onClose} onkeydown={handleKeydown}>
  <!-- svelte-ignore a11y_no_static_element_interactions -->
  <div class="overlay-panel" onclick={(e) => e.stopPropagation()} onkeydown={handleKeydown}>
      <div class="search-input-wrap">
        <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
          <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
        </svg>
        <input
          bind:this={inputEl}
          class="search-input"
          type="text"
          placeholder="Search messages, tasks, communications..."
          bind:value={query}
          oninput={handleInput}
        />
        {#if loading}
          <span class="loading-dot" aria-label="Searching"></span>
        {/if}
        <kbd class="esc-hint">Esc</kbd>
      </div>

      {#if results.length > 0}
        <div class="results-list">
          {#each Object.entries(grouped()) as [type, items]}
            <div class="result-group">
              <div class="group-label">{groupLabels[type] ?? type}</div>
              {#each items as item, i}
                {@const flatIdx = results.indexOf(item)}
                <button
                  class="result-item"
                  class:selected={flatIdx === selectedIndex}
                  onclick={() => selectResult(flatIdx)}
                  onmouseenter={() => selectedIndex = flatIdx}
                >
                  <span class="result-title">{item.title}</span>
                  <span class="result-snippet">{@html highlightQuery(item.snippet, query)}</span>
                </button>
              {/each}
            </div>
          {/each}
        </div>
      {:else if searchError}
        <div class="no-results search-error">Invalid query: {searchError}</div>
      {:else if query.trim() && !loading}
        <div class="no-results">No results for "{query}"</div>
      {/if}
  </div>
</div>

<style>
  .overlay-backdrop {
    position: fixed;
    inset: 0;
    background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
    z-index: 200;
    display: flex;
    justify-content: center;
    padding-top: 15vh;
  }

  .overlay-panel {
    width: min(36rem, 90vw);
    max-height: 60vh;
    background: var(--ctp-base);
    border: 1px solid var(--ctp-surface1);
    border-radius: 0.5rem;
    box-shadow: 0 1rem 3rem color-mix(in srgb, var(--ctp-crust) 50%, transparent);
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  .search-input-wrap {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1rem;
    border-bottom: 1px solid var(--ctp-surface0);
  }

  .search-icon {
    width: 1rem;
    height: 1rem;
    color: var(--ctp-overlay1);
    flex-shrink: 0;
  }

  .search-input {
    flex: 1;
    background: transparent;
    border: none;
    outline: none;
    color: var(--ctp-text);
    font-size: 0.875rem;
    font-family: var(--ui-font-family, system-ui, sans-serif);
  }

  .search-input::placeholder {
    color: var(--ctp-overlay0);
  }

  .loading-dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: var(--ctp-blue);
    animation: pulse 0.8s ease-in-out infinite;
  }

  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.3; }
  }

  .esc-hint {
    padding: 0.1rem 0.3rem;
    background: var(--ctp-surface0);
    border: 1px solid var(--ctp-surface1);
    border-radius: 0.2rem;
    font-size: 0.625rem;
    color: var(--ctp-overlay0);
    font-family: var(--ui-font-family, system-ui, sans-serif);
  }

  .results-list {
    overflow-y: auto;
    padding: 0.25rem 0;
  }

  .result-group {
    padding: 0.25rem 0;
  }

  .group-label {
    font-size: 0.625rem;
    font-weight: 600;
    color: var(--ctp-overlay0);
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0.25rem 1rem;
  }

  .result-item {
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.125rem;
    padding: 0.375rem 1rem;
    background: transparent;
    border: none;
    text-align: left;
    cursor: pointer;
    color: var(--ctp-text);
    font: inherit;
    font-size: 0.8125rem;
  }

  .result-item:hover, .result-item.selected {
    background: var(--ctp-surface0);
  }

  .result-title {
    font-weight: 500;
    color: var(--ctp-text);
  }

  .result-snippet {
    font-size: 0.75rem;
    color: var(--ctp-subtext0);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .result-snippet :global(mark) {
    color: var(--ctp-yellow);
    font-weight: 600;
    background: transparent;
  }

  .search-error {
    color: var(--ctp-red);
  }

  .no-results {
    padding: 1.5rem 1rem;
    text-align: center;
    color: var(--ctp-overlay0);
    font-size: 0.8125rem;
  }

  .results-list::-webkit-scrollbar { width: 0.375rem; }
  .results-list::-webkit-scrollbar-track { background: transparent; }
  .results-list::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
</style>
