/* The Pulse — app root: state, shared persistence, layout, export.

   Editions (the newsletter "issues") are stored on a shared Supabase backend so
   several people can edit the same library with no login. Which edition you are
   currently viewing is a per-browser choice kept in localStorage. If the backend
   is unavailable/unconfigured, the app falls back to browser-local storage. */
(function () {
  const { useState, useEffect, useRef } = React;
  const Editor = window.PulseEditor;
  const Newsletter = window.PulseNewsletter;
  const EditionsBar = window.PulseEditions;
  const I = window.PulseIcons;
  const Store = window.PulseStore || { enabled: false };

  const CUR_KEY = "pulse_current_id";     // which edition this browser is viewing
  const LOCAL_LIB = "pulse_library_v2";   // localStorage fallback (offline / unconfigured)
  const SAVE_DEBOUNCE = 900;

  function deepMerge(base, over) {
    if (Array.isArray(base)) return over !== undefined ? over : base;
    if (base && typeof base === "object") {
      const out = { ...base };
      for (const k in base) if (over && k in over) out[k] = deepMerge(base[k], over[k]);
      return out;
    }
    return over !== undefined ? over : base;
  }

  function freshContent() { return JSON.parse(JSON.stringify(window.PULSE_DEFAULTS)); }
  function blankContent() {
    return window.PULSE_BLANK ? JSON.parse(JSON.stringify(window.PULSE_BLANK)) : freshContent();
  }
  function mergeContent(c) {
    const merged = deepMerge(window.PULSE_DEFAULTS, c);
    if (c && typeof c === "object") {
      Object.keys(c).forEach((k) => { if (!(k in merged)) merged[k] = c[k]; });
    }
    if (c && Array.isArray(c.sectionOrder)) merged.sectionOrder = c.sectionOrder.slice();
    return merged;
  }
  function newId() { return "ed_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); }
  function labelFor(content) {
    const m = content && content.meta;
    return m && m.month ? (m.month + " " + (m.year || "")).trim() : "Untitled issue";
  }
  function makeEdition(content, label) {
    const now = Date.now();
    return { id: newId(), label: label || labelFor(content), updatedAt: now, sortOrder: now, content };
  }

  function buildEmailHTML(content) {
    const css = document.getElementById("newsletter-css").textContent;
    const node = document.getElementById("pulse-email");
    const inner = node ? node.outerHTML : "";
    return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light only">
<title>The Pulse — ${content.meta.month} ${content.meta.year}</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,500&display=swap" rel="stylesheet">
<style>
body{margin:0;background:#eef4f5;}
${css}
</style>
</head>
<body>
<div class="pmail-wrap" style="padding:24px 0;">
${inner}
</div>
</body>
</html>`;
  }

  function App({ user, onSignOut }) {
    // editions === null while the first load is in flight
    const [editions, setEditions] = useState(null);
    const [currentId, setCurrentId] = useState(() => {
      try { return localStorage.getItem(CUR_KEY) || null; } catch (e) { return null; }
    });
    const [toast, setToast] = useState("");
    const [offline, setOffline] = useState(false);
    const [profiles, setProfiles] = useState([]);

    const toastTimer = useRef(null);
    const saveTimers = useRef({});
    const profileTimers = useRef({});
    const editionsRef = useRef(null);
    const currentRef = useRef(currentId);

    const flash = (msg) => {
      setToast(msg);
      clearTimeout(toastTimer.current);
      toastTimer.current = setTimeout(() => setToast(""), 2600);
    };

    useEffect(() => { editionsRef.current = editions; }, [editions]);
    useEffect(() => {
      currentRef.current = currentId;
      try { if (currentId) localStorage.setItem(CUR_KEY, currentId); } catch (e) {}
    }, [currentId]);

    // ---- pick / persist current edition ----
    const chooseCurrent = (list) => {
      setCurrentId((id) => (id && list.some((e) => e.id === id)) ? id : (list[0] ? list[0].id : null));
    };

    // ---- localStorage fallback (offline or unconfigured) ----
    const saveLocal = (list) => {
      try { localStorage.setItem(LOCAL_LIB, JSON.stringify({ editions: list })); } catch (e) {}
    };
    const loadLocal = () => {
      try {
        const raw = localStorage.getItem(LOCAL_LIB);
        if (raw) {
          const lib = JSON.parse(raw);
          if (lib && Array.isArray(lib.editions) && lib.editions.length) {
            return lib.editions.map((e) => ({
              id: e.id || newId(),
              label: e.label || labelFor(e.content),
              updatedAt: e.updatedAt || Date.now(),
              sortOrder: e.sortOrder != null ? e.sortOrder : (e.updatedAt || Date.now()),
              content: mergeContent(e.content)
            }));
          }
        }
      } catch (e) {}
      return [makeEdition(freshContent())];
    };

    // ---- initial load + realtime subscription ----
    useEffect(() => {
      let alive = true;
      let unsub = () => {};

      (async () => {
        if (Store.enabled) {
          try {
            let list = await Store.listEditions();
            if (!list.length) {
              const seed = makeEdition(freshContent());
              await Store.upsertEdition(seed);
              list = [seed];
            }
            list = list.map((e) => ({ ...e, content: mergeContent(e.content) }));
            if (!alive) return;
            setEditions(list);
            chooseCurrent(list);
            unsub = Store.subscribe(applyRealtime);
          } catch (e) {
            console.error("Load from server failed", e);
            if (!alive) return;
            const list = loadLocal();
            setEditions(list);
            chooseCurrent(list);
            setOffline(true);
            flash("Couldn't reach the server — working in this browser only");
          }
        } else {
          const list = loadLocal();
          setEditions(list);
          chooseCurrent(list);
        }
      })();

      return () => { alive = false; unsub(); };
      // eslint-disable-next-line
    }, []);

    // fallback persistence: whenever editions change and we're local-only, save
    useEffect(() => {
      if (editions && (!Store.enabled || offline)) saveLocal(editions);
    }, [editions, offline]);

    // ---- committee profiles (shared, reused across issues) ----
    useEffect(() => {
      let alive = true;
      (async () => {
        const defaults = (window.PULSE_PROFILE_DEFAULTS || []).map((p) => ({ ...p }));
        if (Store.enabled && Store.listProfiles) {
          try {
            let list = await Store.listProfiles();
            if (!list.length && defaults.length) {
              for (const p of defaults) { try { await Store.upsertProfile(p); } catch (e) {} }
              list = defaults;
            }
            if (alive) setProfiles(list);
            return;
          } catch (e) { console.error("Load profiles failed", e); }
        }
        if (alive) setProfiles(defaults);
      })();
      return () => { alive = false; };
      // eslint-disable-next-line
    }, []);

    // update one profile (optimistic) + debounced shared save
    const updateProfile = (id, patch) => {
      setProfiles((list) => {
        const next = (list || []).map((p) => p.id === id ? { ...p, ...patch } : p);
        if (Store.enabled && !offline && Store.upsertProfile) {
          const p = next.find((x) => x.id === id);
          const timers = profileTimers.current;
          clearTimeout(timers[id]);
          timers[id] = setTimeout(async () => {
            try { await Store.upsertProfile(p); }
            catch (e) { console.error("Save profile failed", e); flash("Couldn't save that profile change"); }
          }, 600);
        }
        return next;
      });
    };

    // ---- debounced server save of one edition ----
    const scheduleSave = (ed) => {
      if (!Store.enabled || offline) return;
      const timers = saveTimers.current;
      clearTimeout(timers[ed.id]);
      timers[ed.id] = setTimeout(async () => {
        try {
          const content = Store.hostInlineImages ? await Store.hostInlineImages(ed.content) : ed.content;
          await Store.upsertEdition({ ...ed, content });
        } catch (e) {
          console.error("Save failed", e);
          flash("Save failed — will retry on your next change");
        }
      }, SAVE_DEBOUNCE);
    };

    // ---- realtime updates from other people ----
    const applyRealtime = (payload) => {
      const type = payload.eventType || payload.type;
      const row = payload.new;
      const oldRow = payload.old;

      if (type === "DELETE") {
        const id = oldRow && oldRow.id;
        if (!id) return;
        setEditions((list) => (list || []).filter((e) => e.id !== id));
        if (id === currentRef.current) {
          const remaining = (editionsRef.current || []).filter((e) => e.id !== id);
          if (remaining[0]) setCurrentId(remaining[0].id);
        }
        return;
      }

      if (!row || !row.id) return;
      // Don't clobber the edition this browser is actively editing.
      if (row.id === currentRef.current && type === "UPDATE") return;

      const ed = {
        id: row.id,
        label: row.label || "Untitled issue",
        updatedAt: row.updated_at ? new Date(row.updated_at).getTime() : Date.now(),
        sortOrder: row.sort_order != null ? row.sort_order : 0,
        content: mergeContent(row.content)
      };
      setEditions((list) => {
        const arr = list || [];
        const idx = arr.findIndex((e) => e.id === row.id);
        if (idx === -1) return [...arr, ed].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
        const next = arr.slice();
        next[idx] = ed;
        return next;
      });
    };

    // ---- editing the current edition ----
    const setContent = (updater) => {
      setEditions((list) => {
        const next = (list || []).map((e) =>
          e.id === currentRef.current
            ? { ...e, content: typeof updater === "function" ? updater(e.content) : updater, updatedAt: Date.now() }
            : e
        );
        const cur = next.find((e) => e.id === currentRef.current);
        if (cur) scheduleSave(cur);
        return next;
      });
    };

    // ---- edition operations ----
    const switchEd = (id) => setCurrentId(id);

    const renameEd = (id, label) => {
      setEditions((list) => {
        const next = (list || []).map((e) => e.id === id ? { ...e, label, updatedAt: Date.now() } : e);
        const ed = next.find((e) => e.id === id);
        if (ed) scheduleSave(ed);
        return next;
      });
    };

    const addEdition = async (cnt, label) => {
      const ed = makeEdition(cnt, label);
      setEditions((list) => [...(list || []), ed]);
      setCurrentId(ed.id);
      flash("New edition created");
      if (Store.enabled && !offline) {
        try {
          const content = Store.hostInlineImages ? await Store.hostInlineImages(ed.content) : ed.content;
          await Store.upsertEdition({ ...ed, content });
        }
        catch (e) { console.error(e); flash("Couldn't save the new edition to the server"); }
      }
    };
    const curContent = () => {
      const cur = (editionsRef.current || []).find((e) => e.id === currentRef.current);
      return cur ? cur.content : freshContent();
    };
    const newEd = () => addEdition(JSON.parse(JSON.stringify(curContent())), "Untitled issue");
    const blankEd = () => addEdition(blankContent(), "Untitled issue");
    const duplicateEd = (id) => {
      const src = (editionsRef.current || []).find((e) => e.id === id);
      if (src) addEdition(JSON.parse(JSON.stringify(src.content)), (src.label || "Issue") + " copy");
    };
    const deleteEd = async (id) => {
      const list = editionsRef.current || [];
      if (list.length <= 1) return;
      const ed = list.find((e) => e.id === id);
      if (!confirm('Delete "' + (ed ? ed.label : "this edition") + '"? This removes it for everyone and cannot be undone.')) return;
      const remaining = list.filter((e) => e.id !== id);
      setEditions(remaining);
      if (id === currentRef.current && remaining[0]) setCurrentId(remaining[0].id);
      flash("Edition deleted");
      if (Store.enabled && !offline) {
        try { await Store.deleteEdition(id); }
        catch (e) { console.error(e); flash("Couldn't delete on the server"); }
      }
    };

    // ---- export ----
    const copyForOutlook = async (content) => {
      const html = window.buildOutlookEmail(content);
      const text = html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
      try {
        const item = new ClipboardItem({
          "text/html": new Blob([html], { type: "text/html" }),
          "text/plain": new Blob([text], { type: "text/plain" })
        });
        await navigator.clipboard.write([item]);
        flash("Copied — paste into a new Outlook email");
      } catch (e) {
        try {
          const div = document.createElement("div");
          div.innerHTML = html;
          div.style.position = "fixed"; div.style.left = "-9999px";
          document.body.appendChild(div);
          const r = document.createRange(); r.selectNode(div);
          const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(r);
          document.execCommand("copy");
          sel.removeAllRanges(); document.body.removeChild(div);
          flash("Copied — paste into a new Outlook email");
        } catch (_) { flash("Copy failed — try Download instead"); }
      }
    };

    const downloadHTML = (content, label) => {
      const html = window.buildOutlookDoc(content);
      const blob = new Blob([html], { type: "text/html" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      const safe = (label || (content.meta.month + content.meta.year)).replace(/[^a-z0-9]+/gi, "_").replace(/^_|_$/g, "");
      a.download = "The_Pulse_" + (safe || "issue") + ".html";
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(() => URL.revokeObjectURL(a.href), 1000);
      flash("Downloaded email HTML");
    };

    const reset = () => {
      if (confirm("Reset THIS edition's content back to the sample issue? This clears everyone's edits to it.")) {
        setContent(freshContent());
        flash("Reset to sample content");
      }
    };

    // ---- render ----
    if (editions === null) {
      return (
        <div className="app">
          <div className="editor" style={{ alignItems: "center", justifyContent: "center" }}>
            <div style={{ textAlign: "center", color: "#6D6E71", padding: 40 }}>
              <div className="ed-mark" style={{ margin: "0 auto 14px", background: "#004860" }}><I.Heart size={19} /></div>
              <div style={{ fontWeight: 700, color: "#004860", fontSize: 15 }}>The Pulse</div>
              <div style={{ fontSize: 12, marginTop: 6 }}>Loading your editions…</div>
            </div>
          </div>
          <div className="preview" />
        </div>
      );
    }

    const cur = editions.find((e) => e.id === currentId) || editions[0];
    const content = cur.content;
    const wordW = { editorial: "Editorial", banner: "Banner", gradient: "Gradient" }[content.meta.masthead];

    return (
      <div className="app">
        {/* EDITOR */}
        <div className="editor">
          <div className="ed-head">
            <div className="ed-mark"><I.Heart size={19} /></div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <h1>The Pulse · Issue Builder</h1>
              <p>{offline ? "Offline — saved to this browser only" : (Store.enabled ? "Shared · edits save automatically" : "Local mode — set config.js to share")}</p>
            </div>
            {user && (
              <button
                onClick={onSignOut}
                title={"Signed in as " + displayName(user) + " — click to sign out"}
                style={{
                  flexShrink: 0, border: "1px solid #dce8ea", background: "#fff", color: "#6D6E71",
                  borderRadius: 8, padding: "6px 10px", fontFamily: "inherit", fontSize: 11,
                  fontWeight: 600, cursor: "pointer", lineHeight: 1.2, textAlign: "right"
                }}>
                {displayName(user)}<br /><span style={{ color: "#00778B", fontWeight: 700 }}>Sign out</span>
              </button>
            )}
          </div>
          <EditionsBar
            editions={editions}
            currentId={cur.id}
            onSwitch={switchEd}
            onRename={renameEd}
            onNew={newEd}
            onBlank={blankEd}
            onDuplicate={duplicateEd}
            onDelete={deleteEd}
          />
          <div className="ed-scroll">
            <Editor content={content} setContent={setContent}
                    profiles={profiles} updateProfile={updateProfile} currentUser={user} />
          </div>
          <div className="ed-foot">
            <button className="btn btn-prime" onClick={() => copyForOutlook(content)}><I.Mail size={15} /> Copy for Outlook</button>
            <button className="btn btn-ghost" onClick={() => downloadHTML(content, cur.label)} title="Download .html file"><I.Arrow size={15} /></button>
            <button className="btn btn-ghost" onClick={reset} title="Reset to sample">↺</button>
          </div>
        </div>

        {/* PREVIEW */}
        <div className="preview">
          <div className="preview-bar">
            <span className="pv-t">Live Preview</span>
            <span className="pv-w">{wordW} masthead · paste-ready for Outlook</span>
          </div>
          <div className="preview-stage">
            <div style={{ width: 660, maxWidth: "100%" }}>
              <Newsletter content={content} />
            </div>
          </div>
        </div>

        <div className={"toast" + (toast ? " show" : "")}>{toast}</div>
      </div>
    );
  }

  // "kevinkhyp" -> "Kevin"
  function displayName(u) {
    if (!u) return "";
    let base = String(u).replace(/khyp$/i, "");
    if (!base) base = String(u);
    return base.charAt(0).toUpperCase() + base.slice(1);
  }

  // Simple shared sign-in screen (see config.js `auth`).
  function Gate({ auth, onLogin }) {
    const [u, setU] = useState("");
    const [p, setP] = useState("");
    const [err, setErr] = useState("");
    const submit = (e) => {
      if (e) e.preventDefault();
      const un = u.trim().toLowerCase();
      const valid = (auth.users || []).map((x) => String(x).toLowerCase());
      if (valid.indexOf(un) === -1) { setErr("We don't recognize that username."); return; }
      if (p !== auth.password) { setErr("That password isn't right."); return; }
      onLogin(un);
    };
    return (
      <div style={{
        minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center",
        background: "#e7eef0", backgroundImage: "radial-gradient(circle at 1px 1px,#d2dee0 1px,transparent 0)",
        backgroundSize: "22px 22px", padding: 20
      }}>
        <form onSubmit={submit} style={{
          width: 340, maxWidth: "100%", background: "#fff", borderRadius: 16,
          boxShadow: "0 16px 50px rgba(0,72,96,.16)", padding: "30px 28px", border: "1px solid #dce8ea"
        }}>
          <div style={{ display: "flex", alignItems: "center", gap: 11, marginBottom: 18 }}>
            <div style={{ width: 38, height: 38, borderRadius: 10, background: "#004860", color: "#fff",
                          display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
              <I.Heart size={20} />
            </div>
            <div>
              <div style={{ fontSize: 16, fontWeight: 800, color: "#004860", letterSpacing: "-.2px" }}>The Pulse</div>
              <div style={{ fontSize: 11.5, color: "#6D6E71" }}>Committee sign-in</div>
            </div>
          </div>
          <div className="fld">
            <label>Username</label>
            <input type="text" autoFocus autoCapitalize="none" autoCorrect="off" spellCheck={false}
                   value={u} placeholder="e.g. kevinkhyp"
                   onChange={(e) => { setU(e.target.value); setErr(""); }} />
          </div>
          <div className="fld">
            <label>Password</label>
            <input type="password" value={p} placeholder="Password"
                   onChange={(e) => { setP(e.target.value); setErr(""); }} />
          </div>
          {err && <div style={{ color: "#CB2C30", fontSize: 12, fontWeight: 600, margin: "2px 0 12px" }}>{err}</div>}
          <button type="submit" className="btn btn-prime" style={{ width: "100%", marginTop: 4 }}>Sign in</button>
        </form>
      </div>
    );
  }

  function Root() {
    const auth = (window.PULSE_CONFIG && window.PULSE_CONFIG.auth) || null;
    const [user, setUser] = useState(() => {
      if (!auth) return null;
      try {
        const saved = localStorage.getItem("pulse_auth_user");
        const valid = (auth.users || []).map((x) => String(x).toLowerCase());
        return (saved && valid.indexOf(saved.toLowerCase()) !== -1) ? saved.toLowerCase() : null;
      } catch (e) { return null; }
    });
    if (auth && !user) {
      return <Gate auth={auth} onLogin={(u) => {
        try { localStorage.setItem("pulse_auth_user", u); } catch (e) {}
        setUser(u);
      }} />;
    }
    const signOut = auth ? () => {
      try { localStorage.removeItem("pulse_auth_user"); } catch (e) {}
      setUser(null);
    } : null;
    return <App user={auth ? user : null} onSignOut={signOut} />;
  }

  ReactDOM.createRoot(document.getElementById("root")).render(<Root />);
})();
