/* ============================================================
   Admin dashboard — Overview / Library / Users / Shares / Upload
   Only rendered when ctx.me?.isAdmin === true.
   ============================================================ */

const { useState: useStateAdm, useEffect: useEffectAdm, useRef: useRefAdm, useMemo: useMemoAdm, useCallback: useCallbackAdm } = React;

// ---------------------------------------------------------------
// formatters
// ---------------------------------------------------------------
function fmtRel(ts) {
  if (!ts) return "never";
  const now = Date.now();
  const d = (now - ts) / 1000;
  if (d < 60) return "just now";
  if (d < 3600) return Math.floor(d / 60) + " min ago";
  if (d < 86400) return Math.floor(d / 3600) + " h ago";
  if (d < 86400 * 30) return Math.floor(d / 86400) + " d ago";
  return new Date(ts).toLocaleDateString();
}
function fmtDur(secs) {
  const m = Math.floor((secs || 0) / 60);
  if (m < 60) return m + " min";
  const h = Math.floor(m / 60);
  return h + "h " + (m % 60) + "m";
}
function fmtN(n) {
  if (n == null) return "—";
  if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k";
  return String(n);
}

// ---------------------------------------------------------------
// Top-level
// ---------------------------------------------------------------
function AdminView({ ctx, section = "overview" }) {
  const [data, setData] = useStateAdm(null);
  const [loading, setLoading] = useStateAdm(true);
  const [err, setErr] = useStateAdm("");

  const load = useCallbackAdm(async () => {
    setLoading(true); setErr("");
    try {
      const res = await fetch("/api/admin/dashboard", { credentials: "include" });
      if (!res.ok) throw new Error("HTTP " + res.status);
      setData(await res.json());
    } catch (e) {
      setErr(e.message);
    }
    setLoading(false);
  }, []);
  useEffectAdm(() => { load(); }, [load]);

  const tabs = [
    { id: "overview", label: "Overview" },
    { id: "library",  label: "Library" },
    { id: "users",    label: "Users" },
    { id: "shares",   label: "Shares" },
    { id: "upload",   label: "Upload" },
  ];

  return (
    <div data-screen-label="Admin">
      <section className="section" style={{ marginTop: 8 }}>
        <div className="section-head">
          <h2 className="section-title">Admin <em>dashboard</em></h2>
          <button
            onClick={load}
            disabled={loading}
            style={{
              fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase",
              color: "var(--ink-3)", fontWeight: 700,
            }}
          >{loading ? "Refreshing…" : "Refresh"}</button>
        </div>
        <div className="chip-row" style={{ marginBottom: 24 }}>
          {tabs.map(t => (
            <button
              key={t.id}
              className={"chip " + (section === t.id ? "on" : "")}
              onClick={() => ctx.setView({ kind: "admin", section: t.id })}
            >{t.label}</button>
          ))}
        </div>

        {err && <div style={{ color: "var(--pink)", padding: 12 }}>Could not load dashboard: {err}</div>}
        {!data && loading && <div style={{ color: "var(--ink-3)" }}>Loading…</div>}

        {data && section === "overview" && <Overview data={data} ctx={ctx} />}
        {data && section === "library"  && <LibrarySection data={data} ctx={ctx} reload={load} />}
        {data && section === "users"    && <UsersSection data={data} reload={load} />}
        {data && section === "shares"   && <SharesSection data={data} ctx={ctx} reload={load} />}
        {section === "upload" && <AdminUpload onUploaded={load} />}
      </section>
    </div>
  );
}

// ---------------------------------------------------------------
// Overview
// ---------------------------------------------------------------
function Overview({ data, ctx }) {
  const o = data.overview;
  const cards = [
    { label: "Members",        value: fmtN(o.userCount),        sub: o.adminCount + " admin" },
    { label: "Albums",         value: fmtN(o.albumCount),       sub: o.trackCount + " tracks" },
    { label: "Library length", value: fmtDur(o.totalDurationSec), sub: "" },
    { label: "Total plays",    value: fmtN(o.totalPlays),       sub: "" },
    { label: "Active shares",  value: fmtN(o.shareCount),       sub: o.publicShareCount + " public" },
    { label: "Pending invites",value: fmtN(o.pendingInvites),   sub: "" },
  ];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 28 }}>
      <div style={{
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
        gap: 12,
      }}>
        {cards.map((c, i) => (
          <div key={i} style={{
            background: "var(--bg-2)",
            border: "1px solid var(--line)",
            borderRadius: 14,
            padding: "18px 20px",
          }}>
            <div style={{ fontSize: 10, letterSpacing: "0.24em", textTransform: "uppercase", color: "var(--ink-4)", fontWeight: 800 }}>
              {c.label}
            </div>
            <div style={{ fontFamily: "var(--f-display)", fontWeight: 900, fontSize: 36, marginTop: 4, color: "var(--ink)" }}>
              {c.value}
            </div>
            {c.sub && <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 2 }}>{c.sub}</div>}
          </div>
        ))}
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }}>
        <div>
          <h3 style={sectionH3}>Top tracks</h3>
          {data.topTracks.length === 0 && <div style={mutedRow}>Nothing played yet.</div>}
          <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
            {data.topTracks.map((t, i) => (
              <div key={t.id} style={listRow}>
                <span style={{ width: 22, color: "var(--ink-4)", fontFamily: "var(--f-mono)", fontSize: 12 }}>{i + 1}</span>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 600, fontSize: 14, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.title}</div>
                  <div style={{ fontSize: 11, color: "var(--ink-3)" }}>{t.artist} · {t.albumTitle}</div>
                </div>
                <span style={{ fontFamily: "var(--f-mono)", fontSize: 12, color: t.plays > 0 ? "var(--gold)" : "var(--ink-4)" }}>{t.plays} plays</span>
              </div>
            ))}
          </div>
        </div>
        <div>
          <h3 style={sectionH3}>Recent activity</h3>
          {data.activity.length === 0 && <div style={mutedRow}>No plays yet.</div>}
          <div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
            {data.activity.map((a, i) => (
              <div key={i} style={listRow}>
                <div style={{ width: 28, height: 28, borderRadius: 999, background: "var(--bg-3)", color: "var(--ink-2)", display: "grid", placeItems: "center", fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 12, flexShrink: 0 }}>
                  {(a.username || "?")[0].toUpperCase()}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    <strong style={{ color: "var(--ink)" }}>{a.username}</strong>
                    <span style={{ color: "var(--ink-3)" }}> played </span>
                    <span>{a.track?.title || a.trackId}</span>
                  </div>
                  <div style={{ fontSize: 11, color: "var(--ink-4)" }}>{a.track?.artist} · {fmtRel(a.ts)}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// Library section — edit / delete albums, browse tracks with stats
// ---------------------------------------------------------------
function LibrarySection({ data, ctx, reload }) {
  const [editing, setEditing] = useStateAdm(null); // albumId
  const [draft, setDraft] = useStateAdm({});

  async function deleteAlbum(a) {
    if (!confirm(`Delete "${a.title}" and all ${a.trackCount} track files from R2? This cannot be undone.`)) return;
    const res = await fetch("/api/admin/album/" + encodeURIComponent(a.id), { method: "DELETE", credentials: "include" });
    if (res.ok) reload();
    else alert("Delete failed: " + (await res.text()));
  }
  function startEdit(a) {
    setEditing(a.id);
    setDraft({ title: a.title, artist: a.artist, year: a.year, isSingle: a.isSingle });
  }
  async function saveEdit(a) {
    const res = await fetch("/api/admin/album/" + encodeURIComponent(a.id), {
      method: "PATCH",
      credentials: "include",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(draft),
    });
    if (res.ok) { setEditing(null); reload(); }
    else alert("Save failed: " + (await res.text()));
  }

  if (data.albums.length === 0) {
    return <div style={mutedRow}>No albums yet. Use the Upload tab.</div>;
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      {data.albums.map(a => (
        <div key={a.id} style={{
          background: "var(--bg-2)",
          border: "1px solid var(--line)",
          borderRadius: 14,
          padding: 14,
        }}>
          <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
            <div style={{ width: 56, height: 56, borderRadius: 8, overflow: "hidden", background: "var(--bg-1)", flex: "0 0 56px" }}>
              {a.hasCover ? <img src={`/api/cover/${a.id}`} alt="" style={{ width: "100%", height: "100%", objectFit: "cover" }} /> : null}
            </div>
            {editing === a.id ? (
              <div style={{ flex: 1, display: "grid", gridTemplateColumns: "1.2fr 1fr 80px", gap: 8 }}>
                <input value={draft.title || ""} onChange={e => setDraft(d => ({ ...d, title: e.target.value }))} style={inputStyle} />
                <input value={draft.artist || ""} onChange={e => setDraft(d => ({ ...d, artist: e.target.value }))} style={inputStyle} />
                <input type="number" value={draft.year || 0} onChange={e => setDraft(d => ({ ...d, year: parseInt(e.target.value, 10) || 0 }))} style={inputStyle} />
              </div>
            ) : (
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 18, textTransform: "uppercase", letterSpacing: "0.005em" }}>{a.title}</div>
                <div style={{ fontSize: 13, color: "var(--ink-2)" }}>{a.artist} · {a.year}</div>
                <div style={{ fontSize: 11, color: "var(--ink-4)" }}>
                  {a.trackCount} tracks · {fmtDur(a.duration)} · <strong style={{ color: a.plays > 0 ? "var(--gold)" : "var(--ink-4)" }}>{a.plays} plays</strong>
                </div>
              </div>
            )}
            <div style={{ display: "flex", gap: 6 }}>
              <button
                onClick={() => ctx.openShare({ kind: "album", id: a.id, label: a.title + " — " + a.artist })}
                style={ghostBtnSmall}
              >Share</button>
              {editing === a.id ? (
                <>
                  <button onClick={() => saveEdit(a)} style={pinkBtnSmall}>Save</button>
                  <button onClick={() => setEditing(null)} style={ghostBtnSmall}>Cancel</button>
                </>
              ) : (
                <>
                  <button onClick={() => startEdit(a)} style={ghostBtnSmall}>Edit</button>
                  <button onClick={() => deleteAlbum(a)} style={{ ...ghostBtnSmall, color: "var(--pink)" }}>Delete</button>
                </>
              )}
            </div>
          </div>
          <details style={{ marginTop: 10 }}>
            <summary style={{ fontSize: 12, color: "var(--ink-3)", cursor: "pointer" }}>
              {a.trackCount} tracks
            </summary>
            <div style={{ marginTop: 8 }}>
              {a.tracks.map((t, i) => (
                <div key={t.id} style={{ display: "flex", gap: 10, alignItems: "center", padding: "6px 4px", borderBottom: "1px dashed var(--line)" }}>
                  <span style={{ width: 22, color: "var(--ink-4)", fontFamily: "var(--f-mono)", fontSize: 11 }}>{(i+1).toString().padStart(2, "0")}</span>
                  <span style={{ flex: 1, fontSize: 13 }}>{t.title}</span>
                  <span style={{ fontSize: 10, letterSpacing: "0.12em", color: "var(--ink-4)", fontWeight: 700 }}>
                    {t.hasMaster ? "MAX " : ""}{t.hasHigh ? "HIGH" : ""}
                  </span>
                  <span style={{ fontFamily: "var(--f-mono)", fontSize: 11, color: t.plays > 0 ? "var(--gold)" : "var(--ink-4)", minWidth: 60, textAlign: "right" }}>
                    {t.plays} {t.plays === 1 ? "play" : "plays"}
                  </span>
                  <span style={{ fontFamily: "var(--f-mono)", fontSize: 11, color: "var(--ink-3)", minWidth: 40, textAlign: "right" }}>
                    {Math.floor(t.d/60)}:{(t.d%60).toString().padStart(2, "0")}
                  </span>
                </div>
              ))}
            </div>
          </details>
        </div>
      ))}
    </div>
  );
}

// ---------------------------------------------------------------
// Users section — invite, list with stats, revoke pending invites
// ---------------------------------------------------------------
function UsersSection({ data, reload }) {
  const [invite, setInvite] = useStateAdm(null);
  const [busy, setBusy] = useStateAdm(false);
  const [err, setErr] = useStateAdm("");
  const [copied, setCopied] = useStateAdm(false);

  async function createInvite(asAdmin) {
    setBusy(true); setErr(""); setCopied(false);
    try {
      const res = await fetch("/api/admin/invite", {
        method: "POST", credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ asAdmin }),
      });
      if (!res.ok) {
        const j = await res.json().catch(() => ({}));
        setErr(j.error || "Could not create invite");
      } else {
        setInvite(await res.json());
        reload();
      }
    } catch (e) { setErr("Network error"); }
    setBusy(false);
  }
  async function copyInvite() {
    if (!invite?.url) return;
    try { await navigator.clipboard.writeText(invite.url); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (e) {}
  }
  async function nativeShare() {
    if (!invite?.url) return;
    if (navigator.share) {
      try { await navigator.share({ title: "Discothèque invite", url: invite.url }); return; } catch (e) {}
    }
    copyInvite();
  }
  async function del(username) {
    if (!confirm(`Delete user "${username}"?`)) return;
    const res = await fetch("/api/admin/users/" + encodeURIComponent(username), { method: "DELETE", credentials: "include" });
    if (res.ok) reload();
  }

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
      <div>
        <h3 style={sectionH3}>Invite a member</h3>
        <div style={{ display: "flex", gap: 10, flexWrap: "wrap" }}>
          <button onClick={() => createInvite(false)} disabled={busy} style={pinkBtnStyle}>+ New invite link</button>
          <button onClick={() => createInvite(true)}  disabled={busy} style={ghostBtnStyle}>+ New admin invite</button>
        </div>
        {err && <div style={{ color: "var(--pink)", fontSize: 12, marginTop: 8 }}>{err}</div>}
        {invite && (
          <div style={{
            marginTop: 14, padding: 16,
            background: "var(--bg-2)",
            border: "1px solid " + (invite.asAdmin ? "rgba(245,193,75,0.4)" : "var(--line)"),
            borderRadius: 12, maxWidth: 700,
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
              <span style={{
                fontFamily: "var(--f-display)", fontSize: 10, fontWeight: 800,
                letterSpacing: "0.18em", textTransform: "uppercase",
                color: invite.asAdmin ? "var(--gold)" : "var(--pink)",
              }}>{invite.asAdmin ? "Admin invite" : "Invite"}</span>
              <span style={{ fontSize: 11, color: "var(--ink-4)" }}>Expires in 7 days · works once</span>
            </div>
            <div style={urlBoxStyle}>{invite.url}</div>
            <div style={{ display: "flex", gap: 8, marginTop: 10 }}>
              <button onClick={copyInvite} style={{ ...pinkBtnStyle, background: copied ? "var(--gold)" : "var(--pink)" }}>
                {copied ? "✓ Copied" : "Copy link"}
              </button>
              {typeof navigator !== "undefined" && navigator.share && (
                <button onClick={nativeShare} style={ghostBtnStyle}>Share…</button>
              )}
            </div>
          </div>
        )}
      </div>

      {data.invites.length > 0 && (
        <div>
          <h3 style={sectionH3}>Pending invites ({data.invites.length})</h3>
          <div style={{ display: "flex", flexDirection: "column", gap: 6, maxWidth: 700 }}>
            {data.invites.map(inv => (
              <div key={inv.token} style={pendingRowStyle}>
                <span style={{
                  fontFamily: "var(--f-display)", fontSize: 10, fontWeight: 800,
                  letterSpacing: "0.18em", textTransform: "uppercase",
                  color: inv.asAdmin ? "var(--gold)" : "var(--pink)",
                  padding: "3px 8px", borderRadius: 999,
                  border: "1px solid " + (inv.asAdmin ? "rgba(245,193,75,0.4)" : "rgba(255,61,154,0.4)"),
                }}>{inv.asAdmin ? "admin" : "user"}</span>
                <div style={{ flex: 1, fontSize: 12, color: "var(--ink-3)", fontFamily: "var(--f-mono)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                  /invite/{inv.token}
                </div>
                <span style={{ fontSize: 11, color: "var(--ink-4)" }}>
                  expires {fmtRel(inv.exp * 1000)}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      <div>
        <h3 style={sectionH3}>Members ({data.users.length})</h3>
        <div style={{ display: "flex", flexDirection: "column", gap: 6, maxWidth: 700 }}>
          {data.users.map(u => (
            <div key={u.username} style={userRowStyle}>
              <div style={{
                width: 36, height: 36, borderRadius: 999, flexShrink: 0,
                background: u.isAdmin ? "linear-gradient(135deg, var(--gold), var(--pink))" : "var(--bg-3)",
                color: "var(--bg-0)",
                display: "grid", placeItems: "center",
                fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 14,
              }}>{u.username[0].toUpperCase()}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                  <span style={{ fontWeight: 600, fontSize: 14 }}>{u.username}</span>
                  {u.isAdmin && <span style={{ fontSize: 9, letterSpacing: "0.18em", color: "var(--gold)", textTransform: "uppercase", fontWeight: 800 }}>Admin</span>}
                </div>
                <div style={{ fontSize: 11, color: "var(--ink-4)" }}>
                  {u.plays || 0} plays · last seen {fmtRel(u.lastSeen)}
                  {u.invitedBy && <span> · invited by {u.invitedBy}</span>}
                </div>
              </div>
              {!u.isAdmin && (
                <button onClick={() => del(u.username)} style={{ ...ghostBtnSmall, color: "var(--pink)" }}>Remove</button>
              )}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------
// Shares section — list, revoke
// ---------------------------------------------------------------
function SharesSection({ data, ctx, reload }) {
  async function revoke(token) {
    if (!confirm("Revoke this share link? It will stop working immediately.")) return;
    const res = await fetch("/api/admin/shares/" + encodeURIComponent(token), { method: "DELETE", credentials: "include" });
    if (res.ok) reload();
  }
  async function copy(token) {
    const url = window.location.origin + "/s/" + token;
    try { await navigator.clipboard.writeText(url); } catch (e) {}
  }

  if (data.shares.length === 0) {
    return <div style={mutedRow}>No active share links.</div>;
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 6, maxWidth: 760 }}>
      {data.shares.map(s => {
        const target = s.kind === "album"
          ? data.albums.find(a => a.id === s.id)?.title
          : data.topTracks.find(t => t.id === s.id)?.title
            || data.albums.flatMap(a => a.tracks).find(t => t.id === s.id)?.title;
        return (
          <div key={s.token} style={shareRowStyle}>
            <div style={{
              fontFamily: "var(--f-display)", fontSize: 10, fontWeight: 800,
              letterSpacing: "0.18em", textTransform: "uppercase",
              color: s.public ? "var(--gold)" : "var(--pink)",
              padding: "3px 8px", borderRadius: 999,
              border: "1px solid " + (s.public ? "rgba(245,193,75,0.4)" : "rgba(255,61,154,0.4)"),
              flexShrink: 0,
            }}>{s.public ? "public" : "private"}</div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 13, fontWeight: 600, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                {s.kind === "album" ? "Album: " : "Track: "}{target || s.id}
              </div>
              <div style={{ fontSize: 11, color: "var(--ink-4)" }}>
                by {s.createdBy} · {fmtRel(s.createdAt)} · expires {fmtRel(s.exp * 1000)}
              </div>
              <div style={{ fontSize: 11, color: "var(--ink-3)", fontFamily: "var(--f-mono)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                /s/{s.token}
              </div>
            </div>
            <button onClick={() => copy(s.token)} style={ghostBtnSmall}>Copy</button>
            <button onClick={() => revoke(s.token)} style={{ ...ghostBtnSmall, color: "var(--pink)" }}>Revoke</button>
          </div>
        );
      })}
    </div>
  );
}

// ---------------------------------------------------------------
// Upload — drag-drop FLAC files (with optional FLAC→AAC transcode)
// ---------------------------------------------------------------
function AdminUpload({ onUploaded }) {
  const [albums, setAlbums] = useStateAdm([]);
  const [busyParse, setBusyParse] = useStateAdm(false);
  const [busyUpload, setBusyUpload] = useStateAdm(false);
  const [autoAac, setAutoAac] = useStateAdm(true);
  const [log, setLog] = useStateAdm([]);
  const [ffmpegStatus, setFfmpegStatus] = useStateAdm("idle");
  const dropRef = useRefAdm(null);
  const inputRef = useRefAdm(null);

  function pushLog(s) { setLog(l => [...l, s]); }

  async function handleFiles(fileList) {
    const files = Array.from(fileList).filter(f => /\.(flac|m4a|mp3)$/i.test(f.name));
    if (!files.length) return;
    setBusyParse(true);
    try {
      const parsed = [];
      for (const f of files) {
        const meta = await parseAudioFile(f);
        parsed.push({ file: f, meta });
      }
      const groups = new Map();
      for (const p of parsed) {
        const albumKey = (p.meta.album || p.file.name).trim();
        if (!groups.has(albumKey)) groups.set(albumKey, []);
        groups.get(albumKey).push(p);
      }
      const newAlbums = [];
      for (const [albumName, items] of groups) {
        items.sort((a, b) => {
          const na = parseTrackNo(a.meta.tracknumber, a.file.name);
          const nb = parseTrackNo(b.meta.tracknumber, b.file.name);
          return na - nb;
        });
        const artist = items[0].meta.artist || "Unknown Artist";
        const year = parseInt(items[0].meta.year || items[0].meta.date || 0, 10) || new Date().getFullYear();
        const albumId = slugify(albumName);
        const coverBlob = items.map(it => it.meta.coverBlob).find(Boolean) || null;
        const tracks = items.map((it, i) => ({
          file: it.file,
          title: it.meta.title || stripTrackPrefix(it.file.name),
          d: Math.round(it.meta.duration || 0),
          lyrics: it.meta.lyrics || null,
          trackNo: i + 1,
        }));
        newAlbums.push({ id: albumId, title: albumName, artist, year, isSingle: tracks.length === 1, coverBlob, tracks });
      }
      setAlbums(prev => [...prev, ...newAlbums]);
    } catch (e) {
      pushLog("✗ Parse error: " + e.message);
    }
    setBusyParse(false);
  }

  function onDrop(e) {
    e.preventDefault();
    dropRef.current?.classList.remove("dragover");
    if (e.dataTransfer.files?.length) handleFiles(e.dataTransfer.files);
  }
  function onDragOver(e) { e.preventDefault(); dropRef.current?.classList.add("dragover"); }
  function onDragLeave() { dropRef.current?.classList.remove("dragover"); }

  function updateAlbum(i, patch) { setAlbums(a => a.map((x, j) => j === i ? { ...x, ...patch } : x)); }
  function removeAlbum(i) { setAlbums(a => a.filter((_, j) => j !== i)); }

  async function uploadAll() {
    if (!albums.length) return;
    setBusyUpload(true); setLog([]);
    try {
      const anyFlac = albums.some(a => a.tracks.some(t => /\.flac$/i.test(t.file.name)));
      if (autoAac && anyFlac && !window._discoFFmpeg) {
        pushLog("Loading converter (~25 MB, one-time download)…");
        setFfmpegStatus("loading");
        try {
          await ensureFFmpeg();
          setFfmpegStatus("ready");
          pushLog("Converter ready.");
        } catch (e) {
          setFfmpegStatus("error");
          pushLog("⚠ Couldn't load the converter (" + e.message + "). HIGH tier will fall back to master.");
        }
      }
      for (const a of albums) {
        pushLog(`▸ ${a.title} (${a.artist}) — ${a.tracks.length} track${a.tracks.length === 1 ? "" : "s"}`);
        if (a.coverBlob) {
          pushLog(`  cover.jpg (${(a.coverBlob.size/1024).toFixed(0)} KB)`);
          await uploadBlob(`covers/${a.id}.jpg`, a.coverBlob, "image/jpeg");
        }
        const trackMeta = [];
        const lyricsByTid = {};
        for (let i = 0; i < a.tracks.length; i++) {
          const t = a.tracks[i];
          const tid = `${a.id}-t${i + 1}`;
          const isFlac = /\.flac$/i.test(t.file.name);
          pushLog(`  ${t.title} (${(t.file.size/1e6).toFixed(1)} MB)`);

          let hasHigh = !isFlac;
          let hasMaster = isFlac;

          if (isFlac) {
            await uploadFile(`tracks/${tid}/master.flac`, t.file, "audio/flac");
            if (autoAac && window._discoFFmpeg) {
              try {
                pushLog(`    transcoding → AAC 320…`);
                const aacBlob = await transcodeFlacToAac(t.file);
                pushLog(`    AAC ready (${(aacBlob.size/1e6).toFixed(1)} MB) — uploading`);
                await uploadBlob(`tracks/${tid}/high.m4a`, aacBlob, "audio/mp4");
                hasHigh = true;
              } catch (e) {
                pushLog(`    ⚠ transcode failed (${e.message}). HIGH will fall back to master.`);
              }
            }
          } else {
            await uploadFile(`tracks/${tid}/high.m4a`, t.file, "audio/mp4");
          }
          trackMeta.push({ id: tid, title: t.title, d: t.d, q: hasMaster ? "master" : "high", hasHigh, hasMaster });
          if (t.lyrics) lyricsByTid[tid] = t.lyrics;
        }
        pushLog(`  saving album metadata…`);
        const album = {
          id: a.id, title: a.title, artist: a.artist, year: a.year,
          isSingle: !!a.isSingle,
          cover: { style: "image", hasImage: !!a.coverBlob },
          tracks: trackMeta,
        };
        const res = await fetch("/api/admin/library/album", {
          method: "POST", credentials: "include",
          headers: { "content-type": "application/json" },
          body: JSON.stringify(album),
        });
        if (!res.ok) throw new Error(`Library save failed: HTTP ${res.status}`);
        if (Object.keys(lyricsByTid).length) {
          await fetch("/api/admin/library/lyrics", {
            method: "POST", credentials: "include",
            headers: { "content-type": "application/json" },
            body: JSON.stringify({ lyrics: lyricsByTid }),
          });
        }
        pushLog(`  ✓ done`);
      }
      pushLog("\nAll albums uploaded. Refresh the page to see them.");
      setAlbums([]);
      if (onUploaded) onUploaded();
    } catch (e) {
      pushLog("✗ " + e.message);
    }
    setBusyUpload(false);
  }

  return (
    <div style={{ maxWidth: 880, display: "flex", flexDirection: "column", gap: 16 }}>
      <div
        ref={dropRef}
        className="admin-drop"
        onDrop={onDrop}
        onDragOver={onDragOver}
        onDragLeave={onDragLeave}
        onClick={() => inputRef.current?.click()}
      >
        <input
          ref={inputRef}
          type="file"
          accept=".flac,.m4a,.mp3,audio/*"
          multiple
          hidden
          onChange={e => e.target.files && handleFiles(e.target.files)}
        />
        <div style={{ fontFamily: "var(--f-display)", fontWeight: 900, fontSize: 28, letterSpacing: "0.005em", textTransform: "uppercase", color: "var(--gold)" }}>
          Drop FLAC files here
        </div>
        <div style={{ fontSize: 13, color: "var(--ink-3)" }}>
          {busyParse ? "Reading metadata…" : ".flac / .m4a / .mp3 — title, artist, album, year, cover and lyrics are read from the tags"}
        </div>
        <div style={{ fontSize: 11, color: "var(--ink-4)", letterSpacing: "0.18em", textTransform: "uppercase", fontWeight: 700 }}>
          or click to choose
        </div>
      </div>

      <label style={{
        display: "flex", alignItems: "center", gap: 10,
        padding: "12px 16px", borderRadius: 10,
        background: autoAac ? "rgba(245,193,75,0.08)" : "var(--bg-2)",
        border: "1px solid " + (autoAac ? "rgba(245,193,75,0.3)" : "var(--line)"),
        cursor: "pointer", userSelect: "none", maxWidth: 760,
      }}>
        <input type="checkbox" checked={autoAac} onChange={e => setAutoAac(e.target.checked)} style={{ accentColor: "var(--gold)", width: 16, height: 16 }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 12, letterSpacing: "0.18em", textTransform: "uppercase", color: autoAac ? "var(--gold)" : "var(--ink-2)" }}>
            Auto-generate HIGH tier (AAC 320 .m4a)
          </div>
          <div style={{ fontSize: 12, color: "var(--ink-3)", marginTop: 2, lineHeight: 1.4 }}>
            Transcodes each FLAC in your browser. One-time ~25 MB download for the converter; ~20-40 s per song.
            Turn off to upload faster — streams will fall back to master.
          </div>
        </div>
        {ffmpegStatus === "loading" && <span style={{ fontSize: 11, color: "var(--gold)" }}>loading…</span>}
        {ffmpegStatus === "ready" && <span style={{ fontSize: 11, color: "var(--gold)" }}>ready</span>}
      </label>

      {albums.map((a, i) => (
        <div key={i} style={{
          border: "1px solid var(--line-strong)",
          borderRadius: 14, background: "var(--bg-2)",
          padding: 16, display: "flex", flexDirection: "column", gap: 12,
        }}>
          <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
            <div style={{ width: 64, height: 64, borderRadius: 8, overflow: "hidden", background: "var(--bg-1)", flex: "0 0 64px", display: "grid", placeItems: "center" }}>
              {a.coverBlob
                ? <img src={URL.createObjectURL(a.coverBlob)} style={{ width: "100%", height: "100%", objectFit: "cover" }} alt="" />
                : <span style={{ color: "var(--ink-4)", fontSize: 10 }}>NO ART</span>}
            </div>
            <div style={{ flex: 1, minWidth: 0, display: "grid", gridTemplateColumns: "1fr 1fr 100px", gap: 8 }}>
              <input value={a.title} onChange={e => updateAlbum(i, { title: e.target.value })} style={inputStyle} placeholder="Album title" />
              <input value={a.artist} onChange={e => updateAlbum(i, { artist: e.target.value })} style={inputStyle} placeholder="Artist" />
              <input type="number" value={a.year} onChange={e => updateAlbum(i, { year: parseInt(e.target.value, 10) || 0 })} style={inputStyle} placeholder="Year" />
            </div>
            <button onClick={() => removeAlbum(i)} style={{ color: "var(--ink-4)", fontSize: 11, letterSpacing: "0.18em", textTransform: "uppercase", fontWeight: 700 }}>Remove</button>
          </div>
          <div style={{ fontSize: 11, color: "var(--ink-4)", letterSpacing: "0.18em", textTransform: "uppercase", fontWeight: 700 }}>
            Album id: <code style={{ color: "var(--ink-2)", fontFamily: "var(--f-mono)" }}>{a.id}</code> · {a.isSingle ? "Single" : `${a.tracks.length} tracks`}
          </div>
          <details>
            <summary style={{ fontSize: 12, color: "var(--ink-3)", cursor: "pointer", letterSpacing: "0.05em" }}>
              {a.tracks.length} tracks · {Math.round(a.tracks.reduce((s, t) => s + t.d, 0) / 60)} min · {a.tracks.filter(t => t.lyrics).length} with lyrics
            </summary>
            <div style={{ display: "flex", flexDirection: "column", gap: 4, marginTop: 8 }}>
              {a.tracks.map((t, ti) => (
                <div key={ti} style={{ display: "flex", gap: 10, alignItems: "center", padding: "6px 0", borderBottom: "1px dashed var(--line)" }}>
                  <span style={{ width: 22, color: "var(--ink-3)", fontSize: 11, fontFamily: "var(--f-mono)" }}>{(ti+1).toString().padStart(2, "0")}</span>
                  <span style={{ flex: 1, fontSize: 13 }}>{t.title}</span>
                  {t.lyrics && <span style={{ fontSize: 10, color: "var(--gold)", letterSpacing: "0.18em" }}>♪ LYR</span>}
                  <span style={{ fontSize: 11, color: "var(--ink-3)", fontVariantNumeric: "tabular-nums" }}>{fmtTime(t.d)}</span>
                </div>
              ))}
            </div>
          </details>
        </div>
      ))}

      {albums.length > 0 && (
        <button
          onClick={uploadAll}
          disabled={busyUpload}
          style={{
            ...pinkBtnStyle,
            padding: "16px 22px",
            fontSize: 14,
            alignSelf: "flex-start",
            opacity: busyUpload ? 0.5 : 1,
          }}
        >{busyUpload ? "Uploading…" : `Publish ${albums.length} album${albums.length === 1 ? "" : "s"}`}</button>
      )}

      {log.length > 0 && (
        <pre style={{
          background: "var(--bg-0)", border: "1px solid var(--line)", borderRadius: 8,
          padding: 12, fontFamily: "var(--f-mono)", fontSize: 11,
          color: "var(--ink-3)", maxHeight: 280, overflowY: "auto",
          whiteSpace: "pre-wrap", margin: 0,
        }}>{log.join("\n")}</pre>
      )}
    </div>
  );
}

// ---------------------------------------------------------------
// ffmpeg.wasm loader (single-thread, no SharedArrayBuffer)
// ---------------------------------------------------------------
const FFMPEG_VERSION = "0.11.6";
const FFMPEG_CORE_VERSION = "0.11.0";
async function ensureFFmpeg() {
  if (window._discoFFmpeg) return window._discoFFmpeg;
  if (window._discoFFmpegLoading) return window._discoFFmpegLoading;
  window._discoFFmpegLoading = (async () => {
    await new Promise((resolve, reject) => {
      const s = document.createElement("script");
      s.src = `https://unpkg.com/@ffmpeg/ffmpeg@${FFMPEG_VERSION}/dist/ffmpeg.min.js`;
      s.onload = resolve;
      s.onerror = () => reject(new Error("Could not load ffmpeg.wasm"));
      document.head.appendChild(s);
    });
    const { createFFmpeg } = window.FFmpeg;
    const ff = createFFmpeg({ log: false, corePath: `https://unpkg.com/@ffmpeg/core@${FFMPEG_CORE_VERSION}/dist/ffmpeg-core.js` });
    await ff.load();
    window._discoFFmpeg = ff;
    return ff;
  })();
  try { return await window._discoFFmpegLoading; }
  catch (e) { window._discoFFmpegLoading = null; throw e; }
}

let _ffmpegBusy = Promise.resolve();
async function transcodeFlacToAac(file) {
  const prev = _ffmpegBusy;
  let resolveTurn;
  _ffmpegBusy = new Promise(r => { resolveTurn = r; });
  await prev;
  try {
    const ff = await ensureFFmpeg();
    const buf = new Uint8Array(await file.arrayBuffer());
    ff.FS("writeFile", "in.flac", buf);
    await ff.run("-i", "in.flac", "-vn", "-c:a", "aac", "-b:a", "320k", "-movflags", "+faststart", "out.m4a");
    const out = ff.FS("readFile", "out.m4a");
    try { ff.FS("unlink", "in.flac"); } catch (e) {}
    try { ff.FS("unlink", "out.m4a"); } catch (e) {}
    return new Blob([out.buffer], { type: "audio/mp4" });
  } finally { resolveTurn(); }
}

// ---------------------------------------------------------------
// uploads
// ---------------------------------------------------------------
async function uploadBlob(key, blob, contentType) {
  const res = await fetch("/api/admin/upload/" + encodeURIComponent(key), {
    method: "PUT", credentials: "include",
    headers: { "content-type": contentType },
    body: blob,
  });
  if (!res.ok) throw new Error(`Upload ${key} failed: HTTP ${res.status}`);
}
async function uploadFile(key, file, contentType) { return uploadBlob(key, file, contentType); }

// ---------------------------------------------------------------
// Audio metadata reader (FLAC + M4A via shared parser)
// ---------------------------------------------------------------
async function parseAudioFile(file) {
  const ext = file.name.split(".").pop().toLowerCase();
  const headBuf = await file.slice(0, 8 * 1024 * 1024).arrayBuffer();
  let meta = {};
  if (ext === "flac") meta = parseFlacMeta(headBuf);
  else if (ext === "m4a" || ext === "mp4") meta = await parseM4aMeta(headBuf);
  meta.duration = await probeDuration(file);
  return meta;
}
async function probeDuration(file) {
  return new Promise(resolve => {
    const a = document.createElement("audio");
    a.preload = "metadata";
    const url = URL.createObjectURL(file);
    a.src = url;
    a.onloadedmetadata = () => { URL.revokeObjectURL(url); resolve(a.duration || 0); };
    a.onerror = () => { URL.revokeObjectURL(url); resolve(0); };
  });
}
function parseFlacMeta(buf) {
  const meta = { lyrics: null };
  const view = new DataView(buf);
  if (view.byteLength < 4 || view.getUint32(0) !== 0x664c6143) return meta;
  let off = 4;
  while (off < view.byteLength) {
    const header = view.getUint8(off);
    const last = (header & 0x80) !== 0;
    const type = header & 0x7f;
    const len = (view.getUint8(off + 1) << 16) | (view.getUint8(off + 2) << 8) | view.getUint8(off + 3);
    const body = buf.slice(off + 4, off + 4 + len);
    off += 4 + len;
    if (type === 4) {
      const v = new DataView(body);
      let p = 0;
      const vendorLen = v.getUint32(p, true); p += 4 + vendorLen;
      const count = v.getUint32(p, true); p += 4;
      for (let i = 0; i < count; i++) {
        const l = v.getUint32(p, true); p += 4;
        const text = new TextDecoder("utf-8").decode(new Uint8Array(body, p, l));
        p += l;
        const eq = text.indexOf("=");
        if (eq < 0) continue;
        const key = text.slice(0, eq).toLowerCase();
        const val = text.slice(eq + 1);
        if (key === "title") meta.title = val;
        else if (key === "artist" || key === "albumartist") meta.artist = meta.artist || val;
        else if (key === "album") meta.album = val;
        else if (key === "date" || key === "year") meta.year = (val || "").slice(0, 4);
        else if (key === "tracknumber") meta.tracknumber = val;
        else if (key === "lyrics" || key === "unsyncedlyrics" || key === "syncedlyrics") {
          if (!meta.lyricsRaw) meta.lyricsRaw = val;
        }
      }
    } else if (type === 6) {
      const v = new DataView(body);
      let p = 0;
      p += 4;
      const mimeLen = v.getUint32(p, false); p += 4;
      const mime = new TextDecoder().decode(new Uint8Array(body, p, mimeLen)); p += mimeLen;
      const descLen = v.getUint32(p, false); p += 4; p += descLen;
      p += 16;
      const dataLen = v.getUint32(p, false); p += 4;
      const data = new Uint8Array(body, p, dataLen);
      meta.coverBlob = new Blob([data], { type: mime || "image/jpeg" });
    }
    if (last) break;
  }
  if (meta.lyricsRaw) meta.lyrics = parseLRC(meta.lyricsRaw);
  return meta;
}
async function parseM4aMeta(buf) {
  if (window.parseM4A) {
    try {
      const m = window.parseM4A(buf);
      const out = { title: m.title, artist: m.artist, album: m.album, year: m.year, tracknumber: m.tracknumber };
      if (m.coverBlob) out.coverBlob = m.coverBlob;
      if (m.lyricsRaw) out.lyrics = parseLRC(m.lyricsRaw);
      return out;
    } catch (e) {}
  }
  return {};
}
function parseLRC(text) {
  const out = [];
  const lines = String(text || "").split(/\r?\n/);
  for (const ln of lines) {
    const stamps = [];
    let s = ln;
    while (true) {
      const m = /^\[(\d+):(\d+(?:\.\d+)?)\]/.exec(s);
      if (!m) break;
      stamps.push(parseInt(m[1], 10) * 60 + parseFloat(m[2]));
      s = s.slice(m[0].length);
    }
    const lineText = s.trim();
    for (const t of stamps) out.push({ t, line: lineText, ...(lineText ? {} : { i: true }) });
  }
  out.sort((a, b) => a.t - b.t);
  return out;
}
function parseTrackNo(tagVal, filename) {
  if (tagVal != null) {
    const n = parseInt(String(tagVal).split("/")[0], 10);
    if (!isNaN(n)) return n;
  }
  const m = /^(\d+)/.exec(filename);
  return m ? parseInt(m[1], 10) : 9999;
}
function stripTrackPrefix(name) { return name.replace(/\.[a-z0-9]+$/i, "").replace(/^\d+\s*[-_.]\s*/, ""); }
function slugify(s) {
  return String(s).toLowerCase()
    .normalize("NFD").replace(/[̀-ͯ]/g, "")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 48) || "album";
}

// ---------------------------------------------------------------
// styles
// ---------------------------------------------------------------
const inputStyle = {
  background: "var(--bg-1)",
  border: "1px solid var(--line-strong)",
  borderRadius: 8,
  padding: "10px 12px",
  fontSize: 14,
  color: "var(--ink)",
  outline: "none",
  fontFamily: "inherit",
};
const pinkBtnStyle = {
  background: "var(--pink)", color: "var(--bg-0)",
  padding: "12px 18px", borderRadius: 999,
  fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 12,
  letterSpacing: "0.18em", textTransform: "uppercase",
  cursor: "pointer", border: 0,
};
const ghostBtnStyle = {
  background: "var(--bg-3)", color: "var(--ink)",
  padding: "12px 18px", borderRadius: 999,
  fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 12,
  letterSpacing: "0.18em", textTransform: "uppercase",
  cursor: "pointer", border: "1px solid var(--line-strong)",
};
const pinkBtnSmall  = { ...pinkBtnStyle, padding: "7px 12px", fontSize: 10 };
const ghostBtnSmall = { ...ghostBtnStyle, padding: "7px 12px", fontSize: 10 };
const urlBoxStyle = {
  background: "var(--bg-0)",
  border: "1px solid var(--line-strong)",
  borderRadius: 8,
  padding: "10px 12px",
  fontFamily: "var(--f-mono)",
  fontSize: 12,
  color: "var(--ink-2)",
  wordBreak: "break-all",
  userSelect: "all",
};
const sectionH3 = { fontFamily: "var(--f-display)", fontWeight: 800, fontSize: 18, textTransform: "uppercase", letterSpacing: "0.005em", margin: "0 0 12px" };
const mutedRow = { color: "var(--ink-3)", padding: 16, background: "var(--bg-2)", borderRadius: 10, border: "1px dashed var(--line)" };
const listRow = {
  display: "flex", alignItems: "center", gap: 12,
  padding: "10px 12px",
  background: "var(--bg-2)",
  borderRadius: 8,
};
const userRowStyle = {
  display: "flex", alignItems: "center", gap: 12,
  padding: "10px 14px",
  background: "var(--bg-2)", borderRadius: 10,
  border: "1px solid var(--line)",
};
const pendingRowStyle = {
  display: "flex", alignItems: "center", gap: 12,
  padding: "8px 12px",
  background: "var(--bg-2)", borderRadius: 8,
  border: "1px dashed var(--line-strong)",
};
const shareRowStyle = {
  display: "flex", alignItems: "center", gap: 10,
  padding: "10px 14px",
  background: "var(--bg-2)", borderRadius: 10,
  border: "1px solid var(--line)",
};

Object.assign(window, { AdminView });
