<script lang="ts">
  import { onMount, tick } from 'svelte';
  import { page } from '$app/stores';
  import { CookingPot, X } from 'lucide-svelte';
  import type { Snapshot } from './$types';
  import type { SearchHit } from '$lib/server/recipes/search-local';
  import { randomQuote } from '$lib/quotes';
  import SearchLoader from '$lib/components/SearchLoader.svelte';
  import SearchFilter from '$lib/components/SearchFilter.svelte';
  import { profileStore } from '$lib/client/profile.svelte';
  import { searchFilterStore } from '$lib/client/search-filter.svelte';
  import { requireOnline } from '$lib/client/require-online';
  import { SearchStore, type SearchSnapshot } from '$lib/client/search.svelte';

  const LOCAL_PAGE = 30;

  const store = new SearchStore({
    pageSize: LOCAL_PAGE,
    webFilterParam: () => {
      const p = searchFilterStore.queryParam;
      return p ? `&domains=${encodeURIComponent(p)}` : '';
    }
  });

  let quote = $state('');
  let recent = $state<SearchHit[]>([]);
  let favorites = $state<SearchHit[]>([]);

  const ALL_PAGE = 10;
  type AllSort = 'name' | 'rating' | 'cooked' | 'created';
  const ALL_SORTS: { value: AllSort; label: string }[] = [
    { value: 'name', label: 'Name' },
    { value: 'rating', label: 'Bewertung' },
    { value: 'cooked', label: 'Zuletzt gekocht' },
    { value: 'created', label: 'Hinzugefügt' }
  ];
  let allRecipes = $state<SearchHit[]>([]);
  let allSort = $state<AllSort>('name');
  let allExhausted = $state(false);
  let allLoading = $state(false);
  let allSentinel: HTMLElement | undefined = $state();
  let allChips: HTMLElement | undefined = $state();
  let allObserver: IntersectionObserver | null = null;

  // Snapshot persists across history navigation. We capture not only the
  // search store, but also the pagination depth ("user had loaded 60
  // recipes via infinite scroll") so on back-nav we can re-hydrate the
  // full list in one fetch — otherwise the document is too short and
  // scroll-restore can't reach the saved Y position.
  //
  // SvelteKit calls snapshot.restore AFTER onMount (post-mount tick),
  // so a flag-based handoff to onMount won't fire — we trigger
  // rehydrateAll directly from restore. onMount still calls
  // loadAllMore() for the fresh-mount case; if restore lands first,
  // allLoading guards the duplicate fetch, otherwise rehydrateAll's
  // larger result simply overwrites loadAllMore's initial 10 items.
  type HomeSnapshot = SearchSnapshot & {
    allLoaded: number;
    allSort: AllSort;
    allExhausted: boolean;
  };

  export const snapshot: Snapshot<HomeSnapshot> = {
    capture: () => ({
      ...store.captureSnapshot(),
      allLoaded: allRecipes.length,
      allSort,
      allExhausted
    }),
    restore: (s) => {
      store.restoreSnapshot(s);
      if (s.allLoaded > 0) {
        allSort = s.allSort;
        void rehydrateAll(s.allSort, s.allLoaded, s.allExhausted);
      }
    }
  };

  async function rehydrateAll(sort: AllSort, count: number, exhausted: boolean) {
    allLoading = true;
    try {
      const res = await fetch(`/api/recipes/all?sort=${sort}&limit=${count}&offset=0`);
      if (!res.ok) return;
      const body = await res.json();
      const hits = body.hits as SearchHit[];
      allRecipes = hits;
      allExhausted = exhausted || hits.length < count;
    } finally {
      allLoading = false;
    }
  }

  async function loadRecent() {
    const res = await fetch('/api/recipes/search');
    const body = await res.json();
    recent = body.hits;
  }

  async function loadAllMore() {
    if (allLoading || allExhausted) return;
    allLoading = true;
    try {
      const res = await fetch(
        `/api/recipes/all?sort=${allSort}&limit=${ALL_PAGE}&offset=${allRecipes.length}`
      );
      if (!res.ok) return;
      const body = await res.json();
      const more = body.hits as SearchHit[];
      const seen = new Set(allRecipes.map((h) => h.id));
      const deduped = more.filter((h) => !seen.has(h.id));
      allRecipes = [...allRecipes, ...deduped];
      if (more.length < ALL_PAGE) allExhausted = true;
    } finally {
      allLoading = false;
    }
  }

  async function setAllSort(next: AllSort) {
    if (next === allSort) return;
    allSort = next;
    if (typeof window !== 'undefined') localStorage.setItem('kochwas.allSort', next);
    if (allLoading) return;
    // Position der Sort-Chips vor dem Swap merken — wenn der Rezept-Block
    // beim Tausch kürzer wird, hält der Browser sonst nicht Schritt und
    // snapt nach oben. Wir korrigieren nach dem Render per scrollBy.
    const chipsBefore = allChips?.getBoundingClientRect().top ?? 0;
    allLoading = true;
    try {
      const res = await fetch(
        `/api/recipes/all?sort=${next}&limit=${ALL_PAGE}&offset=0`
      );
      if (!res.ok) return;
      const body = await res.json();
      const hits = body.hits as SearchHit[];
      allRecipes = hits;
      allExhausted = hits.length < ALL_PAGE;
      await tick();
      const chipsAfter = allChips?.getBoundingClientRect().top ?? 0;
      const delta = chipsAfter - chipsBefore;
      if (typeof window !== 'undefined' && Math.abs(delta) > 1) {
        window.scrollBy({ top: delta, left: 0, behavior: 'instant' });
      }
    } finally {
      allLoading = false;
    }
  }

  async function loadFavorites(profileId: number) {
    const res = await fetch(`/api/recipes/favorites?profile_id=${profileId}`);
    if (!res.ok) {
      favorites = [];
      return;
    }
    const body = await res.json();
    favorites = body.hits;
  }

  onMount(() => {
    quote = randomQuote();
    // Restore query from URL so history.back() from preview/recipe
    // brings the user back to the same search results.
    const urlQ = ($page.url.searchParams.get('q') ?? '').trim();
    if (urlQ) store.query = urlQ;
    void loadRecent();
    void searchFilterStore.load();
    const saved = localStorage.getItem('kochwas.allSort');
    if (saved && ['name', 'rating', 'cooked', 'created'].includes(saved)) {
      allSort = saved as AllSort;
    }
    // Fresh-mount: kick off the initial 10. On back-nav, snapshot.restore
    // also fires rehydrateAll(60); if it lands first, allLoading guards
    // this; if loadAllMore lands first, rehydrateAll's larger result
    // simply overwrites allRecipes once it resolves.
    void loadAllMore();
  });

  // IntersectionObserver an den Sentinel hängen — wenn sichtbar, nachladen.
  $effect(() => {
    if (typeof window === 'undefined') return;
    if (!allSentinel) return;
    if (allExhausted) return;
    if (allObserver) allObserver.disconnect();
    allObserver = new IntersectionObserver(
      (entries) => {
        for (const e of entries) {
          if (e.isIntersecting) void loadAllMore();
        }
      },
      { rootMargin: '200px' }
    );
    allObserver.observe(allSentinel);
    return () => {
      allObserver?.disconnect();
      allObserver = null;
    };
  });

  // Bei Änderung der Domain-Auswahl: laufende Suche neu ausführen,
  // damit der User nicht manuell re-tippen muss.
  $effect(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    searchFilterStore.active;
    store.reSearch();
  });

  // Sync current query back into the URL as ?q=... via replaceState,
  // without spamming the history stack. Pushing a new entry happens only
  // when the user clicks a result or otherwise navigates away.
  $effect(() => {
    if (typeof window === 'undefined') return;
    const q = store.query.trim();
    const url = new URL(window.location.href);
    const current = url.searchParams.get('q') ?? '';
    if (q === current) return;
    if (q) url.searchParams.set('q', q);
    else url.searchParams.delete('q');
    history.replaceState(history.state, '', url.toString());
  });

  $effect(() => {
    const active = profileStore.active;
    if (!active) {
      favorites = [];
      return;
    }
    void loadFavorites(active.id);
  });

  $effect(() => {
    // eslint-disable-next-line @typescript-eslint/no-unused-expressions
    store.query; // register reactive dep
    store.runDebounced();
  });

  function submit(e: SubmitEvent) {
    e.preventDefault();
    const q = store.query.trim();
    if (q.length <= 3) return;
    void store.runSearch(q);
  }

  async function dismissFromRecent(recipeId: number, e: MouseEvent) {
    e.preventDefault();
    e.stopPropagation();
    if (!requireOnline('Das Entfernen')) return;
    recent = recent.filter((r) => r.id !== recipeId);
    await fetch(`/api/recipes/${recipeId}`, {
      method: 'PATCH',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ hidden_from_recent: true })
    });
  }

  const activeSearch = $derived(store.query.trim().length > 3);
</script>

<section class="hero">
  <h1>Kochwas</h1>
  <p class="tagline" aria-live="polite">{quote || '\u00a0'}</p>
  <form class="search-form" onsubmit={submit}>
    <div class="search-box">
      <SearchFilter inline />
      <input
        type="search"
        bind:value={store.query}
        placeholder="Rezept suchen…"
        autocomplete="off"
        inputmode="search"
        aria-label="Suchbegriff"
      />
    </div>
  </form>
</section>

{#if activeSearch}
  <section class="results">
    {#if store.searching && store.hits.length === 0 && store.webHits.length === 0}
      <SearchLoader scope="local" />
    {:else}
      {#if store.hits.length > 0}
        <ul class="cards">
          {#each store.hits as r (r.id)}
            <li>
              <a href={`/recipes/${r.id}`} class="card">
                {#if r.image_path}
                  <img src={`/images/${r.image_path}`} alt="" loading="lazy" />
                {:else}
                  <div class="placeholder"><CookingPot size={36} /></div>
                {/if}
                <div class="card-body">
                  <div class="title">{r.title}</div>
                  {#if r.source_domain}
                    <div class="domain">{r.source_domain}</div>
                  {/if}
                </div>
              </a>
            </li>
          {/each}
        </ul>
      {:else if store.searchedFor === store.query.trim() && !store.webSearching && store.webHits.length === 0 && !store.webError}
        <p class="muted no-local-msg">Keine lokalen Rezepte für „{store.searchedFor}".</p>
      {/if}

      {#if store.webHits.length > 0}
        {#if store.hits.length > 0}
          <h3 class="sep">Aus dem Internet</h3>
        {:else if store.searchedFor === store.query.trim()}
          <p class="muted no-local-msg">
            Keine lokalen Rezepte für „{store.searchedFor}" — Ergebnisse aus dem Internet:
          </p>
        {/if}
        <ul class="cards">
          {#each store.webHits as w (w.url)}
            <li>
              <a class="card" href={`/preview?url=${encodeURIComponent(w.url)}`}>
                {#if w.thumbnail}
                  <img src={w.thumbnail} alt="" loading="lazy" />
                {:else}
                  <div class="placeholder"><CookingPot size={36} /></div>
                {/if}
                <div class="card-body">
                  <div class="title">{w.title}</div>
                  <div class="domain">{w.domain}</div>
                </div>
              </a>
            </li>
          {/each}
        </ul>
      {/if}

      {#if store.webSearching}
        <SearchLoader scope="web" />
      {:else if store.webError && store.webHits.length === 0}
        <p class="error">Internet-Suche zurzeit nicht möglich: {store.webError}</p>
      {/if}

      {#if store.searchedFor === store.query.trim() && !(store.localExhausted && store.webExhausted) && !(store.searching && store.hits.length === 0)}
        <div class="more-cta">
          <button class="more-btn" onclick={() => store.loadMore()} disabled={store.loadingMore || store.webSearching}>
            {store.loadingMore || store.webSearching ? 'Lade …' : '+ weitere Ergebnisse'}
          </button>
        </div>
      {/if}
    {/if}
  </section>
{:else}
  {#if profileStore.active && favorites.length > 0}
    <section class="listing">
      <h2>Deine Favoriten</h2>
      <ul class="cards">
        {#each favorites as r (r.id)}
          <li class="card-wrap">
            <a href={`/recipes/${r.id}`} class="card">
              {#if r.image_path}
                <img src={`/images/${r.image_path}`} alt="" loading="lazy" />
              {:else}
                <div class="placeholder"><CookingPot size={36} /></div>
              {/if}
              <div class="card-body">
                <div class="title">{r.title}</div>
                {#if r.source_domain}
                  <div class="domain">{r.source_domain}</div>
                {/if}
              </div>
            </a>
          </li>
        {/each}
      </ul>
    </section>
  {/if}
  {#if recent.length > 0}
    <section class="listing">
      <h2>Zuletzt hinzugefügt</h2>
      <ul class="cards">
        {#each recent as r (r.id)}
          <li class="card-wrap">
            <a href={`/recipes/${r.id}`} class="card">
              {#if r.image_path}
                <img src={`/images/${r.image_path}`} alt="" loading="lazy" />
              {:else}
                <div class="placeholder"><CookingPot size={36} /></div>
              {/if}
              <div class="card-body">
                <div class="title">{r.title}</div>
                {#if r.source_domain}
                  <div class="domain">{r.source_domain}</div>
                {/if}
              </div>
            </a>
            <button
              class="dismiss"
              aria-label="Aus Zuletzt-hinzugefügt entfernen"
              onclick={(e) => dismissFromRecent(r.id, e)}
            >
              <X size={16} strokeWidth={2.5} />
            </button>
          </li>
        {/each}
      </ul>
    </section>
  {/if}
  <section class="listing">
    <div class="listing-head">
      <h2>Alle Rezepte</h2>
    </div>
    <div
      class="sort-chips"
      role="tablist"
      aria-label="Sortierung"
      bind:this={allChips}
    >
      {#each ALL_SORTS as s (s.value)}
        <button
          type="button"
          role="tab"
          aria-selected={allSort === s.value}
          class="chip"
          class:active={allSort === s.value}
          onclick={() => setAllSort(s.value)}
        >
          {s.label}
        </button>
      {/each}
    </div>
    {#if allRecipes.length === 0 && allExhausted}
      <p class="muted">Noch keine Rezepte gespeichert.</p>
    {:else}
      <ul class="cards">
        {#each allRecipes as r (r.id)}
          <li>
            <a href={`/recipes/${r.id}`} class="card">
              {#if r.image_path}
                <img src={`/images/${r.image_path}`} alt="" loading="lazy" />
              {:else}
                <div class="placeholder"><CookingPot size={36} /></div>
              {/if}
              <div class="card-body">
                <div class="title">{r.title}</div>
                <div class="meta-line">
                  {#if r.avg_stars !== null}
                    <span class="stars">★ {r.avg_stars.toFixed(1)}</span>
                  {/if}
                  {#if r.source_domain}
                    <span class="domain">{r.source_domain}</span>
                  {/if}
                </div>
              </div>
            </a>
          </li>
        {/each}
      </ul>
      {#if !allExhausted}
        <div bind:this={allSentinel} class="sentinel" aria-hidden="true">
          {#if allLoading}<span class="loading">Lade …</span>{/if}
        </div>
      {/if}
    {/if}
  </section>
{/if}

<style>
  .hero {
    text-align: center;
    padding: 3rem 0 1.5rem;
  }
  .hero h1 {
    font-size: clamp(2.2rem, 8vw, 3.5rem);
    margin: 0 0 0.5rem;
    color: #2b6a3d;
    letter-spacing: -0.02em;
  }
  .tagline {
    margin: 0 auto 1.5rem;
    max-width: 36rem;
    color: #6a7670;
    font-style: italic;
    font-size: 1rem;
    line-height: 1.35;
    min-height: 1.4rem;
  }
  form {
    display: flex;
  }
  .search-box {
    flex: 1;
    display: flex;
    align-items: stretch;
    background: white;
    border: 1px solid #cfd9d1;
    border-radius: 12px;
    min-height: 52px;
    /* Kein overflow:hidden — sonst clippt der Filter-Dropdown. */
    position: relative;
  }
  .search-box:focus-within {
    outline: 2px solid #2b6a3d;
    outline-offset: 1px;
  }
  input[type='search'] {
    flex: 1;
    padding: 0.9rem 1rem;
    font-size: 1.1rem;
    border: 0;
    background: transparent;
    min-width: 0;
  }
  input[type='search']:focus {
    outline: none;
  }
  .results,
  .listing {
    margin-top: 1.5rem;
  }
  .listing h2 {
    font-size: 1.05rem;
    color: #444;
    margin: 0 0 0.75rem;
  }
  .listing-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin-bottom: 0.75rem;
    flex-wrap: wrap;
  }
  .listing-head h2 {
    margin: 0;
  }
  .sort-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0 0 0.75rem;
  }
  .chip {
    padding: 0.4rem 0.85rem;
    background: white;
    border: 1px solid #cfd9d1;
    border-radius: var(--pill-radius);
    color: #2b6a3d;
    font-size: 0.88rem;
    cursor: pointer;
    min-height: 36px;
    font-family: inherit;
    white-space: nowrap;
  }
  .chip:hover {
    background: #f4f8f5;
  }
  .chip.active {
    background: #2b6a3d;
    color: white;
    border-color: #2b6a3d;
    font-weight: 600;
  }
  .meta-line {
    display: flex;
    gap: 0.4rem;
    font-size: 0.8rem;
    color: #888;
    margin-top: 0.25rem;
    align-items: center;
    flex-wrap: wrap;
  }
  .stars {
    color: #2b6a3d;
    font-weight: 600;
  }
  .sentinel {
    min-height: 40px;
    display: grid;
    place-items: center;
    padding: 1rem 0;
  }
  .loading {
    color: #888;
    font-size: 0.85rem;
  }
  .muted {
    color: #888;
    text-align: center;
    padding: 1rem 0;
  }
  .no-local-msg {
    font-size: 0.95rem;
    padding: 0.25rem 0 1rem;
  }
  .error {
    color: #c53030;
    text-align: center;
    padding: 1rem 0;
  }
  .cards {
    list-style: none;
    padding: 0;
    margin: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 0.75rem;
  }
  .card-wrap {
    position: relative;
  }
  .card {
    display: block;
    background: white;
    border: 1px solid #e4eae7;
    border-radius: 14px;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    transition: transform 0.1s;
  }
  .card:active {
    transform: scale(0.98);
  }
  .card img,
  .placeholder {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    background: #eef3ef;
    display: grid;
    place-items: center;
    color: #8fb097;
  }
  .card-body {
    padding: 0.6rem 0.75rem 0.75rem;
  }
  .title {
    font-weight: 600;
    font-size: 0.95rem;
    line-height: 1.25;
  }
  .domain {
    font-size: 0.8rem;
    color: #888;
    margin-top: 0.25rem;
  }
  .dismiss {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    width: 28px;
    height: 28px;
    border-radius: var(--pill-radius);
    border: 0;
    background: rgba(255, 255, 255, 0.9);
    color: #444;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
  }
  .dismiss:hover {
    background: #fff;
    color: #c53030;
  }
  .sep {
    margin: 1.5rem 0 0.5rem;
    font-size: 0.9rem;
    font-weight: 600;
    color: #666;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    padding-bottom: 0.3rem;
    border-bottom: 1px solid #e4eae7;
  }
  .more-cta {
    margin-top: 1.25rem;
    text-align: center;
  }
  .more-btn {
    padding: 0.75rem 1.25rem;
    background: white;
    color: #2b6a3d;
    border: 1px solid #cfd9d1;
    border-radius: 10px;
    font-size: 0.95rem;
    min-height: 44px;
    cursor: pointer;
  }
  .more-btn:hover:not(:disabled) {
    background: #f4f8f5;
  }
  .more-btn:disabled {
    opacity: 0.6;
    cursor: progress;
  }
</style>
