/* Popitron Mail — the inbox, inside the app. Loaded by index.html as a Babel script.
   Talks only to /api/mail/* (the proxy); never to Stalwart directly.
   Exposes window.PopMail = { Mail } for index.html to render under the Mail tile. */
(function () {
  const { useState, useEffect, useMemo, useCallback, useRef } = React;
  const API = "/api/mail";
  const tok = () => { try { return localStorage.getItem("pop.jwt"); } catch { return null; } };
  async function req(path, init = {}) {
    const h = { Accept: "application/json", ...(init.body ? { "Content-Type": "application/json" } : {}) };
    if (tok()) h.Authorization = "Bearer " + tok();
    const r = await fetch(API + path, { ...init, headers: h, body: init.body ? JSON.stringify(init.body) : undefined });
    const t = await r.text(); let d = null; try { d = t ? JSON.parse(t) : null; } catch {}
    if (!r.ok) throw new Error((d && d.error) || `Mail request failed (${r.status})`);
    return d;
  }
  const m = {
    boxes: () => req("/mailboxes"),
    list: (mailbox, page, q) => req(`/messages?mailbox=${encodeURIComponent(mailbox || "")}&page=${page}&q=${encodeURIComponent(q || "")}`),
    get: (id) => req(`/message?id=${encodeURIComponent(id)}`),
    flag: (ids, key, value) => req("/flag", { method: "POST", body: { ids, key, value } }),
    move: (ids, to) => req("/move", { method: "POST", body: { ids, to } }),
    del: (ids) => req("/delete", { method: "POST", body: { ids } }),
    send: (msg) => req("/send", { method: "POST", body: msg }),
    blobUrl: (a) => `${API}/blob?id=${encodeURIComponent(a.blobId)}&name=${encodeURIComponent(a.name)}&type=${encodeURIComponent(a.type || "application/octet-stream")}`,
  };

  const when = (ts) => { if (!ts) return ""; const d = new Date(ts), now = new Date(); const sameDay = d.toDateString() === now.toDateString();
    if (sameDay) return d.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
    if (now - d < 6 * 86400000) return d.toLocaleDateString([], { weekday: "short" });
    return d.toLocaleDateString([], { month: "short", day: "numeric", year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined }); };
  const who = (list, me) => (list || []).map((a) => a.email === me ? "me" : (a.name || a.email || "")).join(", ");
  const fmtAddr = (a) => a.name ? `${a.name} <${a.email}>` : a.email;
  const size = (n) => n > 1048576 ? (n / 1048576).toFixed(1) + " MB" : n > 1024 ? Math.round(n / 1024) + " KB" : n + " B";
  const ROLE_LABEL = { inbox: "Inbox", drafts: "Drafts", sent: "Sent", archive: "Archive", junk: "Junk", trash: "Trash" };

  // Render HTML mail in a fully sandboxed iframe via srcdoc: no scripts, no same-origin
  // access, so nothing in a message can touch the app. Remote images are blocked by a
  // CSP inside the frame until the reader asks for them. Height is read via postMessage.
  function HtmlBody({ html, allowRemote }) {
    const ref = useRef(null); const [h, setH] = useState(160);
    const csp = allowRemote ? "default-src 'none'; img-src * data: cid:; style-src 'unsafe-inline'; font-src *; script-src 'unsafe-inline'"
                            : "default-src 'none'; img-src data: cid:; style-src 'unsafe-inline'; script-src 'unsafe-inline'";
    // the only script in the frame is ours: it reports the content height
    const doc = `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${csp}"><base target="_blank"><style>body{margin:0;padding:4px;font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;word-break:break-word}img{max-width:100%;height:auto}a{color:#2b5bd7}blockquote{border-left:3px solid #ddd;margin:8px 0;padding-left:12px;color:#555}</style></head><body>${html}<script>function r(){parent.postMessage({popmail:document.documentElement.scrollHeight},"*")}addEventListener("load",r);setTimeout(r,300);setTimeout(r,1500);</script></body></html>`;
    useEffect(() => { const on = (e) => { if (e.source === (ref.current && ref.current.contentWindow) && e.data && e.data.popmail) setH(Math.min(4000, e.data.popmail + 16)); }; window.addEventListener("message", on); return () => window.removeEventListener("message", on); }, []);
    return <iframe ref={ref} title="Message" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox" srcDoc={doc} style={{ width: "100%", height: h, border: 0, background: "#fff", borderRadius: 8 }} />;
  }

  function Compose({ me, draft, onClose, onSent, setErr }) {
    const [f, setF] = useState({ to: draft.to || "", cc: draft.cc || "", subject: draft.subject || "", text: draft.text || "" });
    const [cc, setCc] = useState(!!draft.cc); const [busy, setBusy] = useState(false);
    const send = async () => { setBusy(true); try { const sent = await m.send({ to: f.to.split(/[,;]/).map((s) => s.trim()).filter(Boolean), cc: cc ? f.cc.split(/[,;]/).map((s) => s.trim()).filter(Boolean) : [], subject: f.subject, text: f.text, inReplyTo: draft.inReplyTo, references: draft.references }); onSent(sent); } catch (e) { setErr(e.message); } finally { setBusy(false); } };
    useEffect(() => { const k = (e) => { if (e.key === "Escape") onClose(); if ((e.metaKey || e.ctrlKey) && e.key === "Enter") send(); }; window.addEventListener("keydown", k); return () => window.removeEventListener("keydown", k); });
    return (<div className="mail-compose card" role="dialog" aria-label="New message">
      <div className="row" style={{ padding: "12px 16px" }}><b>{draft.mode === "reply" ? "Reply" : draft.mode === "forward" ? "Forward" : "New message"}</b><button className="btn btn-quiet" style={{ padding: "4px 10px" }} onClick={onClose}>Close</button></div>
      <div style={{ padding: "0 16px 16px" }}>
        <div className="mail-field"><label>To</label><input value={f.to} onChange={(e) => setF({ ...f, to: e.target.value })} placeholder="name@example.com, another@example.com" autoFocus={!draft.to} /><button className="muted" style={{ fontSize: 12, whiteSpace: "nowrap" }} onClick={() => setCc(!cc)}>{cc ? "Hide Cc" : "Cc"}</button></div>
        {cc && <div className="mail-field"><label>Cc</label><input value={f.cc} onChange={(e) => setF({ ...f, cc: e.target.value })} /></div>}
        <div className="mail-field"><label>Subject</label><input value={f.subject} onChange={(e) => setF({ ...f, subject: e.target.value })} /></div>
        <textarea className="mail-body" value={f.text} onChange={(e) => setF({ ...f, text: e.target.value })} autoFocus={!!draft.to} />
        <div className="row" style={{ padding: "10px 0 0", border: 0 }}>
          <span className="muted" style={{ fontSize: 12 }}>From {me} · <span className="kbd">⌘/Ctrl</span>+<span className="kbd">Enter</span> to send</span>
          <button className="btn btn-accent" onClick={send} disabled={busy || !f.to.trim()}>{busy ? "Sending…" : "Send"}</button></div>
      </div></div>);
  }

  function Reader({ id, me, onBack, onAction, setErr, onCompose }) {
    const [msg, setMsg] = useState(null); const [remote, setRemote] = useState(false);
    useEffect(() => { setMsg(null); setRemote(false); m.get(id).then(setMsg).catch((e) => setErr(e.message)); }, [id]);
    if (!msg) return <div className="muted" style={{ padding: 24 }}>Opening…</div>;
    const quote = () => `\n\nOn ${new Date(msg.sentAt || msg.receivedAt).toLocaleString()}, ${fmtAddr(msg.from[0] || {})} wrote:\n` + (msg.text || "").split("\n").map((l) => "> " + l).join("\n");
    const reply = (all) => { const from = (msg.replyTo && msg.replyTo.length ? msg.replyTo : msg.from).map((a) => a.email).filter((e) => e !== me);
      const others = all ? [...msg.to, ...msg.cc].map((a) => a.email).filter((e) => e !== me && !from.includes(e)) : [];
      onCompose({ mode: "reply", to: from.join(", "), cc: others.join(", "), subject: /^re:/i.test(msg.subject) ? msg.subject : "Re: " + msg.subject, text: quote(), inReplyTo: (msg.messageId || [])[0], references: [...(msg.references || []), ...(msg.messageId || [])] }); };
    const forward = () => onCompose({ mode: "forward", to: "", subject: /^fwd?:/i.test(msg.subject) ? msg.subject : "Fwd: " + msg.subject, text: `\n\n---------- Forwarded message ----------\nFrom: ${fmtAddr(msg.from[0] || {})}\nDate: ${new Date(msg.sentAt || msg.receivedAt).toLocaleString()}\nSubject: ${msg.subject}\nTo: ${msg.to.map(fmtAddr).join(", ")}\n\n${msg.text || ""}` });
    const hasRemote = /<img[^>]+src=["']?https?:/i.test(msg.html || "");
    return (<article className="mail-reader">
      <div className="mail-toolbar">
        <button className="btn btn-quiet" onClick={onBack}>← Back</button>
        <span style={{ flex: 1 }} />
        <button className="btn btn-quiet" onClick={() => reply(false)}>Reply</button>
        <button className="btn btn-quiet" onClick={() => reply(true)}>Reply all</button>
        <button className="btn btn-quiet" onClick={forward}>Forward</button>
        <button className="btn btn-quiet" onClick={() => onAction("archive", [msg.id])}>Archive</button>
        <button className="btn btn-quiet" onClick={() => onAction("trash", [msg.id])}>Trash</button>
      </div>
      <h2 className="display" style={{ fontSize: 22, margin: "14px 0 8px", wordBreak: "break-word" }}>{msg.subject}</h2>
      <div className="mail-meta">
        {/* Two sender addresses: the one the message shows, and the one it was really sent with. */}
        {(() => { const sd = msg.sender; if (!sd) return <div><b>{fmtAddr(msg.from[0] || {})}</b></div>;
          const differs = sd.sent_by && !sd.same_sender;
          return (<>
            <div className="mail-who"><span className="mail-k">From</span><b>{fmtAddr(msg.from[0] || {})}</b>
              {sd.status === "spoof" ? <span className="mail-tag spoof">{sd.label}</span> : sd.status === "verified" ? <span className="mail-tag ok">Verified</span> : <span className="mail-tag">Not verified</span>}</div>
            {differs && <div className="mail-who"><span className="mail-k">Sent by</span><code>{sd.sent_by}</code>
              <span className="mail-tag">{sd.status === "verified" ? "Sending service" : "Actual sender"}</span></div>}
          </>); })()}
        <div className="muted">to {msg.to.map(fmtAddr).join(", ")}{msg.cc.length ? ` · cc ${msg.cc.map(fmtAddr).join(", ")}` : ""}</div>
        <div className="muted">{new Date(msg.sentAt || msg.receivedAt).toLocaleString()}</div>
      </div>
      {msg.sender && msg.sender.status === "spoof" && <div className="mail-warn" role="alert"><b>{msg.sender.label}.</b> {msg.sender.detail}
        {msg.sender.reply_to ? ` A reply would go to ${msg.sender.reply_to}.` : ""} Be careful with links, attachments, and any request for money or a password.</div>}
      {msg.sender && msg.sender.status !== "spoof" && (msg.sender.status === "unverified" || (msg.sender.sent_by && !msg.sender.same_sender) || msg.sender.reply_to) &&
        <div className="mail-notice">{msg.sender.detail}{msg.sender.reply_to ? ` A reply would go to ${msg.sender.reply_to}.` : ""}</div>}
      {msg.attachments.length > 0 && <div className="mail-atts">{msg.attachments.map((a, i) => <a key={i} className="mail-att" href={m.blobUrl(a)} download={a.name}>{a.name} <span className="muted">{size(a.size || 0)}</span></a>)}</div>}
      {hasRemote && !remote && <div className="mail-notice">Remote images are hidden. <button onClick={() => setRemote(true)}>Show images</button></div>}
      <div className="mail-bodywrap">{msg.html ? <HtmlBody html={msg.html} allowRemote={remote} /> : <pre className="mail-text">{msg.text}</pre>}</div>
    </article>);
  }

  function Mail({ me, setErr, setOk }) {
    const [boxes, setBoxes] = useState([]); const [box, setBox] = useState(null); const [list, setList] = useState({ items: [], total: 0, page: 0 });
    const [open, setOpen] = useState(null); const [q, setQ] = useState(""); const [sel, setSel] = useState(new Set()); const [compose, setCompose] = useState(null); const [loading, setLoading] = useState(true); const [unavailable, setUnavailable] = useState("");
    const loadBoxes = useCallback(async () => { try { const b = await m.boxes(); setBoxes(b); setUnavailable(""); if (!box) { const inbox = b.find((x) => x.role === "inbox") || b[0]; if (inbox) setBox(inbox.id); } } catch (e) { setUnavailable(e.message); } }, [box]);
    const loadList = useCallback(async (page = 0) => { if (!box) return; setLoading(true); try { setList(await m.list(box, page, q)); setSel(new Set()); } catch (e) { setErr(e.message); } finally { setLoading(false); } }, [box, q]);
    useEffect(() => { loadBoxes(); const t = setInterval(loadBoxes, 30000); return () => clearInterval(t); }, [loadBoxes]);
    useEffect(() => { const t = setTimeout(() => loadList(0), q ? 300 : 0); return () => clearTimeout(t); }, [loadList]);
    useEffect(() => { if (!box || open || compose) return; const t = setInterval(() => loadList(list.page), 45000); return () => clearInterval(t); }, [box, open, compose, list.page, loadList]);
    const cur = boxes.find((b) => b.id === box);
    const act = async (what, ids) => { try {
        if (what === "archive" || what === "trash" || what === "inbox") await m.move(ids, what);
        else if (what === "delete") { if (!confirm(`Permanently delete ${ids.length} message${ids.length > 1 ? "s" : ""}?`)) return; await m.del(ids); }
        else if (what === "read") await m.flag(ids, "seen", true); else if (what === "unread") await m.flag(ids, "seen", false);
        else if (what === "flag") await m.flag(ids, "flagged", true); else if (what === "unflag") await m.flag(ids, "flagged", false);
        setOk({ archive: "Archived.", trash: "Moved to Trash.", inbox: "Moved to Inbox.", delete: "Deleted.", read: "Marked read.", unread: "Marked unread.", flag: "Flagged.", unflag: "Flag removed." }[what]);
        if (open && ids.includes(open)) setOpen(null);
        await loadList(list.page); loadBoxes();
      } catch (e) { setErr(e.message); } };
    // keyboard: j/k move, enter open, e archive, # trash, u back
    const [cursor, setCursor] = useState(0);
    useEffect(() => { const k = (e) => { if (compose || e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return;
        if (open) { if (e.key === "u" || e.key === "Escape") setOpen(null); if (e.key === "e") act("archive", [open]); if (e.key === "#") act("trash", [open]); return; }
        if (e.key === "j") setCursor((c) => Math.min(list.items.length - 1, c + 1)); if (e.key === "k") setCursor((c) => Math.max(0, c - 1));
        if (e.key === "Enter" && list.items[cursor]) setOpen(list.items[cursor].id); if (e.key === "e" && list.items[cursor]) act("archive", [list.items[cursor].id]);
        if (e.key === "#" && list.items[cursor]) act("trash", [list.items[cursor].id]); if (e.key === "c") setCompose({ mode: "new" }); };
      window.addEventListener("keydown", k); return () => window.removeEventListener("keydown", k); }, [open, compose, list.items, cursor]);
    if (unavailable) return (<div className="card doorcard"><div className="display" style={{ fontSize: 22 }}>Mail isn't ready yet</div><p className="muted" style={{ marginBottom: 0 }}>{unavailable}</p></div>);
    const pages = Math.max(1, Math.ceil(list.total / (list.pageSize || 50)));
    const toggle = (id) => { const s = new Set(sel); s.has(id) ? s.delete(id) : s.add(id); setSel(s); };
    return (<div className="mail">
      <aside className="mail-rail">
        <button className="btn btn-accent" style={{ width: "100%", marginBottom: 12 }} onClick={() => setCompose({ mode: "new" })}>New message <span className="kbd" style={{ opacity: .8 }}>c</span></button>
        {boxes.map((b) => (<button key={b.id} className={"mail-box" + (b.id === box ? " on" : "")} onClick={() => { setBox(b.id); setOpen(null); setQ(""); }}>
          <span>{ROLE_LABEL[b.role] || b.name}</span>{b.unread > 0 && <span className="mail-unread">{b.unread}</span>}</button>))}
      </aside>
      <section className="mail-main">
        {compose && <Compose me={me} draft={compose} onClose={() => setCompose(null)} onSent={(r) => { setCompose(null); const bad = Object.entries((r && r.refused) || {}); if (bad.length) setErr(`Sent, but not to everyone. Refused: ${bad.map(([k, v]) => `${k} (${v})`).join("; ")}`);
      const a = (r && r.allowed) || []; if (!bad.length) setOk(a.length ? `Sent. ${a.join(", ")} ${a.length === 1 ? "is" : "are"} now on your White list, so replies will reach you.` : "Sent."); loadBoxes(); }} setErr={setErr} />}
        {open ? <Reader id={open} me={me} onBack={() => setOpen(null)} onAction={act} setErr={setErr} onCompose={setCompose} /> : (<>
          <div className="mail-toolbar">
            <input type="search" placeholder={`Search ${cur ? (ROLE_LABEL[cur.role] || cur.name) : "mail"}`} value={q} onChange={(e) => setQ(e.target.value)} aria-label="Search mail" style={{ flex: 1, padding: "9px 12px" }} />
            {sel.size > 0 && <>
              <button className="btn btn-quiet" onClick={() => act("archive", [...sel])}>Archive</button>
              <button className="btn btn-quiet" onClick={() => act("trash", [...sel])}>Trash</button>
              <button className="btn btn-quiet" onClick={() => act("read", [...sel])}>Read</button>
              <button className="btn btn-quiet" onClick={() => act("unread", [...sel])}>Unread</button>
              {cur && cur.role === "trash" && <button className="btn btn-quiet" style={{ color: "var(--deny)" }} onClick={() => act("delete", [...sel])}>Delete forever</button>}
            </>}
          </div>
          <div className="card mail-list">
            {loading && list.items.length === 0 && <div className="muted" style={{ padding: 24 }}>Loading…</div>}
            {!loading && list.items.length === 0 && <div className="muted" style={{ padding: 32, textAlign: "center" }}>{q ? "Nothing matches." : cur && cur.role === "inbox" ? "Inbox zero. Anyone new who writes to you waits at your Grey list first." : "Nothing here."}</div>}
            {list.items.map((it, i) => (<div key={it.id} className={"mail-row" + (it.seen ? "" : " unread") + (i === cursor ? " cur" : "") + (sel.has(it.id) ? " sel" : "")} onClick={() => setOpen(it.id)} onMouseEnter={() => setCursor(i)}>
              <input type="checkbox" checked={sel.has(it.id)} onChange={() => toggle(it.id)} onClick={(e) => e.stopPropagation()} aria-label="Select" />
              <button className={"mail-star" + (it.flagged ? " on" : "")} onClick={(e) => { e.stopPropagation(); act(it.flagged ? "unflag" : "flag", [it.id]); }} aria-label={it.flagged ? "Remove flag" : "Flag"}>★</button>
              <span className="mail-from">{cur && cur.role === "sent" ? "To: " + who(it.to, me) : who(it.from, me)}</span>
              <span className="mail-subj"><span>{it.subject}</span><span className="muted"> — {it.preview}</span></span>
              {it.hasAttachment && <span className="muted" title="Has attachment">📎</span>}
              <span className="mail-when muted">{when(it.receivedAt)}</span>
            </div>))}
          </div>
          {pages > 1 && <div className="row" style={{ justifyContent: "center", border: 0, gap: 12 }}>
            <button className="btn btn-quiet" disabled={list.page === 0} onClick={() => loadList(list.page - 1)}>Newer</button>
            <span className="muted">{list.page * (list.pageSize || 50) + 1}–{Math.min(list.total, (list.page + 1) * (list.pageSize || 50))} of {list.total}</span>
            <button className="btn btn-quiet" disabled={list.page >= pages - 1} onClick={() => loadList(list.page + 1)}>Older</button></div>}
          <p className="muted" style={{ fontSize: 12, textAlign: "center" }}><span className="kbd">j</span>/<span className="kbd">k</span> move · <span className="kbd">Enter</span> open · <span className="kbd">e</span> archive · <span className="kbd">#</span> trash · <span className="kbd">c</span> compose</p>
        </>)}
      </section>
    </div>);
  }

  window.PopMail = { Mail };
})();
