/* Nominations — "Albums we missed."
   A community-driven, voteable list of albums people think belong in the
   Hundred. Reuses the existing Supabase `comments` table for nominations:
   every nomination is a top-level comment stored under the reserved slug
   "__nominations", and the existing upvote mechanism doubles as the vote to
   include. The leaderboard sorts by net votes.

   Nominating and commenting require an account. Voting is open to everyone:
   logged-out visitors are deduped by an anonymous per-device key stored in
   localStorage (not ballot-proof, but stops casual double-voting). Votes live
   in a dedicated `nomination_votes` table, separate from the per-album
   `comment_votes` system so that remains untouched.

   The nomination's album data is stored as JSON in the comment body:
     {"t":"Title","a":"Artist","y":1979,"c":"https://cover...","p":"optional pitch"}
*/

const {
  useState: useNomState,
  useEffect: useNomEffect,
  useMemo: useNomMemo,
  useRef: useNomRef,
} = React;

const NOM_SLUG = "__nominations";

// Anonymous voting: a per-device id kept in localStorage. Lets logged-out
// visitors vote while still deduping casual double-votes. Not ballot-proof
// (clearing storage / new device resets it) — the pragmatic bar for this.
function nomVoterKey(me) {
  if (me && me.id) return "u:" + me.id;
  try {
    let k = localStorage.getItem("bbr:voter");
    if (!k) {
      k = "d:" + (crypto.randomUUID ? crypto.randomUUID() : (Date.now() + "-" + Math.random().toString(36).slice(2)));
      localStorage.setItem("bbr:voter", k);
    }
    return k;
  } catch (e) {
    // localStorage unavailable — fall back to a volatile session key.
    if (!window.__bbrVoterMem) window.__bbrVoterMem = "d:" + Math.random().toString(36).slice(2);
    return window.__bbrVoterMem;
  }
}

function parseNom(body) {
  try {
    const o = JSON.parse(body);
    if (o && (o.t || o.a)) return o;
  } catch (e) { /* legacy / malformed — fall through */ }
  return null;
}

function Nominations({ albums }) {
  const [me, setMe] = useNomState(null);
  const [noms, setNoms] = useNomState([]);          // [{id, name, nom, created_at}]
  const [votes, setVotes] = useNomState({});        // { id: tally }
  const [myVotes, setMyVotes] = useNomState({});    // { id: +1 }
  const [loading, setLoading] = useNomState(true);
  const [composing, setComposing] = useNomState(false);

  // nominate composer state
  const [query, setQuery] = useNomState("");
  const [searchResults, setSearchResults] = useNomState([]);
  const [searchBusy, setSearchBusy] = useNomState(false);
  const [picked, setPicked] = useNomState(null);    // {title, artist, year, coverUrl}
  const [pitch, setPitch] = useNomState("");
  const [posting, setPosting] = useNomState(false);
  const [note, setNote] = useNomState("");
  const searchTimer = useNomRef(null);

  // Set of titles already in the Hundred, to flag duplicates.
  const curatedKeys = useNomMemo(
    () => new Set((albums || []).map(a => (a.title + "|" + a.artist).toLowerCase())),
    [albums]
  );

  useNomEffect(() => {
    let active = true;
    if (window.BBR_auth) {
      window.BBR_auth.current().then((u) => { if (active) setMe(u || null); });
    }
    const off = window.BBR_auth && window.BBR_auth.onChange
      ? window.BBR_auth.onChange((u) => setMe(u || null))
      : null;
    return () => { active = false; if (typeof off === "function") off(); };
  }, []);

  async function loadAll() {
    if (!window.BBR_supabase) { setLoading(false); return; }
    try {
      const { data: cData } = await window.BBR_supabase
        .from("comments").select("*").eq("slug", NOM_SLUG)
        .order("created_at", { ascending: false });
      const list = (cData || [])
        .filter(c => !c.parent_id)
        .map(c => ({ id: c.id, name: c.name || "Member", created_at: c.created_at, nom: parseNom(c.body) }))
        .filter(c => c.nom);
      setNoms(list);

      const ids = list.map(c => c.id);
      const tally = {}, mine = {};
      const vkey = nomVoterKey(me);
      if (ids.length) {
        const { data: vData } = await window.BBR_supabase
          .from("nomination_votes").select("comment_id, value, voter_key").in("comment_id", ids);
        (vData || []).forEach(v => {
          tally[v.comment_id] = (tally[v.comment_id] || 0) + v.value;
          if (v.voter_key === vkey) mine[v.comment_id] = v.value;
        });
      }
      setVotes(tally);
      setMyVotes(mine);
    } catch (e) {
      console.error("[BBR] load nominations failed", e);
    } finally {
      setLoading(false);
    }
  }
  useNomEffect(() => { loadAll(); }, [me && me.id]);

  // Debounced catalogue search for the nominate composer.
  useNomEffect(() => {
    if (!composing) return;
    const q = query.trim();
    if (searchTimer.current) clearTimeout(searchTimer.current);
    if (q.length < 3) { setSearchResults([]); setSearchBusy(false); return; }
    setSearchBusy(true);
    searchTimer.current = setTimeout(() => {
      fetch("/api/search?q=" + encodeURIComponent(q))
        .then(r => r.json())
        .then(j => { setSearchResults(j.results || []); setSearchBusy(false); })
        .catch(() => { setSearchResults([]); setSearchBusy(false); });
    }, 350);
    return () => { if (searchTimer.current) clearTimeout(searchTimer.current); };
  }, [query, composing]);

  function requireMe() {
    if (!me) { if (window.__bbrRequireAuth) window.__bbrRequireAuth("nominate"); return false; }
    return true;
  }

  function startNominate() {
    if (!requireMe()) return;
    setComposing(true);
    setQuery(""); setSearchResults([]); setPicked(null); setPitch(""); setNote("");
  }

  async function submitNomination() {
    if (!picked) return;
    if (!requireMe()) return;
    // Block exact duplicates of albums already in the Hundred.
    if (curatedKeys.has((picked.title + "|" + picked.artist).toLowerCase())) {
      setNote("That one's already in the Hundred.");
      return;
    }
    // Block duplicate nominations — bump the existing one instead.
    const dupe = noms.find(n => n.nom &&
      (n.nom.t + "|" + n.nom.a).toLowerCase() === (picked.title + "|" + picked.artist).toLowerCase());
    if (dupe) {
      setNote("Already nominated — vote for it instead.");
      setComposing(false);
      return;
    }
    setPosting(true);
    try {
      const payload = JSON.stringify({
        t: picked.title, a: picked.artist,
        y: picked.year || null, c: picked.coverUrl || null,
        p: pitch.trim() || null,
      });
      const row = { slug: NOM_SLUG, user_id: me.id, name: me.name || "Member", body: payload, parent_id: null };
      const { data } = await window.BBR_supabase.from("comments").insert(row).select().single();
      if (data) {
        // Optimistically add + self-upvote.
        const newNom = { id: data.id, name: row.name, created_at: data.created_at, nom: JSON.parse(payload) };
        setNoms(prev => [newNom, ...prev]);
        await castVote(data.id, 1, true);
      }
      setComposing(false);
    } catch (e) {
      console.error("[BBR] nominate failed", e);
      setNote("Couldn't post that — try again.");
    } finally {
      setPosting(false);
    }
  }

  async function castVote(id, value, skipGuard) {
    // Voting is open to everyone (logged-out included) — no auth guard.
    const vkey = nomVoterKey(me);
    const existing = myVotes[id] || 0;
    const next = existing === value ? 0 : value;
    setMyVotes(prev => ({ ...prev, [id]: next }));
    setVotes(prev => ({ ...prev, [id]: (prev[id] || 0) - existing + next }));
    try {
      if (next === 0) {
        await window.BBR_supabase.from("nomination_votes")
          .delete().eq("comment_id", id).eq("voter_key", vkey);
      } else {
        await window.BBR_supabase.from("nomination_votes")
          .upsert({ comment_id: id, voter_key: vkey, value: next, user_id: (me && me.id) || null }, { onConflict: "comment_id,voter_key" });
      }
    } catch (e) {
      console.error("[BBR] nomination vote failed", e);
      loadAll();
    }
  }

  // Leaderboard: sort by net votes desc, then most recent.
  const ranked = useNomMemo(() => {
    return noms.slice().sort((a, b) => {
      const va = votes[a.id] || 0, vb = votes[b.id] || 0;
      if (vb !== va) return vb - va;
      return new Date(b.created_at) - new Date(a.created_at);
    });
  }, [noms, votes]);

  return (
    <section className="nom" id="nominations">
      <div className="nom-head">
        <div className="nom-eyebrow">§ The people's ballot</div>
        <h2 className="nom-title">Albums we missed</h2>
        <p className="nom-lede">
          The Hundred is a position, not the last word. Nominate a record you think belongs,
          and vote the contenders up. The most-wanted omissions are reviewed for the next edition.
        </p>
        <button className="nom-cta" onClick={startNominate}>
          + Nominate an album
        </button>
      </div>

      {loading ? (
        <div className="nom-empty">Loading the ballot…</div>
      ) : ranked.length === 0 ? (
        <div className="nom-empty">No nominations yet — be the first to put one forward.</div>
      ) : (
        <ol className="nom-list">
          {ranked.map((n, i) => {
            const score = votes[n.id] || 0;
            const mv = myVotes[n.id] || 0;
            return (
              <li className="nom-row" key={n.id}>
                <div className="nom-rank">{i + 1}</div>
                <div className="nom-cover">
                  {n.nom.c
                    ? <img src={n.nom.c} alt="" loading="lazy" onError={(e) => { e.target.style.visibility = "hidden"; }} />
                    : <div className="nom-cover-blank" />}
                </div>
                <div className="nom-meta">
                  <div className="nom-album">{n.nom.t}</div>
                  <div className="nom-artist">{n.nom.a}{n.nom.y ? " · " + n.nom.y : ""}</div>
                  {n.nom.p && <div className="nom-pitch">“{n.nom.p}”</div>}
                  <div className="nom-by">nominated by {n.name}</div>
                </div>
                <div className="nom-vote">
                  <button
                    className={"nom-up" + (mv === 1 ? " on" : "")}
                    onClick={() => castVote(n.id, 1)}
                    aria-label="Vote to include"
                    aria-pressed={mv === 1}
                  >
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 19V5M5 12l7-7 7 7" /></svg>
                  </button>
                  <span className="nom-score">{score}</span>
                </div>
              </li>
            );
          })}
        </ol>
      )}

      {composing && (
        <div className="nom-modal" onClick={(e) => { if (e.target.classList.contains("nom-modal")) setComposing(false); }}>
          <div className="nom-modal-card">
            <div className="nom-modal-head">
              <h3>Nominate an album</h3>
              <button className="nom-modal-x" onClick={() => setComposing(false)} aria-label="Close">×</button>
            </div>

            {!picked ? (
              <>
                <div className="nom-search">
                  <input
                    autoFocus
                    placeholder="Search any album by title or artist…"
                    value={query}
                    onChange={(e) => setQuery(e.target.value)}
                  />
                </div>
                <div className="nom-search-results">
                  {searchBusy && <div className="nom-empty">Searching…</div>}
                  {!searchBusy && searchResults.map(r => (
                    <button className="nom-result" key={r.id} onClick={() => { setPicked({ title: r.title, artist: r.artist, year: r.year, coverUrl: r.coverUrl }); setNote(""); }}>
                      <div className="nom-result-cover">
                        {r.coverUrl ? <img src={r.coverUrl} alt="" loading="lazy" onError={(e) => { e.target.style.visibility = "hidden"; }} /> : <div className="nom-cover-blank" />}
                      </div>
                      <div>
                        <div className="nom-result-t">{r.title}</div>
                        <div className="nom-result-a">{r.artist}{r.year ? " · " + r.year : ""}</div>
                      </div>
                    </button>
                  ))}
                  {!searchBusy && query.trim().length >= 3 && searchResults.length === 0 && (
                    <div className="nom-empty">No matches — try the artist and album title.</div>
                  )}
                </div>
              </>
            ) : (
              <div className="nom-confirm">
                <div className="nom-confirm-album">
                  <div className="nom-confirm-cover">
                    {picked.coverUrl ? <img src={picked.coverUrl} alt="" /> : <div className="nom-cover-blank" />}
                  </div>
                  <div>
                    <div className="nom-album">{picked.title}</div>
                    <div className="nom-artist">{picked.artist}{picked.year ? " · " + picked.year : ""}</div>
                    <button className="nom-change" onClick={() => setPicked(null)}>Change</button>
                  </div>
                </div>
                <label className="nom-pitch-label">Why does it belong? <span>(optional, one line)</span></label>
                <textarea
                  className="nom-pitch-input"
                  maxLength={180}
                  rows={2}
                  placeholder="Make the case in a sentence…"
                  value={pitch}
                  onChange={(e) => setPitch(e.target.value)}
                />
                <button className="nom-submit" onClick={submitNomination} disabled={posting}>
                  {posting ? "Posting…" : "Put it forward"}
                </button>
              </div>
            )}
            {note && <div className="nom-note">{note}</div>}
          </div>
        </div>
      )}
    </section>
  );
}

window.Nominations = Nominations;

/* ---------------------------------------------------------------------------
   ListDiscussion — a general discussion thread for the whole Hundred.
   Same comments table, reserved slug "__listtalk". Threaded comments + voting,
   no star ratings (those are for album pages). Posting requires sign-in; voting
   is anonymous via the same per-device key as nominations.
   --------------------------------------------------------------------------- */
const LISTTALK_SLUG = "__listtalk";

function ListDiscussion() {
  const [me, setMe] = useNomState(null);
  const [comments, setComments] = useNomState([]);
  const [votes, setVotes] = useNomState({});
  const [myVotes, setMyVotes] = useNomState({});
  const [loading, setLoading] = useNomState(true);
  const [draft, setDraft] = useNomState("");
  const [replyTo, setReplyTo] = useNomState(null);
  const [replyBody, setReplyBody] = useNomState("");
  const [posting, setPosting] = useNomState(false);

  useNomEffect(() => {
    let active = true;
    if (window.BBR_auth && window.BBR_auth.current) {
      window.BBR_auth.current().then((u) => { if (active) setMe(u || null); });
    }
    const off = window.BBR_auth && window.BBR_auth.onChange
      ? window.BBR_auth.onChange((u) => setMe(u || null))
      : null;
    return () => { active = false; if (typeof off === "function") off(); };
  }, []);

  async function loadAll() {
    if (!window.BBR_supabase) { setLoading(false); return; }
    try {
      const { data: cData } = await window.BBR_supabase
        .from("comments").select("*").eq("slug", LISTTALK_SLUG)
        .order("created_at", { ascending: true });
      const list = cData || [];
      setComments(list);
      const ids = list.map(c => c.id);
      const tally = {}, mine = {};
      const vkey = nomVoterKey(me);
      if (ids.length) {
        const { data: vData } = await window.BBR_supabase
          .from("nomination_votes").select("comment_id, value, voter_key").in("comment_id", ids);
        (vData || []).forEach(v => {
          tally[v.comment_id] = (tally[v.comment_id] || 0) + v.value;
          if (v.voter_key === vkey) mine[v.comment_id] = v.value;
        });
      }
      setVotes(tally); setMyVotes(mine);
    } catch (e) {
      console.error("[BBR] load list discussion failed", e);
    } finally { setLoading(false); }
  }
  useNomEffect(() => { loadAll(); }, [me && me.id]);

  function requireMe() {
    if (!me) { if (window.__bbrRequireAuth) window.__bbrRequireAuth("comment"); return false; }
    return true;
  }

  async function post(parentId) {
    const body = (parentId ? replyBody : draft).trim();
    if (!body) return;
    if (!requireMe()) return;
    setPosting(true);
    try {
      const row = { slug: LISTTALK_SLUG, user_id: me.id, name: me.name || "Member", body, rating: null, parent_id: parentId || null };
      const { data } = await window.BBR_supabase.from("comments").insert(row).select().single();
      if (data) setComments(prev => [...prev, data]);
      if (parentId) { setReplyBody(""); setReplyTo(null); } else { setDraft(""); }
    } catch (e) {
      console.error("[BBR] list comment failed", e);
    } finally { setPosting(false); }
  }

  async function castVote(id, value) {
    const vkey = nomVoterKey(me);
    const existing = myVotes[id] || 0;
    const next = existing === value ? 0 : value;
    setMyVotes(prev => ({ ...prev, [id]: next }));
    setVotes(prev => ({ ...prev, [id]: (prev[id] || 0) - existing + next }));
    try {
      if (next === 0) {
        await window.BBR_supabase.from("nomination_votes").delete().eq("comment_id", id).eq("voter_key", vkey);
      } else {
        await window.BBR_supabase.from("nomination_votes")
          .upsert({ comment_id: id, voter_key: vkey, value: next, user_id: (me && me.id) || null }, { onConflict: "comment_id,voter_key" });
      }
    } catch (e) { console.error("[BBR] list vote failed", e); loadAll(); }
  }

  const topLevel = comments.filter(c => !c.parent_id);
  const repliesByParent = {};
  comments.filter(c => c.parent_id).forEach(r => {
    (repliesByParent[r.parent_id] = repliesByParent[r.parent_id] || []).push(r);
  });
  // Sort top-level by net votes then recency.
  const sorted = topLevel.slice().sort((a, b) => {
    const va = votes[a.id] || 0, vb = votes[b.id] || 0;
    if (vb !== va) return vb - va;
    return new Date(a.created_at) - new Date(b.created_at);
  });

  function Row({ c, isReply }) {
    const score = votes[c.id] || 0;
    const mv = myVotes[c.id] || 0;
    return (
      <li className={isReply ? "ld-c ld-reply" : "ld-c"}>
        <div className="ld-body">
          <div className="ld-meta"><span className="ld-name">{c.name || "Member"}</span></div>
          <div className="ld-text">{c.body}</div>
          <div className="ld-actions">
            <span className="ld-votes">
              <button className={"ld-vote up" + (mv === 1 ? " on" : "")} onClick={() => castVote(c.id, 1)} aria-label="Like" aria-pressed={mv === 1}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill={mv === 1 ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2"><path d="M7 10v11H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h3zm0 0l5-7a2 2 0 0 1 2 2v3h5a2 2 0 0 1 2 2.4l-1.5 7A2 2 0 0 1 16.5 21H7"/></svg>
              </button>
              <span className="ld-score">{score}</span>
              <button className={"ld-vote down" + (mv === -1 ? " on" : "")} onClick={() => castVote(c.id, -1)} aria-label="Dislike" aria-pressed={mv === -1}>
                <svg width="14" height="14" viewBox="0 0 24 24" fill={mv === -1 ? "currentColor" : "none"} stroke="currentColor" strokeWidth="2"><path d="M17 14V3h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-3zm0 0l-5 7a2 2 0 0 1-2-2v-3H5a2 2 0 0 1-2-2.4l1.5-7A2 2 0 0 1 7.5 3H17"/></svg>
              </button>
            </span>
            {!isReply && (
              <button className="ld-reply-btn" onClick={() => { setReplyTo(replyTo === c.id ? null : c.id); setReplyBody(""); }}>
                {replyTo === c.id ? "Cancel" : "Reply"}
              </button>
            )}
          </div>
          {replyTo === c.id && (
            <div className="ld-reply-box">
              <textarea rows={2} placeholder="Add a reply…" value={replyBody} onChange={(e) => setReplyBody(e.target.value)} />
              <button className="ld-post" onClick={() => post(c.id)} disabled={posting || !replyBody.trim()}>{posting ? "Posting…" : "Reply"}</button>
            </div>
          )}
          {(repliesByParent[c.id] || []).map(r => <ul className="ld-replies" key={r.id}><Row c={r} isReply /></ul>)}
        </div>
      </li>
    );
  }

  return (
    <section className="ld" id="list-discussion">
      <div className="ld-head">
        <div className="nom-eyebrow">§ The floor is open</div>
        <h2 className="nom-title">Discuss the list</h2>
        <p className="nom-lede">Argue the order, defend a snub, or tell us what we got right. Like the takes you agree with.</p>
      </div>
      <div className="ld-composer">
        <textarea
          rows={3}
          placeholder={me ? "Say your piece on the Hundred…" : "Sign in to join the discussion…"}
          value={draft}
          onChange={(e) => setDraft(e.target.value)}
          onFocus={() => { if (!me && window.__bbrRequireAuth) window.__bbrRequireAuth("comment"); }}
        />
        <button className="ld-post" onClick={() => post(null)} disabled={posting || !draft.trim()}>{posting ? "Posting…" : "Post comment"}</button>
      </div>
      {loading ? (
        <div className="nom-empty">Loading the discussion…</div>
      ) : sorted.length === 0 ? (
        <div className="nom-empty">No comments yet — start the conversation.</div>
      ) : (
        <ul className="ld-list">{sorted.map(c => <Row c={c} key={c.id} />)}</ul>
      )}
    </section>
  );
}

window.ListDiscussion = ListDiscussion;
