<script lang="ts">
  import { useQueryClient } from '@tanstack/svelte-query';
  import {
    createSuggestionsQuery,
    createSnoozesQuery,
    snoozeSuggestion,
    unsnoozeSuggestion
  } from '$lib/api/suggestions';
  import { createRequest } from '$lib/api/requests';
  import { qk } from '$lib/api/queries';
  import { pushToast } from '$lib/stores/toast.svelte';
  import DiscoverResultCard from './DiscoverResultCard.svelte';
  import type { ArtistSuggestion, SeedContribution, SuggestionSnooze } from '$lib/api/types';

  const client = useQueryClient();
  const queryStore = createSuggestionsQuery();
  const query = $derived($queryStore);
  const suggestions = $derived((query.data ?? []) as ArtistSuggestion[]);

  const snoozeStore = createSnoozesQuery();
  const snoozeQuery = $derived($snoozeStore);
  const snoozes = $derived((snoozeQuery.data ?? []) as SuggestionSnooze[]);

  // Track MBIDs the user just requested so the card flips immediately.
  let optimisticRequested = $state(new Set<string>());
  // Snoozed-just-now MBIDs. These keep their card in place showing an Undo,
  // rather than yanking it out of the grid under the cursor — the card is
  // gone on the next refetch, and the snoozed list below is the way back
  // after that.
  let optimisticSnoozed = $state(new Set<string>());

  function visible(s: ArtistSuggestion): boolean {
    return !optimisticRequested.has(s.mbid);
  }

  function cardState(s: ArtistSuggestion): 'requestable' | 'snoozed' {
    return optimisticSnoozed.has(s.mbid) ? 'snoozed' : 'requestable';
  }

  function withMbid(set: Set<string>, mbid: string, present: boolean): Set<string> {
    const next = new Set(set);
    if (present) next.add(mbid);
    else next.delete(mbid);
    return next;
  }

  // "Matches your taste in shoegaze and dream pop." — the reason line when the
  // candidate's own tags overlap the taste profile (#2377). Preferred over seed
  // attribution because it describes the MUSIC ("sounds like what you like")
  // rather than the graph ("adjacent to something you played"), which is the
  // whole point of slice 6. Falls back when there are no matched tags, which is
  // the common case: coverage is partial by nature (#2376).
  function reasonText(s: ArtistSuggestion): string {
    const tags = s.matched_tags ?? [];
    if (tags.length === 0) return attributionText(s.attribution);
    if (tags.length === 1) return `Matches your taste in ${tags[0]}.`;
    if (tags.length === 2) return `Matches your taste in ${tags[0]} and ${tags[1]}.`;
    return `Matches your taste in ${tags[0]}, ${tags[1]}, and ${tags[2]}.`;
  }

  function attributionText(attribution: SeedContribution[]): string {
    if (attribution.length === 0) return '';
    const verb = (s: SeedContribution) => (s.is_liked ? 'liked' : 'played');
    const phrases = attribution.map((s) => `${verb(s)} ${s.name}`);
    if (phrases.length === 1) {
      return `Because you ${phrases[0]}.`;
    }
    if (phrases.length === 2) {
      return `Because you ${phrases[0]} and ${phrases[1]}.`;
    }
    // 3 with Oxford comma
    return `Because you ${phrases[0]}, ${phrases[1]}, and ${phrases[2]}.`;
  }

  // "in 3 months" / "in 12 days" — a relative phrase, because the exact
  // calendar date of a 90-day snooze is noise the user never asked for.
  function returnsIn(snoozedUntil: string): string {
    const ms = new Date(snoozedUntil).getTime() - Date.now();
    if (!Number.isFinite(ms) || ms <= 0) return 'shortly';
    const days = Math.round(ms / 86_400_000);
    if (days < 1) return 'today';
    if (days === 1) return 'tomorrow';
    // The 30 must not exceed the divisor below, or the singular "in about a
    // month" is unreachable — a rounded month count of 1 needs 15..44 days,
    // and any higher threshold sends all of those down the days branch. This
    // read 45 and the singular case was dead code (caught by the Android
    // unit test for the same logic).
    if (days < 30) return `in ${days} days`;
    const months = Math.round(days / 30);
    return months === 1 ? 'in about a month' : `in about ${months} months`;
  }

  async function onRequest(s: ArtistSuggestion) {
    try {
      await createRequest({
        kind: 'artist',
        lidarr_artist_mbid: s.mbid,
        artist_name: s.name
      });
      optimisticRequested = withMbid(optimisticRequested, s.mbid, true);
      // The server-side filter hides this candidate on next refetch.
      await client.invalidateQueries({ queryKey: qk.suggestions() });
    } catch {
      // Swallow for v1; the SPA will refetch on next mount and the card
      // stays requestable so the user can retry.
    }
  }

  async function onSnooze(s: ArtistSuggestion) {
    // Flip first so the tap feels instant, then reconcile. On failure the
    // card goes back to requestable and says so — a snooze that silently
    // did nothing would leave the user tapping it again.
    optimisticSnoozed = withMbid(optimisticSnoozed, s.mbid, true);
    try {
      await snoozeSuggestion(s.mbid, s.name);
      await client.invalidateQueries({ queryKey: qk.suggestionSnoozes() });
    } catch {
      optimisticSnoozed = withMbid(optimisticSnoozed, s.mbid, false);
      pushToast(`Couldn't hide ${s.name}`, 'error');
    }
  }

  async function onUnsnooze(mbid: string, name: string) {
    optimisticSnoozed = withMbid(optimisticSnoozed, mbid, false);
    try {
      await unsnoozeSuggestion(mbid);
    } catch (e) {
      // 404 means it wasn't snoozed after all — the user's intended end
      // state, so it isn't an error worth showing them.
      if ((e as { status?: number })?.status !== 404) {
        optimisticSnoozed = withMbid(optimisticSnoozed, mbid, true);
        pushToast(`Couldn't bring ${name} back`, 'error');
        return;
      }
    }
    await Promise.all([
      client.invalidateQueries({ queryKey: qk.suggestionSnoozes() }),
      client.invalidateQueries({ queryKey: qk.suggestions() })
    ]);
  }
</script>

<div>
  <header class="mb-4 space-y-1">
    <h2 class="font-display text-2xl font-medium text-text-primary">Suggested for you</h2>
    <p class="text-text-secondary">Out-of-library artists drawn from what you've liked and played.</p>
  </header>

  {#if !query.isPending && suggestions.length === 0}
    <!--
      An empty deck used to mean one thing — no listening signal yet. With
      snoozing it can also mean "you parked them all", and telling that user to
      go listen to something would be wrong advice.
    -->
    <p class="text-text-secondary">
      {snoozes.length === 0
        ? 'Listen to something or like an artist to start getting suggestions.'
        : "Nothing new right now — the artists you've parked are below."}
    </p>
  {:else if suggestions.length > 0}
    <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
      {#each suggestions.filter(visible) as s (s.mbid)}
        <DiscoverResultCard
          kind="artist"
          title={s.name}
          imageUrl={s.image_url}
          state={cardState(s)}
          attribution={reasonText(s)}
          onRequest={() => onRequest(s)}
          onSnooze={() => onSnooze(s)}
          onUnsnooze={() => onUnsnooze(s.mbid, s.name)}
        />
      {/each}
    </div>
  {/if}

  <!--
    The snoozed list is not a nicety: a parked suggestion is by definition
    absent from the deck above, so without this there is no way back. Only
    rendered when non-empty, so the surface stays quiet for the common case.
  -->
  {#if snoozes.length > 0}
    <section class="mt-8 border-t border-border pt-6" aria-labelledby="snoozed-heading">
      <h3 id="snoozed-heading" class="font-display text-lg font-medium text-text-primary">
        Not right now
      </h3>
      <p class="mt-1 text-sm text-text-secondary">
        These come back on their own. Nothing here counts against your taste profile.
      </p>
      <ul class="mt-3 divide-y divide-border">
        {#each snoozes as snoozed (snoozed.mbid)}
          <li class="flex items-center justify-between gap-4 py-2">
            <div class="min-w-0">
              <div class="truncate text-sm text-text-primary">{snoozed.name}</div>
              <div class="text-xs text-text-secondary">
                Back {returnsIn(snoozed.snoozed_until)}
              </div>
            </div>
            <button
              type="button"
              aria-label={`Bring ${snoozed.name} back now`}
              class="shrink-0 rounded-md border border-border px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover focus:outline-none focus:ring-2 focus:ring-accent"
              onclick={() => onUnsnooze(snoozed.mbid, snoozed.name)}
            >
              Bring back
            </button>
          </li>
        {/each}
      </ul>
    </section>
  {/if}
</div>
