// ===========================================================
// TodayPage — /today, a stable URL for the one thing here with a daily cadence.
//
// Album of the Day had an email and (since 2 Aug) a homepage card, but no
// destination: nothing to put in a bio link, nothing for the email to point at
// beyond a specific essay, and no page that answers "what's today's record?" for
// somebody who has not signed up. That is what this is. It is deliberately a
// visit target, not an essay — the essay is one click away.
//
// The pick MUST agree with the morning email and the homepage card, so it calls
// bbrAlbumOfDay() from TodayCard.jsx rather than recomputing the rotation. If that
// file has not loaded yet the page renders nothing rather than a different record.
//
// The countdown is driven by setInterval and recomputed from Date() on every tick,
// so a throttled or backgrounded tab self-corrects when it wakes. It is decoration:
// the page is complete and correct without it ever firing, which is the lesson from
// the homepage counters that froze in background tabs.
// ===========================================================
const { useState: useTpState, useEffect: useTpEffect, useRef: useTpRef } = React;

// ---- the sticky add bar (mobile) -------------------------------------------
// Measured on the live site, 15 Aug 2026, at 375x812: "Add №N to my shelf" sits at
// pageY 1070 — 1.32 screens down, behind ~400px of preamble and a 320px cover. The
// single action this page exists for could not be reached without a scroll, on the
// viewport the only channel with an audience (TikTok) arrives from.
//
// This does NOT reimplement the add. It renders the same <TodayAdd compact/>, so
// owned-state, the guest cap, the saving state and the error copy all keep living in
// one place. TodayAdd deliberately holds no "added" state of its own and reads
// store.ownedSlugV2 instead; copying that logic here is precisely how the two
// surfaces would drift apart the first time either changed.
//
// It shows only while the inline add is off-screen, so the same button is never on
// screen twice, and only for a record not already owned when the collection settled —
// a bar telling a returning collector something is already "on your shelf" is
// furniture, not confirmation. When they DO add from the bar, TodayAdd swaps itself
// for its own ✓ line in place, so the bar becomes the receipt rather than vanishing
// at the instant of success.
//
// No collision to manage with the other fixed bar: FRB_ROUTES in App.jsx does not
// include "today", so .frb never runs here. It does set a body class the same way,
// because the floating feedback launcher lifts off whatever bar is present.
function TodayStickyAdd({ album, onRequireAuth, watch }) {
  const store = window.BBR_store;
  const [inView, setInView] = useTpState(true);
  const [ownedAtLoad, setOwnedAtLoad] = useTpState(null);
  const [, force] = useTpState(0);

  useTpEffect(() => (store ? store.subscribe(() => force((n) => n + 1)) : undefined), []);

  const loaded = !!(store && store.collectionV2Loaded);
  const owned = !!(store && store.ownedSlugV2 && album && store.ownedSlugV2(album.slug));

  // Settle "did they already have this" only once the collection has actually
  // resolved. A member's load is async, so reading it earlier returns false for
  // everybody and the bar would offer to add a record that is already on the shelf.
  // Guests settle synchronously off localStorage, and the store sets the same flag
  // for them, so one gate covers both.
  useTpEffect(() => {
    if (loaded && ownedAtLoad === null) setOwnedAtLoad(owned);
  }, [loaded, owned, ownedAtLoad]);

  // Watch the inline add ITSELF rather than a fixed scroll offset, so this stays
  // correct when the copy above it changes length — the hook, the streak line and
  // the standfirst all vary per record and per signed-in state. IntersectionObserver
  // rather than a scroll listener: it also catches the element moving for reasons
  // that are not a scroll (the cover finishing loading, a font swap, the streak line
  // appearing when the store resolves), which a scroll handler misses entirely.
  //
  // Where there is no IntersectionObserver the bar simply never shows and the page
  // is exactly what it is today — the inline button, reachable by scrolling.
  useTpEffect(() => {
    const el = watch && watch.current;
    if (!el || typeof IntersectionObserver !== "function") return;
    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { threshold: 0 });
    io.observe(el);
    return () => io.disconnect();
  }, [watch, album && album.slug, owned]);

  const showing = !!album && loaded && ownedAtLoad === false && !inView;

  useTpEffect(() => {
    document.body.classList.toggle("tsb-active", showing);
    return () => document.body.classList.remove("tsb-active");
  }, [showing]);

  if (!showing || !window.TodayAdd) return null;
  return (
    <div className="tsb" role="region" aria-label={"Add " + album.title + " to your shelf"}>
      <div className="tsb-in">
        <img className="tsb-cover" src={"/covers/" + album.slug + ".jpg"} alt="" width="36" height="36" />
        <TodayAdd album={album} onRequireAuth={onRequireAuth} compact />
      </div>
    </div>
  );
}

// Milliseconds until the next UTC midnight, when the rotation advances.
function bbrMsToRollover() {
  const n = new Date();
  const next = Date.UTC(n.getUTCFullYear(), n.getUTCMonth(), n.getUTCDate() + 1);
  return Math.max(0, next - n.getTime());
}

function bbrCountdownLabel(ms) {
  const total = Math.floor(ms / 1000);
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  if (h >= 1) return h + "h " + String(m).padStart(2, "0") + "m";
  if (m >= 1) return m + "m";
  return "moments";
}

function TodayPage({ onAlbum, onHome, onList, onRequireAuth, user }) {
  const store = window.BBR_store;
  const [, force] = useTpState(0);
  const [left, setLeft] = useTpState(bbrMsToRollover());
  const addRef = useTpRef(null);

  useTpEffect(() => (store ? store.subscribe(() => force((n) => n + 1)) : undefined), []);

  useTpEffect(() => {
    // Recomputed from the clock each tick, so this survives being throttled.
    const t = setInterval(() => setLeft(bbrMsToRollover()), 30000);
    return () => clearInterval(t);
  }, []);

  const pick = window.bbrAlbumOfDay;
  const album = pick ? pick(0) : null;
  if (!album) return null;

  const visit = store && store.visit;
  // >= 1, matching TodayCard: touch_visit() writes streak_days = 1 on a fresh
  // visit, so day one is a real streak and the two surfaces must not disagree.
  const streak = visit && visit.streakDays >= 1 ? visit.streakDays : 0;
  const essay = (window.ESSAYS_FULL && window.ESSAYS_FULL[album.slug]) || {};
  const hook = (window.BLURBS && window.BLURBS[album.rank]) ||
    (essay.essay && essay.essay.dek) || "";

  // The four days behind us. Not the days ahead — knowing tomorrow's record is
  // the one thing that would remove the reason to come back tomorrow.
  const recent = [];
  for (let d = 1; d <= 4; d++) {
    const a = pick(-d);
    if (a) recent.push(a);
  }

  const dateLabel = new Date().toLocaleDateString("en-GB", {
    weekday: "long", day: "numeric", month: "long", timeZone: "UTC",
  });

  return (
    <main className="tdp">
      <div className="tdp-inner">
        <nav className="tdp-crumb" aria-label="Breadcrumb">
          <a href="/" onClick={(e) => { if (!e.metaKey && !e.ctrlKey && e.button === 0) { e.preventDefault(); onHome && onHome(); } }}>The Lead-In</a>
          <span aria-hidden="true"> / </span>
          <span>Today</span>
        </nav>

        <header className="tdp-head">
          <div className="tdp-kicker">
            Today from the canon
            <span className="tdp-date"> · {dateLabel}</span>
          </div>
          <h1 className="tdp-title">
            One record a day, out of the hundred.
          </h1>
          <p className="tdp-standfirst">
            The same record as this morning&rsquo;s email, chosen by the date rather than by us,
            so everybody reading is on the same one. It changes in <strong>{bbrCountdownLabel(left)}</strong>.
          </p>
        </header>

        <section className="tdp-pick" aria-labelledby="tdp-pick-h">
          <div className="tdp-cover">
            <img src={"/covers/" + album.slug + ".jpg"}
                 alt={album.title + " — " + album.artist}
                 width="320" height="320" />
          </div>
          <div className="tdp-meta">
            <div className="tdp-rank">&#8470;{album.rank} of the hundred</div>
            <h2 className="tdp-album" id="tdp-pick-h">{album.title}</h2>
            <div className="tdp-artist">{album.artist} · {album.year}{album.genre ? " · " + album.genre : ""}</div>
            {hook ? <p className="tdp-hook">{hook}</p> : null}
            <div className="tdp-actions">
              <button className="tdp-cta" onClick={() => onAlbum && onAlbum(album.slug)}>
                Read the case for &#8470;{album.rank} &rarr;
              </button>
              <button className="tdp-cta2" onClick={() => onList && onList()}>
                See all 100
              </button>
            </div>
            {/* Wrapped so the sticky bar can watch this exact element for visibility.
                .tdp-meta is a plain block container and .tda carries its own
                margin-top, so the wrapper changes no layout. */}
            <div ref={addRef}>
              {window.TodayAdd ? <TodayAdd album={album} onRequireAuth={onRequireAuth} /> : null}
            </div>
            {streak >= 2 ? (
              <div className="tdp-streak" title="Consecutive days you have visited">
                You&rsquo;re on a <strong>{streak}-day streak</strong>. Come back tomorrow to keep it.
              </div>
            ) : streak === 1 ? (
              /* Day one counts. This page used to fall through to "a streak starts
                 counting here" whenever streak_days was 1, which is the day it
                 already started — and now contradicts the homepage card, which
                 states "1 day in a row" from the same value. */
              <div className="tdp-streak" title="Consecutive days you have visited">
                That&rsquo;s <strong>day one</strong>. Come back tomorrow to make it two.
              </div>
            ) : user ? (
              <div className="tdp-streak">
                Visit tomorrow and a streak starts counting here.
              </div>
            ) : (
              <div className="tdp-streak">
                Sign in and this page starts counting your streak.
              </div>
            )}
          </div>
        </section>

        {/* The notification ask lives here rather than in settings alone: this is the
            page a returning collector opens, so it is where an offer to interrupt
            them is least unwelcome. Asked once — it remembers "Not now". */}
        {window.PushOptIn ? <PushOptIn variant="inline" user={user} /> : null}

        {recent.length ? (
          <section className="tdp-recent" aria-labelledby="tdp-recent-h">
            <h2 className="tdp-recent-h" id="tdp-recent-h">The last few days</h2>
            <ul className="tdp-recent-list">
              {recent.map((a, i) => (
                <li key={a.slug}>
                  <a href={"/album/" + a.slug}
                     onClick={(e) => { if (!e.metaKey && !e.ctrlKey && e.button === 0) { e.preventDefault(); onAlbum && onAlbum(a.slug); } }}>
                    <img src={"/covers/" + a.slug + ".jpg"} alt="" width="64" height="64" loading="lazy" />
                    <span className="tdp-recent-when">{i === 0 ? "Yesterday" : i + 1 + " days ago"}</span>
                    <span className="tdp-recent-title">{a.title}</span>
                    <span className="tdp-recent-artist">{a.artist}</span>
                  </a>
                </li>
              ))}
            </ul>
          </section>
        ) : null}
      </div>
      <TodayStickyAdd album={album} onRequireAuth={onRequireAuth} watch={addRef} />
    </main>
  );
}

Object.assign(window, { TodayPage, TodayStickyAdd });
