// vault.jsx — Password Vault.
//
// Sections of this file
//   1. VaultView                — the page itself (search + filters + list).
//   2. VaultEntryCard           — one row in the list with copy / reveal.
//   3. VaultEntryModal          — create / edit modal.
//   4. VaultShareModal          — manage who can see this entry.
//   5. VaultAuditModal          — read the audit log of reveal events.
//   6. VAULT_CSS                — single style string emitted once on mount.
//
// Security notes
//   - The list never carries decrypted secrets; the server returns
//     metadata-only. A reveal is an explicit GET /entries/:id/secret
//     that's audit-logged, even for the owner.
//   - The reveal button auto-re-masks after AUTO_HIDE_MS so the
//     password isn't left on screen if the user walks away.
//   - Clipboard copies clear themselves after CLIPBOARD_CLEAR_MS
//     when the browser supports it (best-effort; see _scheduleClear).
//   - Inputs use type=password by default; a small reveal toggle
//     flips to text only when the user is actively reading.

const VAULT_CATEGORIES = [
  { id: "login",    label: "Login",       glyph: "🔑" },
  { id: "card",     label: "Card",        glyph: "💳" },
  { id: "note",     label: "Secure note", glyph: "📝" },
  { id: "api_key",  label: "API key",     glyph: "🔌" },
  { id: "identity", label: "Identity",    glyph: "🪪" },
  { id: "wifi",     label: "Wi-Fi",       glyph: "📶" },
  { id: "database", label: "Database",    glyph: "🗄️" },
];
const VAULT_CAT_BY_ID = Object.fromEntries(VAULT_CATEGORIES.map(c => [c.id, c]));

const AUTO_HIDE_MS = 30_000;
const CLIPBOARD_CLEAR_MS = 30_000;

function _vaultCat(id) { return VAULT_CAT_BY_ID[id] || VAULT_CATEGORIES[0]; }
function _vaultRel(iso) { return typeof fmtRelative === "function" ? fmtRelative(iso) : (iso || ""); }

// Best-effort clipboard helper: writes the string, then schedules a
// clear on the same value. If the user copies something else in the
// meantime, the clear is a no-op.
async function _vaultCopy(text, onToast) {
  let ok = false;
  try {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(text);
      ok = true;
    }
  } catch {}
  if (!ok) {
    try {
      const ta = document.createElement("textarea");
      ta.value = text;
      ta.style.position = "fixed"; ta.style.opacity = "0";
      document.body.appendChild(ta);
      ta.select();
      ok = document.execCommand("copy");
      document.body.removeChild(ta);
    } catch { ok = false; }
  }
  if (ok) {
    onToast && onToast("Copied · clipboard clears in 30s");
    _scheduleClear(text);
  } else if (onToast) {
    onToast("Couldn't copy — your browser blocked clipboard access", 4000);
  }
  return ok;
}
function _scheduleClear(value) {
  setTimeout(async () => {
    try {
      if (navigator.clipboard && navigator.clipboard.readText && navigator.clipboard.writeText) {
        const cur = await navigator.clipboard.readText();
        if (cur === value) await navigator.clipboard.writeText("");
      }
    } catch { /* clipboard read often blocked — that's fine */ }
  }, CLIPBOARD_CLEAR_MS);
}

// ── Page ────────────────────────────────────────────────────────────
function VaultView({ currentUserId, people, onToast }) {
  const [entries, setEntries] = React.useState(null);   // null = loading
  const [err, setErr]         = React.useState("");
  const [q, setQ]             = React.useState("");
  const [filter, setFilter]   = React.useState("all");  // all | mine | shared | favorite
  const [category, setCategory] = React.useState("any");
  const [editing, setEditing] = React.useState(null);   // null | {} (new) | entry (edit)
  const [sharing, setSharing] = React.useState(null);   // entry being shared
  const [auditing, setAuditing] = React.useState(null); // entry whose audit is open

  const reload = React.useCallback(async () => {
    try {
      const list = await api.vault.list();
      setEntries(Array.isArray(list) ? list : []);
      setErr("");
    } catch (e) {
      setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't load vault");
      setEntries([]);
    }
  }, []);
  React.useEffect(() => { reload(); }, [reload]);

  const counts = React.useMemo(() => {
    const e = entries || [];
    return {
      all: e.length,
      mine: e.filter(x => x.permission === "owner").length,
      shared: e.filter(x => x.permission !== "owner").length,
      favorite: e.filter(x => x.favorite).length,
    };
  }, [entries]);

  const filtered = React.useMemo(() => {
    const arr = (entries || []).filter(x => {
      if (filter === "mine"     && x.permission !== "owner") return false;
      if (filter === "shared"   && x.permission === "owner") return false;
      if (filter === "favorite" && !x.favorite) return false;
      if (category !== "any" && x.category !== category) return false;
      if (q.trim()) {
        const s = q.trim().toLowerCase();
        return (
          (x.title    || "").toLowerCase().includes(s) ||
          (x.username || "").toLowerCase().includes(s) ||
          (x.url      || "").toLowerCase().includes(s) ||
          (x.notes_plain || "").toLowerCase().includes(s)
        );
      }
      return true;
    });
    arr.sort((a, b) => {
      if ((b.favorite ? 1 : 0) !== (a.favorite ? 1 : 0)) return (b.favorite ? 1 : 0) - (a.favorite ? 1 : 0);
      return (a.title || "").localeCompare(b.title || "");
    });
    return arr;
  }, [entries, filter, category, q]);

  function patchInPlace(id, patch) {
    setEntries(es => (es || []).map(x => x.id === id ? { ...x, ...patch } : x));
  }
  async function toggleFavorite(entry) {
    const next = !entry.favorite;
    patchInPlace(entry.id, { favorite: next });
    try { await api.vault.patch(entry.id, { favorite: next }); }
    catch (e) {
      patchInPlace(entry.id, { favorite: !next });
      onToast && onToast("Couldn't update favorite", 4000);
    }
  }
  async function deleteEntry(entry) {
    if (!window.confirm(`Permanently delete "${entry.title}"?\n\nThis removes the encrypted entry AND every share. There is no undo.`)) return;
    try {
      await api.vault.remove(entry.id);
      setEntries(es => (es || []).filter(x => x.id !== entry.id));
      onToast && onToast("Entry deleted");
    } catch (e) {
      onToast && onToast("Couldn't delete: " + (e.message || "network error"), 4000);
    }
  }

  return (
    <div className="vault-page">
      <header className="vault-head">
        <div className="vault-head-titles">
          <h1>🔒 Password vault</h1>
          <div className="vault-head-sub">
            Encrypted at rest with AES-256-GCM · share with teammates one entry at a time
          </div>
        </div>
        <div className="vault-head-actions">
          <button className="btn btn-primary" onClick={() => setEditing({})}>
            + New entry
          </button>
        </div>
      </header>

      <div className="vault-toolbar">
        <input
          className="search-input vault-search"
          placeholder="Search by title, username, URL, or notes…"
          value={q}
          onChange={e => setQ(e.target.value)}
        />
        <div className="vault-filters" role="tablist">
          {[
            ["all",      "All"],
            ["mine",     "Mine"],
            ["shared",   "Shared"],
            ["favorite", "★ Favorites"],
          ].map(([id, label]) => (
            <button
              key={id}
              role="tab"
              aria-selected={filter === id}
              className={`vault-filter-pill ${filter === id ? "is-active" : ""}`}
              onClick={() => setFilter(id)}>
              {label} <span className="vault-filter-count">{counts[id] || 0}</span>
            </button>
          ))}
        </div>
        <select
          className="search-input vault-cat-select"
          value={category}
          onChange={e => setCategory(e.target.value)}>
          <option value="any">All categories</option>
          {VAULT_CATEGORIES.map(c => (
            <option key={c.id} value={c.id}>{c.glyph}  {c.label}</option>
          ))}
        </select>
      </div>

      {err && <div className="vault-error">{err}</div>}

      {entries === null && (
        <div className="vault-empty">Loading vault…</div>
      )}

      {entries !== null && filtered.length === 0 && (
        <div className="vault-empty">
          {q.trim() || filter !== "all" || category !== "any"
            ? "No entries match — try a different filter"
            : <>
                <div style={{ fontSize: 36, marginBottom: 8 }}>🔐</div>
                <div style={{ fontWeight: 700, marginBottom: 4 }}>Your vault is empty</div>
                <div>Save your first password — it's encrypted with AES-256-GCM before it touches the database.</div>
                <button className="btn btn-primary" style={{ marginTop: 14 }} onClick={() => setEditing({})}>
                  + New entry
                </button>
              </>}
        </div>
      )}

      {entries !== null && filtered.length > 0 && (
        <div className="vault-grid">
          {filtered.map(entry => (
            <VaultEntryCard
              key={entry.id}
              entry={entry}
              currentUserId={currentUserId}
              onEdit={() => setEditing(entry)}
              onShare={() => setSharing(entry)}
              onAudit={() => setAuditing(entry)}
              onDelete={() => deleteEntry(entry)}
              onToggleFavorite={() => toggleFavorite(entry)}
              onToast={onToast}/>
          ))}
        </div>
      )}

      {editing !== null && (
        <VaultEntryModal
          entry={editing && editing.id ? editing : null}
          onClose={() => setEditing(null)}
          onSaved={(saved, action) => {
            setEditing(null);
            if (action === "created") setEntries(es => [...(es || []), saved]);
            else patchInPlace(saved.id, saved);
            onToast && onToast(action === "created" ? "Entry saved" : "Entry updated");
          }}
          onError={msg => onToast && onToast(msg, 4500)}/>
      )}

      {sharing && (
        <VaultShareModal
          entry={sharing}
          people={people}
          currentUserId={currentUserId}
          onClose={() => setSharing(null)}
          onChanged={(meta) => {
            // Refresh the entry's shared_with_count + is_private flag.
            patchInPlace(sharing.id, meta);
          }}
          onToast={onToast}/>
      )}

      {auditing && (
        <VaultAuditModal
          entry={auditing}
          onClose={() => setAuditing(null)}/>
      )}

      <style>{VAULT_CSS}</style>
    </div>
  );
}

// ── Entry card ──────────────────────────────────────────────────────
function VaultEntryCard({ entry, currentUserId, onEdit, onShare, onAudit, onDelete, onToggleFavorite, onToast }) {
  const [revealed, setRevealed] = React.useState(false);
  const [secret, setSecret]     = React.useState(null);
  const [busy, setBusy]         = React.useState(false);
  const hideTimerRef = React.useRef(null);

  const isOwner  = entry.permission === "owner";
  const canEdit  = isOwner || entry.permission === "editor";
  const cat = _vaultCat(entry.category);

  React.useEffect(() => () => { if (hideTimerRef.current) clearTimeout(hideTimerRef.current); }, []);

  async function reveal() {
    if (revealed) {
      setRevealed(false);
      if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
      return;
    }
    if (!secret) {
      setBusy(true);
      try {
        const r = await api.vault.reveal(entry.id);
        setSecret(r.secret);
      } catch (e) {
        const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't reveal";
        onToast && onToast(msg, 4500);
        setBusy(false);
        return;
      }
      setBusy(false);
    }
    setRevealed(true);
    if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
    hideTimerRef.current = setTimeout(() => {
      setRevealed(false);
      setSecret(null);          // forget on auto-hide so re-reveal hits the server again (re-audits)
    }, AUTO_HIDE_MS);
  }
  async function copy() {
    let value = secret;
    if (!value) {
      try {
        const r = await api.vault.reveal(entry.id);
        value = r.secret;
      } catch (e) {
        const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't reveal";
        onToast && onToast(msg, 4500);
        return;
      }
    }
    await _vaultCopy(value, onToast);
  }
  async function copyUsername() {
    if (!entry.username) return;
    await _vaultCopy(entry.username, onToast);
  }
  function openURL() {
    if (!entry.url) return;
    let u = entry.url.trim();
    if (!/^https?:\/\//i.test(u)) u = "https://" + u;
    try { window.open(u, "_blank", "noopener,noreferrer"); } catch {}
  }

  return (
    <div className={`vault-card cat-${entry.category}`}>
      <div className="vault-card-head">
        <div className="vault-card-glyph" aria-hidden="true">{cat.glyph}</div>
        <div className="vault-card-titles">
          <div className="vault-card-title" title={entry.title}>{entry.title}</div>
          <div className="vault-card-meta">
            {entry.username && (
              <span className="vault-card-meta-piece">{entry.username}</span>
            )}
            {entry.url && (
              <a className="vault-card-meta-piece is-link" onClick={openURL} title="Open in new tab">
                {(() => { try { return new URL(/^https?:\/\//i.test(entry.url) ? entry.url : "https://" + entry.url).hostname; } catch { return entry.url; } })()}
              </a>
            )}
          </div>
        </div>
        <button
          type="button"
          className={`vault-fav ${entry.favorite ? "is-on" : ""}`}
          onClick={onToggleFavorite}
          title={entry.favorite ? "Unfavorite" : "Favorite"}>
          ★
        </button>
      </div>

      <div className="vault-card-secret-row">
        <input
          className="vault-card-secret"
          type={revealed ? "text" : "password"}
          value={revealed && secret ? secret : "••••••••••••"}
          readOnly
          aria-label="Stored password"
        />
        <button
          type="button"
          className="vault-card-icon-btn"
          onClick={reveal}
          disabled={busy}
          title={revealed ? "Hide" : "Reveal · auto-hides after 30s"}>
          {busy ? "…" : (revealed ? "🙈" : "👁")}
        </button>
        <button
          type="button"
          className="vault-card-icon-btn"
          onClick={copy}
          title="Copy password to clipboard">
          📋
        </button>
      </div>

      <div className="vault-card-foot">
        <div className="vault-card-foot-tags">
          <span className={`vault-chip is-${entry.permission}`}>
            {entry.permission === "owner"  ? "Mine"
             : entry.permission === "editor" ? "Editor"
             : "Viewer"}
          </span>
          {entry.permission === "owner" && entry.shared_with_count > 0 && (
            <span className="vault-chip is-shared">
              Shared · {entry.shared_with_count}
            </span>
          )}
          {entry.permission === "owner" && entry.shared_with_count === 0 && (
            <span className="vault-chip is-private">Private</span>
          )}
          {entry.permission !== "owner" && (
            <span className="vault-chip is-from">
              from {entry.owner_name || "—"}
            </span>
          )}
        </div>
        <div className="vault-card-foot-actions">
          {entry.username && (
            <button type="button" className="vault-card-text-btn" onClick={copyUsername} title="Copy username">
              Copy user
            </button>
          )}
          {canEdit && (
            <button type="button" className="vault-card-text-btn" onClick={onEdit}>Edit</button>
          )}
          {isOwner && (
            <>
              <button type="button" className="vault-card-text-btn" onClick={onShare}>Share</button>
              <button type="button" className="vault-card-text-btn" onClick={onAudit} title="Who looked at this">Audit</button>
              <button type="button" className="vault-card-text-btn vault-card-danger" onClick={onDelete}>Delete</button>
            </>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Entry create/edit modal ─────────────────────────────────────────
function VaultEntryModal({ entry, onClose, onSaved, onError }) {
  const isNew = !entry;
  const [title, setTitle]       = React.useState(entry?.title || "");
  const [username, setUsername] = React.useState(entry?.username || "");
  const [url, setUrl]           = React.useState(entry?.url || "");
  const [category, setCategory] = React.useState(entry?.category || "login");
  const [notes, setNotes]       = React.useState(entry?.notes_plain || "");
  const [secret, setSecret]     = React.useState("");
  const [revealNew, setRevealNew] = React.useState(false);
  const [busy, setBusy]         = React.useState(false);
  const [loadingSecret, setLoadingSecret] = React.useState(false);
  const [favorite, setFavorite] = React.useState(!!entry?.favorite);

  // For edit mode: we DON'T pre-load the secret. The user has to
  // explicitly reveal it (and accept an audit-log entry) before
  // editing it. Empty `secret` field on save means "don't change the
  // password" — only metadata is patched.
  async function loadCurrentSecret() {
    if (!entry || !entry.id) return;
    setLoadingSecret(true);
    try {
      const r = await api.vault.reveal(entry.id);
      setSecret(r.secret || "");
      setRevealNew(true);
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't reveal";
      onError && onError(msg);
    } finally { setLoadingSecret(false); }
  }

  function genPassword() {
    // Cryptographically-strong default — 20 chars, mixed-case +
    // digits + symbols. Uses crypto.getRandomValues; falls back to
    // Math.random only if that's missing (really old browsers).
    const charset = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*-_+=";
    const len = 20;
    let out = "";
    if (window.crypto && window.crypto.getRandomValues) {
      const arr = new Uint32Array(len);
      window.crypto.getRandomValues(arr);
      for (let i = 0; i < len; i++) out += charset[arr[i] % charset.length];
    } else {
      for (let i = 0; i < len; i++) out += charset[Math.floor(Math.random() * charset.length)];
    }
    setSecret(out);
    setRevealNew(true);
  }

  async function save() {
    if (!title.trim()) { onError && onError("Title is required"); return; }
    if (isNew && !secret) { onError && onError("Password / secret is required"); return; }
    setBusy(true);
    try {
      const body = {
        title: title.trim(), username: username.trim() || null,
        url: url.trim() || null, category,
        notes_plain: notes.trim() || null,
        favorite,
      };
      if (secret) body.secret = secret;
      let saved;
      if (isNew) {
        saved = await api.vault.create(body);
        onSaved && onSaved(saved, "created");
      } else {
        await api.vault.patch(entry.id, body);
        saved = { ...entry, ...body, secret: undefined };  // don't keep secret in memory
        onSaved && onSaved(saved, "updated");
      }
    } catch (e) {
      const msg = (e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't save";
      onError && onError(msg);
      setBusy(false);
    }
  }

  return (
    <div className="modal-backdrop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ width: 580 }} onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <div>
            <div className="modal-title">{isNew ? "New vault entry" : "Edit entry"}</div>
            <div className="modal-subtitle">Encrypted with AES-256-GCM before saving</div>
          </div>
          <button className="modal-close" onClick={onClose}>✕</button>
        </div>
        <div className="modal-body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <label className="ms-row">
            <span className="ms-label">Title</span>
            <input type="text" className="ms-input" autoFocus
                   value={title} onChange={e => setTitle(e.target.value)}
                   placeholder="e.g. AWS root account, Production DB"/>
          </label>

          <label className="ms-row">
            <span className="ms-label">Category</span>
            <select className="ms-input" value={category}
                    onChange={e => setCategory(e.target.value)}>
              {VAULT_CATEGORIES.map(c => (
                <option key={c.id} value={c.id}>{c.glyph}  {c.label}</option>
              ))}
            </select>
          </label>

          <div className="ms-row-split">
            <label className="ms-row">
              <span className="ms-label">Username / email</span>
              <input type="text" className="ms-input"
                     value={username} onChange={e => setUsername(e.target.value)}
                     placeholder="Optional"/>
            </label>
            <label className="ms-row">
              <span className="ms-label">URL</span>
              <input type="url" className="ms-input"
                     value={url} onChange={e => setUrl(e.target.value)}
                     placeholder="https://…"/>
            </label>
          </div>

          <div className="ms-row">
            <span className="ms-label">
              <span>Password / secret {!isNew && <span className="ms-optional">(leave blank to keep current)</span>}</span>
              <span className="vault-secret-tools">
                <button type="button" className="vault-card-text-btn" onClick={genPassword}>Generate</button>
                {!isNew && (
                  <button type="button" className="vault-card-text-btn" onClick={loadCurrentSecret} disabled={loadingSecret}>
                    {loadingSecret ? "Revealing…" : "Reveal current"}
                  </button>
                )}
              </span>
            </span>
            <div className="vault-secret-input-row">
              <input
                className="ms-input"
                type={revealNew ? "text" : "password"}
                value={secret}
                onChange={e => setSecret(e.target.value)}
                placeholder={isNew ? "Type or click Generate" : "Leave blank to keep the existing password"}
              />
              <button type="button" className="vault-card-icon-btn"
                      onClick={() => setRevealNew(r => !r)}
                      title={revealNew ? "Hide" : "Show"}>
                {revealNew ? "🙈" : "👁"}
              </button>
            </div>
          </div>

          <label className="ms-row">
            <span className="ms-label">Notes <span className="ms-optional">(plain — searchable)</span></span>
            <textarea className="ms-input" rows={3}
                      value={notes} onChange={e => setNotes(e.target.value)}
                      placeholder="Reminders, recovery hints, environment context…"/>
          </label>

          <label className="vault-fav-row">
            <input type="checkbox" checked={favorite} onChange={e => setFavorite(e.target.checked)}/>
            <span>Favorite — pin to the top of the vault</span>
          </label>
        </div>
        <div className="modal-footer">
          <button className="btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn-primary" onClick={save} disabled={busy || !title.trim() || (isNew && !secret)}>
            {busy ? "Saving…" : (isNew ? "Save entry" : "Save changes")}
          </button>
        </div>
      </div>
    </div>
  );
}

// ── Share modal ─────────────────────────────────────────────────────
function VaultShareModal({ entry, people, currentUserId, onClose, onChanged, onToast }) {
  const [shares, setShares] = React.useState(null);
  const [pickedId, setPickedId] = React.useState("");
  const [permission, setPermission] = React.useState("viewer");
  const [busy, setBusy] = React.useState(false);
  const [q, setQ] = React.useState("");

  React.useEffect(() => {
    let alive = true;
    api.vault.shares(entry.id)
      .then(rows => { if (alive) setShares(Array.isArray(rows) ? rows : []); })
      .catch(() => { if (alive) setShares([]); });
    return () => { alive = false; };
  }, [entry.id]);

  const sharedIds = new Set((shares || []).map(s => s.user_id));
  const candidates = (people || [])
    .filter(p => p.status !== "deactivated")
    .filter(p => p.id !== currentUserId)
    .filter(p => !sharedIds.has(p.id))
    .filter(p => {
      if (!q.trim()) return true;
      const s = q.trim().toLowerCase();
      return (p.name || "").toLowerCase().includes(s) || (p.email || "").toLowerCase().includes(s);
    });

  async function add() {
    if (!pickedId) return;
    setBusy(true);
    try {
      await api.vault.share(entry.id, { user_id: pickedId, permission });
      const target = (people || []).find(p => p.id === pickedId);
      const newRow = {
        user_id: pickedId, permission, shared_at: new Date().toISOString(),
        name: target?.name, email: target?.email, avatar: target?.avatar, color: target?.color,
      };
      setShares(s => [...(s || []), newRow]);
      setPickedId(""); setQ("");
      onChanged && onChanged({
        is_private: false,
        shared_with_count: (shares || []).length + 1,
      });
      onToast && onToast(`Shared with ${target?.name || "user"}`);
    } catch (e) {
      onToast && onToast((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't share", 4000);
    } finally { setBusy(false); }
  }
  async function changePerm(uid, perm) {
    try {
      await api.vault.share(entry.id, { user_id: uid, permission: perm });
      setShares(s => (s || []).map(r => r.user_id === uid ? { ...r, permission: perm } : r));
    } catch (e) {
      onToast && onToast("Couldn't change permission", 4000);
    }
  }
  async function remove(uid) {
    try {
      await api.vault.unshare(entry.id, uid);
      const next = (shares || []).filter(r => r.user_id !== uid);
      setShares(next);
      onChanged && onChanged({
        shared_with_count: next.length,
        is_private: next.length === 0,
      });
    } catch (e) {
      onToast && onToast("Couldn't remove access", 4000);
    }
  }

  return (
    <div className="modal-backdrop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ width: 540 }} onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <div>
            <div className="modal-title">Share “{entry.title}”</div>
            <div className="modal-subtitle">
              Pick a teammate · they can reveal the password but can't re-share it
            </div>
          </div>
          <button className="modal-close" onClick={onClose}>✕</button>
        </div>
        <div className="modal-body" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <div className="vault-share-add">
            <select className="ms-input" value={pickedId}
                    onChange={e => setPickedId(e.target.value)}>
              <option value="">— Pick a person —</option>
              {candidates.map(p => (
                <option key={p.id} value={p.id}>{p.name} · {p.email}</option>
              ))}
            </select>
            <select className="ms-input vault-share-perm"
                    value={permission}
                    onChange={e => setPermission(e.target.value)}>
              <option value="viewer">Viewer · reveal only</option>
              <option value="editor">Editor · reveal + edit</option>
            </select>
            <button className="btn btn-primary" disabled={!pickedId || busy} onClick={add}>
              {busy ? "Sharing…" : "Share"}
            </button>
          </div>

          {shares === null && (
            <div className="vault-empty" style={{ padding: 18 }}>Loading…</div>
          )}
          {shares !== null && shares.length === 0 && (
            <div className="vault-empty" style={{ padding: 18 }}>
              Not shared with anyone yet. Pick a teammate above.
            </div>
          )}
          {shares && shares.length > 0 && (
            <div className="vault-share-list">
              {shares.map(s => (
                <div key={s.user_id} className="vault-share-row">
                  {s.avatar
                    ? <img className="owner-dash-avatar" src={s.avatar} alt=""/>
                    : <span className="owner-dash-avatar owner-dash-avatar-fallback"
                            style={{ background: s.color || "#b3b8c2" }}>
                        {String(s.name || "?").trim().split(/\s+/).map(x => x[0]).slice(0,2).join("").toUpperCase()}
                      </span>}
                  <div className="vault-share-row-info">
                    <div className="vault-share-row-name">{s.name}</div>
                    <div className="vault-share-row-sub">{s.email}</div>
                  </div>
                  <select className="ms-input vault-share-row-perm"
                          value={s.permission}
                          onChange={e => changePerm(s.user_id, e.target.value)}>
                    <option value="viewer">Viewer</option>
                    <option value="editor">Editor</option>
                  </select>
                  <button className="vault-card-text-btn vault-card-danger"
                          onClick={() => remove(s.user_id)}>Remove</button>
                </div>
              ))}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn-primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

// ── Audit modal ─────────────────────────────────────────────────────
function VaultAuditModal({ entry, onClose }) {
  const [rows, setRows] = React.useState(null);
  const [err, setErr]   = React.useState("");

  React.useEffect(() => {
    let alive = true;
    api.vault.audit(entry.id)
      .then(r => { if (alive) setRows(Array.isArray(r) ? r : []); })
      .catch(e => {
        if (!alive) return;
        setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Couldn't load audit log");
        setRows([]);
      });
    return () => { alive = false; };
  }, [entry.id]);

  function actionLabel(a) {
    if (a === "viewed")   return { label: "Revealed", tone: "view" };
    if (a === "created")  return { label: "Created",  tone: "ok" };
    if (a === "updated")  return { label: "Edited",   tone: "ok" };
    if (a === "deleted")  return { label: "Deleted",  tone: "danger" };
    if (a === "shared")   return { label: "Shared",   tone: "info" };
    if (a === "unshared") return { label: "Unshared", tone: "info" };
    return { label: a, tone: "view" };
  }

  return (
    <div className="modal-backdrop" onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="modal" style={{ width: 520 }} onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <div>
            <div className="modal-title">Audit · {entry.title}</div>
            <div className="modal-subtitle">Most recent reveals + changes (last 100)</div>
          </div>
          <button className="modal-close" onClick={onClose}>✕</button>
        </div>
        <div className="modal-body" style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {err && <div className="vault-error">{err}</div>}
          {rows === null && <div className="vault-empty" style={{ padding: 18 }}>Loading…</div>}
          {rows && rows.length === 0 && !err && (
            <div className="vault-empty" style={{ padding: 18 }}>No activity yet.</div>
          )}
          {rows && rows.length > 0 && (
            <div className="vault-audit-list">
              {rows.map(r => {
                const a = actionLabel(r.action);
                return (
                  <div key={r.id} className="vault-audit-row">
                    <span className={`vault-audit-tag is-${a.tone}`}>{a.label}</span>
                    <div className="vault-audit-info">
                      <div className="vault-audit-who">{r.name || (r.user_id || "system")}</div>
                      <div className="vault-audit-meta">
                        {_vaultRel(r.created_at)}
                        {r.detail ? <> · <span style={{ color: "var(--ink-muted)" }}>{r.detail}</span></> : null}
                        {r.ip_address ? <> · <span style={{ color: "var(--ink-muted)" }}>{r.ip_address}</span></> : null}
                      </div>
                    </div>
                  </div>
                );
              })}
            </div>
          )}
        </div>
        <div className="modal-footer">
          <button className="btn-primary" onClick={onClose}>Close</button>
        </div>
      </div>
    </div>
  );
}

// ── CSS ─────────────────────────────────────────────────────────────
const VAULT_CSS = `
.vault-page {
  flex: 1; overflow-y: auto;
  background: var(--bg-app, #f5f6f8);
  padding: 24px 28px 60px;
}
.vault-head {
  display: flex; align-items: flex-end; justify-content: space-between;
  gap: 16px; flex-wrap: wrap;
  margin-bottom: 16px;
}
.vault-head h1 { font-size: 22px; font-weight: 800; margin: 0; letter-spacing: -.012em; }
.vault-head-sub { font-size: 12px; color: var(--ink-muted); margin-top: 2px; }
.vault-head-actions { display: inline-flex; gap: 8px; }

.vault-toolbar {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) auto auto;
  gap: 10px;
  margin-bottom: 14px;
  align-items: center;
}
.vault-search { width: 100%; }
.vault-cat-select { min-width: 180px; }
.vault-filters {
  display: inline-flex; gap: 4px;
  background: var(--surface-2, #f1f4f9);
  border: 1px solid var(--border, #e6e9ef);
  border-radius: 999px; padding: 3px;
}
.vault-filter-pill {
  border: none; background: transparent;
  padding: 6px 14px; font: inherit; font-size: 12.5px; font-weight: 600;
  color: var(--ink-muted); border-radius: 999px; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
}
.vault-filter-pill:hover { color: var(--ink); }
.vault-filter-pill.is-active { background: #fff; color: var(--brand, #0073ea); box-shadow: 0 1px 3px rgba(15,23,41,.08); }
.vault-filter-count {
  background: rgba(0,0,0,.05); padding: 1px 7px; border-radius: 999px;
  font-size: 11px; color: var(--ink-muted);
}
.vault-filter-pill.is-active .vault-filter-count {
  background: rgba(0,115,234,.12); color: var(--brand);
}

.vault-empty {
  background: #fff;
  border: 1px dashed var(--border);
  border-radius: 12px;
  padding: 36px 16px;
  text-align: center; color: var(--ink-muted); font-size: 13px;
  line-height: 1.5;
}
.vault-error {
  background: rgba(226,68,92,.08);
  border: 1px solid rgba(226,68,92,.25);
  color: #8a1024;
  padding: 8px 12px; border-radius: 8px;
  font-size: 12.5px; margin-bottom: 10px;
}

.vault-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 14px;
}

.vault-card {
  background: #fff;
  border: 1px solid var(--border, #e6e9ef);
  border-left: 3px solid var(--brand, #0073ea);
  border-radius: 12px;
  padding: 14px 16px;
  display: flex; flex-direction: column; gap: 12px;
  transition: box-shadow .15s ease, transform .15s ease;
}
.vault-card:hover { box-shadow: 0 6px 18px rgba(15,23,41,.07); }
.vault-card.cat-card     { border-left-color: #a25ddc; }
.vault-card.cat-note     { border-left-color: #f1c40f; }
.vault-card.cat-api_key  { border-left-color: #11a89c; }
.vault-card.cat-identity { border-left-color: #f0832c; }
.vault-card.cat-wifi     { border-left-color: #5e6ad2; }
.vault-card.cat-database { border-left-color: #26d07c; }

.vault-card-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.vault-card-glyph {
  width: 34px; height: 34px; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 18px;
  background: var(--surface-2, #f1f4f9);
  flex-shrink: 0;
}
.vault-card-titles { flex: 1; min-width: 0; }
.vault-card-title {
  font-weight: 700; color: var(--ink); font-size: 14px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vault-card-meta {
  display: flex; gap: 6px; align-items: center; flex-wrap: wrap;
  font-size: 11.5px; color: var(--ink-muted);
}
.vault-card-meta-piece {
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 180px;
}
.vault-card-meta-piece.is-link {
  color: var(--brand, #0073ea); cursor: pointer;
}
.vault-card-meta-piece.is-link:hover { text-decoration: underline; }
.vault-fav {
  border: none; background: transparent;
  font-size: 18px; line-height: 1;
  color: var(--border, #d3d6de); cursor: pointer;
  width: 28px; height: 28px; border-radius: 6px;
  display: inline-flex; align-items: center; justify-content: center;
}
.vault-fav.is-on { color: #f5b400; }
.vault-fav:hover { background: rgba(0,0,0,.04); }

.vault-card-secret-row {
  display: flex; align-items: center; gap: 6px;
}
.vault-card-secret {
  flex: 1; min-width: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13px;
  border: 1px solid var(--border);
  background: var(--surface-2, #f6f7fb);
  border-radius: 6px;
  padding: 6px 10px;
  color: var(--ink);
  letter-spacing: .04em;
}
.vault-card-icon-btn {
  border: 1px solid var(--border, #e6e9ef);
  background: #fff; cursor: pointer;
  width: 30px; height: 30px; border-radius: 6px;
  font-size: 13px; line-height: 1;
  color: var(--ink-muted);
  display: inline-flex; align-items: center; justify-content: center;
}
.vault-card-icon-btn:hover { color: var(--brand); border-color: var(--brand); }
.vault-card-icon-btn:disabled { opacity: .5; cursor: progress; }

.vault-card-foot {
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px; flex-wrap: wrap;
  border-top: 1px solid var(--border, #e6e9ef);
  padding-top: 10px;
}
.vault-card-foot-tags { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.vault-card-foot-actions { display: inline-flex; gap: 4px; flex-wrap: wrap; }

.vault-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 10.5px; font-weight: 700;
  padding: 2px 8px; border-radius: 999px;
  text-transform: uppercase; letter-spacing: .04em;
}
.vault-chip.is-owner   { background: rgba(0,115,234,.10); color: #0050a8; }
.vault-chip.is-editor  { background: rgba(253,171,61,.16); color: #8a5300; }
.vault-chip.is-viewer  { background: rgba(107,114,128,.14); color: #374151; }
.vault-chip.is-shared  { background: rgba(0,200,117,.14); color: #007a47; }
.vault-chip.is-private { background: rgba(107,114,128,.10); color: #374151; }
.vault-chip.is-from    { background: rgba(0,0,0,.04); color: var(--ink-muted); text-transform: none; font-weight: 500; letter-spacing: 0; }

.vault-card-text-btn {
  border: none; background: transparent; cursor: pointer;
  font: inherit; font-size: 12px; font-weight: 600;
  color: var(--ink-muted);
  padding: 4px 8px; border-radius: 6px;
}
.vault-card-text-btn:hover { background: rgba(0,0,0,.04); color: var(--ink); }
.vault-card-danger { color: #c0223a; }
.vault-card-danger:hover { background: rgba(226,68,92,.08); color: #8a1024; }

/* Edit modal — secret input row with toggle */
.vault-secret-tools { display: inline-flex; gap: 4px; }
.vault-secret-input-row {
  display: flex; align-items: center; gap: 6px;
}
.vault-secret-input-row .ms-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  letter-spacing: .04em;
}
.vault-fav-row {
  display: flex; align-items: center; gap: 8px;
  font-size: 12.5px; color: var(--ink);
  background: var(--surface-2, #f6f7fb);
  padding: 8px 12px; border-radius: 8px;
}
.vault-fav-row input { accent-color: var(--brand, #0073ea); }

/* Share modal */
.vault-share-add {
  display: grid;
  grid-template-columns: 1fr 160px auto;
  gap: 8px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}
.vault-share-perm { font-size: 12.5px; }
.vault-share-list {
  display: flex; flex-direction: column; gap: 6px;
}
.vault-share-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px;
  background: var(--surface-2, #f6f7fb);
  border-radius: 8px;
}
.vault-share-row-info { flex: 1; min-width: 0; }
.vault-share-row-name { font-weight: 600; font-size: 13px; }
.vault-share-row-sub { font-size: 11px; color: var(--ink-muted); }
.vault-share-row-perm { width: 110px; font-size: 12px; }

/* Audit modal */
.vault-audit-list { display: flex; flex-direction: column; gap: 6px; }
.vault-audit-row {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  background: var(--surface-2, #f6f7fb);
  border-radius: 8px;
}
.vault-audit-tag {
  font-size: 10px; font-weight: 800;
  padding: 3px 8px; border-radius: 999px;
  text-transform: uppercase; letter-spacing: .06em;
  flex-shrink: 0;
}
.vault-audit-tag.is-view   { background: rgba(0,115,234,.10); color: #0050a8; }
.vault-audit-tag.is-ok     { background: rgba(0,200,117,.14); color: #007a47; }
.vault-audit-tag.is-info   { background: rgba(125,90,220,.10); color: #4d2c9e; }
.vault-audit-tag.is-danger { background: rgba(226,68,92,.10); color: #8a1024; }
.vault-audit-info { flex: 1; min-width: 0; }
.vault-audit-who { font-size: 12.5px; font-weight: 600; }
.vault-audit-meta { font-size: 11px; color: var(--ink-muted); }

@media (max-width: 760px) {
  .vault-page { padding: 14px; }
  .vault-toolbar { grid-template-columns: 1fr; }
  .vault-share-add { grid-template-columns: 1fr; }
  .vault-grid { grid-template-columns: 1fr; }
}
`;

Object.assign(window, { VaultView, VaultEntryModal, VaultShareModal, VaultAuditModal });
