<script>
  import { onMount, untrack } from 'svelte';
  import { goto } from '$app/navigation';
  import { getJSON, postJSON, putJSON, delJSON } from '$lib/api.js';
  import * as P from '$lib/prefs.js';
  import Header from '$lib/components/Header.svelte';
  import BottomNav from '$lib/components/BottomNav.svelte';
  import MoodNav from '$lib/components/MoodNav.svelte';
  import ArticleCard from '$lib/components/ArticleCard.svelte';
  import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
  import SignIn from '$lib/components/SignIn.svelte';
  import { auth, savedIds, refresh as refreshAuth } from '$lib/auth.svelte.js';

  let moods = $state([]);
  let topics = $state([]);
  let families = $state([]);
  let selected = $state('today'); // 'today' | a mood key | a topic key | 'tag:<slug>'
  let brief = $state(null);
  let heroIdx = $state(0); // which brief item fills the hero (advances if its image won't load)
  let feed = $state([]);
  let userPrefs = $state(P.blank());
  let showBoundaries = $state(false);
  let showHistory = $state(false);
  let showSignIn = $state(false);

  // Account/settings is its own page (/account) now — robust + scrolls to top.
  function openAccount() {
    if (auth.user) goto('/account');
    else showSignIn = true;
  }
  function openHistory() {
    showHistory = true;
    loadServerHistory();
  }

  // React only to sign-in (auth.user); untrack so reading history/prefs inside
  // doesn't make this re-run on every browse.
  $effect(() => {
    const u = auth.user;
    if (u && typeof window !== 'undefined') untrack(() => onLogin(u));
  });

  async function onLogin(u) {
    // One-time per account/device: fold this device's MEANINGFUL history
    // (opened/replaced — not everything shown) into the account.
    const key = 'goodnews:imported:' + u.id;
    if (!localStorage.getItem(key)) {
      try {
        await postJSON('/api/import', { seen: history.map((a) => a.id), saved: [] });
        localStorage.setItem(key, '1');
      } catch { /* best-effort */ }
    }
    loadServerHistory();
    // Prefs: adopt the account's saved prefs if any; otherwise seed from this device.
    try {
      const res = await getJSON('/api/prefs');
      if (res && res.prefs) {
        const incoming = Object.assign(P.blank(), res.prefs);
        if (JSON.stringify(incoming) !== JSON.stringify(userPrefs)) {
          userPrefs = incoming;
          P.save(userPrefs);
          select(selected, true);
        }
      } else {
        await putJSON('/api/prefs', { prefs: userPrefs });
      }
    } catch { /* best-effort */ }
  }
  let loading = $state(true);
  let error = $state('');

  // Device-local memory (no account), persisted in localStorage.
  const SEEN_KEY = 'goodnews:seen';
  const DISMISSED_KEY = 'goodnews:dismissed';
  const HISTORY_KEY = 'goodnews:history';
  const BRIEF_VIEW_KEY = 'goodnews:brief_view';
  const HISTORY_CAP = 200;

  let seenIds = new Set();      // articles DISPLAYED — so Replace doesn't recycle them
  let dismissed = $state(new Set());
  let history = $state([]);     // articles OPENED or REPLACED-away — the meaningful history
  let serverHistory = $state([]); // account history (cross-device) when signed in

  function persistSession() {
    P.saveJSON(SEEN_KEY, [...seenIds]);
    P.saveJSON(DISMISSED_KEY, [...dismissed]);
    P.saveJSON(HISTORY_KEY, history.slice(0, HISTORY_CAP));
  }
  // Mark articles as shown (for Replace exclusion only — NOT history).
  function markDisplayed(items) {
    let changed = false;
    for (const a of items || []) {
      if (a && !seenIds.has(a.id)) { seenIds.add(a.id); changed = true; }
    }
    if (changed) P.saveJSON(SEEN_KEY, [...seenIds]);
  }
  // Record a deliberate event: an article the user OPENED, or one they REPLACED
  // away (kept so an accidental replace is recoverable).
  function recordHistory(article) {
    if (!article) return;
    if (!history.some((h) => h.id === article.id)) {
      history = [article, ...history].slice(0, HISTORY_CAP);
      P.saveJSON(HISTORY_KEY, history);
    }
    if (auth.user) {
      if (!serverHistory.some((h) => h.id === article.id)) serverHistory = [article, ...serverHistory];
      postJSON('/api/history', { ids: [article.id] }).catch(() => {});
    }
  }
  function removeFromHistory(id) {
    history = history.filter((h) => h.id !== id);
    serverHistory = serverHistory.filter((h) => h.id !== id);
    P.saveJSON(HISTORY_KEY, history);
    if (auth.user) delJSON(`/api/history/${id}`).catch(() => {});
  }
  // The list shown in the History panel: account history when signed in, else device.
  let historyItems = $derived(auth.user ? serverHistory : history);
  async function loadServerHistory() {
    if (!auth.user) return;
    try { serverHistory = (await getJSON('/api/history')).items; } catch { /* leave as-is */ }
  }
  function clearSession() {
    seenIds = new Set();
    dismissed = new Set();
    history = [];
    persistSession();
    P.saveJSON(BRIEF_VIEW_KEY, null);
    showHistory = false;
    select(selected, true);
  }

  const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : s);
  const humanize = (s) => (s || '').replace(/-/g, ' ');
  let filtersOn = $derived(P.active(userPrefs));
  let currentMood = $derived(moods.find((m) => m.key === selected));
  let currentTopic = $derived(topics.find((t) => t.key === selected));
  let currentTag = $derived(selected.startsWith('tag:') ? selected.slice(4) : null);
  // The family a grouping tag belongs to — for the lane's calm subtitle.
  let tagFamily = $derived(
    currentTag ? families.find((f) => f.tags.some((t) => t.key === currentTag)) : null
  );
  let viewLabel = $derived(
    selected === 'today'
      ? 'Highlights from Today'
      : selected === 'saved'
        ? 'Saved'
        : currentTag
          ? humanize(currentTag)
          : (currentMood?.label ?? cap(currentTopic?.key) ?? '')
  );
  let viewSubtitle = $derived(
    selected === 'today'
      ? (brief?.brief_date ?? '')
      : selected === 'saved'
        ? 'Articles you saved to read later'
        : currentTag
          ? (tagFamily?.description ?? '')
          : (currentMood?.description ?? currentTopic?.description ?? '')
  );
  let activeTab = $derived(selected === 'today' ? 'today' : 'browse');

  // The hero is the only image slot. Some sources hotlink-protect their images
  // (e.g. Guardian → 401), so if the lead's image won't load, promote the next
  // brief item that has one. The failed lead just becomes a text tile.
  let heroArticle = $derived(brief?.items?.[heroIdx] ?? null);
  let restArticles = $derived((brief?.items ?? []).filter((_, i) => i !== heroIdx));
  function heroImageFailed() {
    const items = brief?.items ?? [];
    for (let i = heroIdx + 1; i < items.length; i++) {
      if (items[i]?.image_url) { heroIdx = i; return; }
    }
  }

  // The filter for the current view: a mood's preset, a topic include, or none.
  function viewFilter(key = selected) {
    if (key === 'today') return {};
    const m = moods.find((x) => x.key === key);
    if (m) return m.filter ?? {};
    return { include_topics: [key] }; // a topic
  }
  function mergedParam() {
    return P.param(P.merge(userPrefs, viewFilter()));
  }

  async function loadToday(fresh) {
    const q = P.param(userPrefs);
    const ex = Array.from(dismissed).join(',');
    const fetched = await getJSON(`/api/brief?limit=7${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`);
    const view = P.loadJSON(BRIEF_VIEW_KEY, null);
    const sameServerBrief =
      view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at;
    if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) {
      // Same server brief: keep the user's pinned order + replacements, but
      // refresh server-owned metadata by id. image_url especially is enriched
      // AFTER the brief is built (without bumping generated_at), so a verbatim
      // pinned copy can stay imageless forever. Items the user swapped in
      // (absent from the fresh brief) keep their own data.
      const freshById = new Map(fetched.items.map((a) => [a.id, a]));
      const items = view.items.map((it) => freshById.get(it.id) ?? it);
      brief = { ...fetched, items };
      P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items });
    } else {
      brief = fetched;
      P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
    }
    heroIdx = 0; // fresh brief — start the hero at the lead again
    markDisplayed(brief.items);
  }

  async function select(key, fresh = false) {
    selected = key;
    error = '';
    try {
      if (key === 'today') {
        await loadToday(fresh);
      } else if (key === 'saved') {
        feed = (await getJSON('/api/saved')).items;
        markDisplayed(feed);
      } else if (key.startsWith('tag:')) {
        const tag = key.slice(4);
        const q = P.param(userPrefs);
        const ex = Array.from(dismissed).join(',');
        feed = (await getJSON(`/api/feed?limit=24&tag=${encodeURIComponent(tag)}${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`)).items;
        markDisplayed(feed);
      } else {
        const q = P.param(P.merge(userPrefs, viewFilter(key)));
        const ex = Array.from(dismissed).join(',');
        feed = (await getJSON(`/api/feed?limit=24${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`)).items;
        markDisplayed(feed);
      }
    } catch (e) {
      error = 'Something went quiet — could not reach the feed.';
    }
    if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' });
  }

  function refreshPrefs() {
    userPrefs = { ...userPrefs };
    P.save(userPrefs);
    if (auth.user) putJSON('/api/prefs', { prefs: userPrefs }).catch(() => {}); // sync to the account
    select(selected, true);
  }
  function applyAction(kind, value) {
    P[kind]?.(userPrefs, value);
    refreshPrefs();
  }

  let notice = $state('');
  function flash(msg) {
    notice = msg;
    if (typeof window !== 'undefined') setTimeout(() => (notice = ''), 4000);
  }

  async function replaceArticle(article) {
    const list = selected === 'today' ? brief?.items : feed;
    if (!list) return;
    const isHero = selected === 'today' && heroArticle?.id === article.id;
    const exclude = Array.from(seenIds).join(',');
    const q = mergedParam();
    const url = `/api/replacement?exclude=${exclude}&avoid_paywall=true${isHero ? '&gentle=true' : ''}${q ? '&' + q : ''}`;
    let repl;
    try {
      repl = await getJSON(url);
    } catch {
      flash('Could not reach the feed just now.');
      return;
    }
    if (!repl) {
      flash("That's everything fresh for now — nothing new to swap in.");
      return;
    }
    dismissed.add(article.id);
    seenIds.add(article.id);
    markDisplayed([repl]);
    recordHistory(article); // keep the swapped-away story so an accidental replace is recoverable
    persistSession();
    if (selected === 'today') {
      const i = brief.items.findIndex((a) => a.id === article.id);
      if (i >= 0) {
        brief.items[i] = repl;
        brief = { ...brief, items: [...brief.items] };
        P.saveJSON(BRIEF_VIEW_KEY, { generated_at: brief.generated_at, items: brief.items });
      }
    } else {
      const i = feed.findIndex((a) => a.id === article.id);
      if (i >= 0) {
        feed[i] = repl;
        feed = [...feed];
      }
    }
  }

  function browse() {
    const go = () => document.getElementById('explore')?.scrollIntoView({ behavior: 'smooth' });
    if (selected !== 'today') select('today').then(go);
    else go();
  }

  onMount(async () => {
    userPrefs = P.load();
    seenIds = new Set(P.loadJSON(SEEN_KEY, []));
    dismissed = new Set(P.loadJSON(DISMISSED_KEY, []));
    history = P.loadJSON(HISTORY_KEY, []);
    refreshAuth(); // resolve any existing session (non-blocking)
    try {
      moods = await getJSON('/api/moods');
      topics = (await getJSON('/api/categories')).topics;
      // Non-fatal: the groupings backend (B1) may not be deployed yet. If it
      // isn't, the Explore-by-family section simply stays hidden and cards fall
      // back to the topic pill — the rest of the page works unchanged.
      try { families = await getJSON('/api/families'); } catch { families = []; }
      // Intent from the /account quick links (Saved / History / Boundaries).
      const params = new URLSearchParams(window.location.search);
      const view = params.get('view');
      const open = params.get('open');
      await select(view === 'saved' ? 'saved' : 'today');
      if (open === 'history') openHistory();
      if (open === 'boundaries') showBoundaries = true;
      if (view || open) window.history.replaceState({}, '', '/'); // tidy the URL
    } catch (e) {
      error = 'Could not reach Upbeat Bytes.';
    }
    loading = false;
  });
</script>

<Header
  onBoundaries={() => (showBoundaries = !showBoundaries)}
  onHistory={() => (showHistory ? (showHistory = false) : openHistory())}
  onaccount={openAccount}
  user={auth.user}
  {filtersOn}
/>

{#if showSignIn}<SignIn onclose={() => (showSignIn = false)} />{/if}

<main class="container">
  {#if moods.length}
    <MoodNav {moods} {selected} onselect={select} />
  {/if}

  {#if showBoundaries}
    <BoundariesPanel prefs={userPrefs} onchange={refreshPrefs} onclose={() => (showBoundaries = false)} />
  {/if}

  {#if showHistory}
    <section class="panel rise">
      <div class="phead">
        <h2>History</h2>
        <button class="close" onclick={() => (showHistory = false)}>done</button>
      </div>
      <p class="reassure">
        Stories you've opened, plus any you swapped away — so an accidental Replace stays
        recoverable. {auth.user ? 'Synced to your account, across devices.' : 'Kept on this device only.'}
        Remove anything you don't want to keep.
      </p>
      {#if historyItems.length}
        <ul class="hist">
          {#each historyItems as a (a.id)}
            <li>
              <a href={a.url} target="_blank" rel="noopener" onclick={() => recordHistory(a)}>{a.title}</a>
              <span class="hsrc">{a.source}</span>
              <button class="hx" title="Remove from history" aria-label="Remove from history"
                      onclick={() => removeFromHistory(a.id)}>×</button>
            </li>
          {/each}
        </ul>
      {:else}
        <p class="empty">Nothing yet — stories you open or swap away will appear here.</p>
      {/if}
      {#if historyItems.length || dismissed.size}
        <button class="reset" onclick={clearSession}>Clear my history (start fresh)</button>
      {/if}
    </section>
  {/if}

  {#if notice}<p class="notice rise">{notice}</p>{/if}

  {#if loading}
    <p class="muted center pad">Gathering the good news…</p>
  {:else if error}
    <p class="muted center pad">{error}</p>
  {:else}
    {#key selected}
      <header class="view-head rise">
        <h1>{viewLabel}</h1>
        {#if viewSubtitle}<p class="sub">{viewSubtitle}</p>{/if}
      </header>

      {#if selected === 'today'}
        {#if brief?.items?.length}
          <section class="rise">
            <ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={recordHistory} onimageerror={heroImageFailed} />
            {#if restArticles.length}
              <div class="grid rest">
                {#each restArticles as a (a.id)}
                  <ArticleCard article={a} onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={recordHistory} />
                {/each}
              </div>
            {/if}
          </section>
          <p class="endcap rise">✦ that's the good news for today ✦</p>
        {:else}
          <p class="muted center pad">No highlights yet today — try a calmer filter, or check back soon.</p>
        {/if}
      {:else if feed.length}
        <div class="grid rise">
          {#each feed as a (a.id)}
            <ArticleCard article={a} onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={recordHistory} />
          {/each}
        </div>
      {:else}
        {#if selected === 'saved'}
          <p class="muted center pad">Nothing saved yet — tap <strong>Save</strong> on any story to keep it here.</p>
        {:else}
          <p class="muted center pad">Nothing here right now — try another, or ease a boundary.</p>
        {/if}
      {/if}
    {/key}

    {#if families.length}
      <section id="explore" class="explore">
        <h2>Explore Upbeat Bytes</h2>
        <div class="families">
          {#each families as f (f.name)}
            {@const tags = f.tags.filter((t) => t.count > 0)}
            {#if tags.length}
              <div class="family">
                <h3>{f.name}</h3>
                <p class="fdesc">{f.description}</p>
                <div class="chips">
                  {#each tags as t (t.key)}
                    <button
                      class="chip"
                      class:active={selected === 'tag:' + t.key}
                      onclick={() => select('tag:' + t.key)}
                    >{humanize(t.key)}</button>
                  {/each}
                </div>
              </div>
            {/if}
          {/each}
        </div>
      </section>
    {/if}
  {/if}
</main>

<BottomNav active={activeTab} onToday={() => select('today')} onBrowse={browse} onYou={openAccount} user={auth.user} />

<style>
  main.container { padding-top: 6px; padding-bottom: 40px; min-height: 60vh; }

  .view-head { margin: 18px 0 18px; }
  .view-head h1 { font-size: clamp(2.1rem, 5.5vw, 2.8rem); line-height: 1.05; text-transform: capitalize; }
  .view-head .sub { margin: 8px 0 0; color: var(--muted); font-size: 1.02rem; }
  .view-head::after {
    content: ''; display: block; width: 46px; height: 3px;
    background: var(--accent); border-radius: 2px; margin-top: 14px; opacity: 0.8;
  }

  /* Explore — a quiet repository of groupings beneath the brief, not a nav row.
     Four calm families, each a doorway into its tags. */
  .explore { margin: 52px 0 8px; padding-top: 28px; border-top: 1px solid var(--line); }
  .explore h2 {
    font-size: 0.74rem; text-transform: uppercase; letter-spacing: 0.14em;
    color: var(--muted); font-family: var(--label); font-weight: 400; margin: 0 0 22px;
  }
  .families { display: grid; grid-template-columns: repeat(auto-fit, minmax(248px, 1fr)); gap: 26px 32px; }
  .family h3 { font-size: 1.16rem; margin: 0 0 2px; }
  .family .fdesc { margin: 0 0 11px; color: var(--muted); font-size: 0.86rem; line-height: 1.45; }
  .explore .chips { display: flex; flex-wrap: wrap; gap: 7px; }
  .explore .chip {
    border: 1px solid var(--line); background: var(--surface); color: var(--ink);
    border-radius: 999px; padding: 5px 13px; font-size: 0.82rem; cursor: pointer;
    transition: all 0.14s ease; text-transform: capitalize;
  }
  .explore .chip:hover { border-color: var(--accent); color: var(--accent-deep); }
  .explore .chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }

  /* Panels (Boundaries handled by its own component; History + You here) */
  .panel {
    background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
    box-shadow: var(--shadow); padding: 20px 22px; margin: 12px 0 6px;
  }
  .phead { display: flex; align-items: baseline; justify-content: space-between; }
  .phead h2 { font-size: 1.3rem; }
  .close { background: none; border: none; color: var(--accent-deep); font-size: 0.85rem; text-decoration: underline; }
  .reassure { margin: 4px 0 14px; color: var(--muted); font-size: 0.85rem; }
  .hist { list-style: none; margin: 0; padding: 0; }
  .hist li { padding: 8px 0; border-bottom: 1px solid var(--line); display: flex; gap: 12px; align-items: baseline; }
  .hist li:last-child { border-bottom: none; }
  .hist a { color: var(--ink); }
  .hist a:hover { color: var(--accent-deep); }
  .hsrc { margin-left: auto; color: var(--muted); font-size: 0.78rem; white-space: nowrap; }
  .hist .hx { background: none; border: none; color: var(--muted); font-size: 1.15rem; line-height: 1; cursor: pointer; padding: 0 2px; }
  .hist .hx:hover { color: var(--accent-deep); }
  .empty { margin: 0; color: var(--muted); font-style: italic; font-size: 0.85rem; }
  .reset { background: none; border: none; color: var(--muted); font-size: 0.82rem; text-decoration: underline; margin-top: 12px; }
  .reset:hover { color: var(--accent-deep); }

  .notice {
    text-align: center; color: var(--accent-deep); background: var(--accent-soft);
    border-radius: 999px; padding: 8px 16px; margin: 10px auto 0; width: fit-content; font-size: 0.86rem;
  }
  .rest { margin-top: 18px; }
  .center { text-align: center; }
  .pad { padding: 48px 0; }
  .endcap {
    text-align: center; color: var(--muted); font-family: var(--serif);
    font-style: italic; margin: 40px 0 10px; letter-spacing: 0.02em;
  }
</style>
