/* Access — admin-only allow-list management. Add/remove the people who may sign in
   to Ledger, assign which APP each one gets, and mark who is an admin. Backed by
   the ledger_access table; every write is admin-gated by RLS (migration 0036), so
   this UI is convenience over a control already enforced in the database.

   Since 0098 the apps are independent grants: Workspace (matters), Content
   Operations (built on Workspace), Performance (platform revenue), the Rights
   Tracker and Engineering. Being on this list only means a person may sign in;
   the per-app columns decide what they see, and is_workspace() / is_analytics() /
   is_rights() enforce it at the row level.
   Assigns window.LedgerAccess */
(function () {
  // The apps an admin can hand out, and the ledger_access column behind each. This
  // is the only list in the file — the add form and the table columns both render
  // from it, so a third app is one row here rather than three parallel edits.
  const APPS = [
    { key: "is_workspace", label: "Workspace", icon: "folder-open",
      blurb: "Matters, Trial Calendar, Coverage, Agencies, Search" },
    // Column is still `is_content_center` (0054). Content Operations works matters,
    // so it `requires` the Workspace: granting it grants that too (add form and
    // toggle both), and a row holding it without Workspace reads "Needs Workspace".
    { key: "is_content_center", label: "Content Operations", icon: "boxes",
      blurb: "Asset Requests desk (Content Center, Media Management, Feed Operations). Includes Workspace.",
      requires: "is_workspace" },
    // Performance has LEVELS rather than an on/off switch (0124): some people may
    // see its dashboards but not revenue. Each level is the full patch it writes,
    // so a change lands both flags at once. The first level is the default.
    // Not an app: a role inside Content Operations (0137). Only Content Center staff
    // who process requests are Assignable; requesters never are. Requires the grant.
    { key: "can_be_assigned", label: "Assignable", icon: "user-check",
      blurb: "Can be assigned asset requests to process. Content Center staff only, never requesters. Includes Content Operations.",
      requires: "is_content_center" },
    { key: "is_analytics", label: "Performance", icon: "trending-up",
      blurb: "Dashboards: Viewership (HOV) and any non-revenue board. Revenue dashboards: also CTV Revenue.",
      levels: [
        { value: "none",       label: "No access",          patch: { is_analytics: false, is_analytics_revenue: false } },
        { value: "dashboards", label: "Dashboards",         patch: { is_analytics: true,  is_analytics_revenue: false } },
        { value: "revenue",    label: "Revenue dashboards", patch: { is_analytics: true,  is_analytics_revenue: true } },
      ] },
    // Editorial (0131): shows, episodes and their links to matters. Rows are
    // Workspace RLS, so like Content Operations it `requires` the Workspace.
    // Producers who review links for a show are set in staff_shows, not here (E2c).
    { key: "is_editorial", label: "Editorial", icon: "clapperboard",
      blurb: "Shows, episodes, and the matters and material they use. Includes Workspace.",
      requires: "is_workspace" },
    // Sponsors (0143): not every Editorial user may see who sponsored an episode
    // (operator 2026-10-07). A grant on top of Editorial, like Assignable on Content
    // Operations: granting it grants Editorial; removing Editorial removes it.
    { key: "is_editorial_sponsors", label: "Sponsors", icon: "badge-dollar-sign",
      blurb: "Sees which sponsors ran on each episode, and the Sponsors totals. Includes Editorial.",
      requires: "is_editorial" },
    { key: "is_rights", label: "Rights Tracker", icon: "scale",
      blurb: "Catalogue, partner deals, clearance" },
    { key: "is_engineering", label: "Engineering", icon: "git-pull-request",
      blurb: "Repos, pull requests, approve and merge" },
  ];

  // What granting `key` also grants, nearest first (Assignable → Content Operations
  // → Workspace), and what removing it must also remove.
  const basesOf = (key) => { const out = []; let a = APPS.find((x) => x.key === key);
    while (a && a.requires) { out.push(a.requires); a = APPS.find((x) => x.key === a.requires); } return out; };
  const dependentsOf = (key) => APPS.filter((d) => d.requires === key)
    .flatMap((d) => [d.key, ...dependentsOf(d.key)]);

  // Which level a row holds: the last level whose whole patch matches it. A row
  // that matches none (revenue flag without the base flag) grants nothing, and
  // reads as the first level, which is what the database enforces.
  function levelOf(row, app) {
    const hit = app.levels.filter((l) => Object.entries(l.patch).every(([k, v]) => !!row[k] === v));
    return hit.length ? hit[hit.length - 1] : app.levels[0];
  }
  const held = (row, a) => (a.levels ? levelOf(row, a) !== a.levels[0] : !!row[a.key]);
  function grantedLabel(row, a) {
    return a.levels ? `${a.label}: ${levelOf(row, a).label}` : a.label;
  }

  function AddRow({ onAdd, busy }) {
    const { Button } = window.LawCrimeDesignSystem_e18d68;
    const [email, setEmail] = React.useState("");
    const [asAdmin, setAsAdmin] = React.useState(false);
    // Workspace pre-ticked because it is the common case, but the DB column
    // defaults to FALSE — a row created outside this form grants nothing. The
    // default lives in the UI, where it's visible and can be unticked, rather than
    // in the schema where it would silently grant.
    const NEW_ROW = { is_workspace: true, is_content_center: false, can_be_assigned: false, is_analytics: false, is_analytics_revenue: false, is_rights: false, is_engineering: false, is_editorial: false, is_editorial_sponsors: false };
    const [apps, setApps] = React.useState(NEW_ROW);
    const domain = window.LEDGER_ALLOWED_DOMAIN || "lawnewz.com";
    const none = !asAdmin && !APPS.some((a) => held(apps, a));

    function submit(e) {
      e.preventDefault();
      const v = email.trim().toLowerCase();
      if (!v || none) return;
      onAdd(v, asAdmin, apps).then((ok) => {
        if (ok) { setEmail(""); setAsAdmin(false); setApps(NEW_ROW); }
      });
    }

    return (
      <form className="access-add" onSubmit={submit}>
        <input
          className="lc-input"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder={`name@${domain}`}
          autoComplete="off"
        />
        {APPS.map((a) => a.levels ? (
          <label key={a.key} className="access-add__admin" title={a.blurb}>
            <span>{a.label}</span>
            <select className="access-level" value={levelOf(apps, a).value}
              onChange={(e) => {
                const l = a.levels.find((x) => x.value === e.target.value);
                setApps((p) => ({ ...p, ...l.patch }));
              }}>
              {a.levels.map((l) => <option key={l.value} value={l.value}>{l.label}</option>)}
            </select>
          </label>
        ) : (
          <label key={a.key} className="access-add__admin" title={a.blurb}>
            <input type="checkbox" checked={!!apps[a.key]}
              onChange={(e) => {
                const on = e.target.checked;
                setApps((p) => {
                  const next = { ...p, [a.key]: on };
                  // Ticking an app that builds on another ticks that one too; unticking
                  // the base unticks what depends on it. The pair can't be split here.
                  if (on) basesOf(a.key).forEach((k) => { next[k] = true; });
                  if (!on) dependentsOf(a.key).forEach((k) => { next[k] = false; });
                  return next;
                });
              }} />
            <span>{a.label}</span>
          </label>
        ))}
        <label className="access-add__admin">
          <input type="checkbox" checked={asAdmin} onChange={(e) => setAsAdmin(e.target.checked)} />
          <span>Admin</span>
        </label>
        {/* Adding someone with no app at all is almost always a slip — they'd sign
            in and hit the "access pending" screen. Admins imply every app, so the
            Admin box satisfies this on its own. */}
        <Button type="submit" disabled={busy || !email.trim() || none}
          title={none ? "Pick at least one app (or Admin)" : undefined}>Add</Button>
      </form>
    );
  }

  function Access() {
    const { Card, Button, Badge } = window.LawCrimeDesignSystem_e18d68;
    const [rows, setRows] = React.useState(null);
    const [err, setErr] = React.useState(null);
    const [msg, setMsg] = React.useState(null);
    const [busy, setBusy] = React.useState(false);
    const [meEmail, setMeEmail] = React.useState("");

    const load = React.useCallback(async () => {
      setErr(null);
      try {
        const [list, { data }] = await Promise.all([
          window.LedgerAPI.listAccess(),
          window.SB.auth.getUser(),
        ]);
        setRows(list);
        setMeEmail(((data && data.user && data.user.email) || "").toLowerCase());
      } catch (e) { setErr(e.message || String(e)); }
    }, []);

    React.useEffect(() => { load(); }, [load]);
    React.useEffect(() => { setTimeout(() => window.lucide && window.lucide.createIcons(), 20); });

    // `apps` is the per-app patch; grantAccess writes only the keys it's handed, so
    // this is safe for both add and re-add.
    async function add(email, isAdmin, apps) {
      setBusy(true); setMsg(null); setErr(null);
      try {
        await window.LedgerAPI.grantAccess(email, isAdmin, apps);
        const granted = isAdmin
          ? "admin"
          : APPS.filter((a) => apps && held(apps, a)).map((a) => grantedLabel(apps, a)).join(" + ");
        setMsg({ ok: true, text: `Added ${email}${granted ? ` (${granted})` : ""}.` });
        await load();
        return true;
      } catch (e) { setErr(e.message || String(e)); return false; }
      finally { setBusy(false); }
    }

    async function toggleAdmin(row) {
      setBusy(true); setMsg(null); setErr(null);
      try {
        await window.LedgerAPI.grantAccess(row.email, !row.is_admin);
        await load();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusy(false); }
    }

    // App toggle, kept separate from the admin toggle on purpose: the whole point of
    // the split is to grant one app WITHOUT granting admin — or the other app.
    async function toggleApp(row, key) {
      setBusy(true); setMsg(null); setErr(null);
      try {
        // Granting an app that builds on another grants the base first, so the new
        // app never opens empty. Removing a base leaves the dependent flag as it is
        // (the cell then reads "Needs Workspace") rather than silently revoking it.
        const app = APPS.find((a) => a.key === key);
        if (!row[key] && app) {
          for (const base of basesOf(key).reverse()) {
            if (!row[base]) await window.LedgerAPI.setAccessFlag(row.email, base, true);
          }
        }
        await window.LedgerAPI.setAccessFlag(row.email, key, !row[key]);
        await load();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusy(false); }
    }

    // Multi-level app: write the chosen level's whole patch in one update.
    async function setLevel(row, app, value) {
      const l = app.levels.find((x) => x.value === value);
      if (!l) return;
      setBusy(true); setMsg(null); setErr(null);
      try {
        await window.LedgerAPI.setAccessFlags(row.email, l.patch);
        setMsg({ ok: true, text: `${row.email}: ${app.label} set to ${l.label}.` });
        await load();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusy(false); }
    }

    async function revoke(row) {
      if (!window.confirm(`Remove access for ${row.email}? They will be signed out of Ledger on their next action.`)) return;
      setBusy(true); setMsg(null); setErr(null);
      try {
        await window.LedgerAPI.revokeAccess(row.email);
        setMsg({ ok: true, text: `Removed ${row.email}.` });
        await load();
      } catch (e) { setErr(e.message || String(e)); }
      finally { setBusy(false); }
    }

    return (
      <div>
        <div className="page-head">
          <div>
            <div className="eyebrow">Administration · Access</div>
            <h1 className="page-title">Access</h1>
          </div>
        </div>
        <p style={{ color: "var(--text-muted)", fontSize: 13, maxWidth: 760, marginTop: -4 }}>
          Only people on this list can sign in to Ledger, and sign-in is still limited to
          {" "}<code>@{window.LEDGER_ALLOWED_DOMAIN || "lawnewz.com"}</code> accounts. Being on the
          list is not access on its own — <b>assign at least one app</b>, and they are independent:
          {" "}<b>Workspace</b> is the case system (Matters, Trial Calendar, Coverage, Agencies,
          Search); <b>Content Operations</b> is the Content Center, Media Management and Feed
          Operations desk, and includes Workspace; <b>Editorial</b> is the show and episode
          {" "}browser for the Social team, and includes Workspace; <b>Performance</b> has three levels: <i>No access</i>,
          {" "}<i>Dashboards</i> (Viewership and any board that isn't revenue) and <i>Revenue dashboards</i>
          {" "}(adds CTV Revenue). Someone who only needs revenue numbers should have Performance and
          {" "}<i>not</i> Workspace, and vice versa. <b>Admin</b>
          {" "}carries every app plus this list and the AI configuration.
        </p>

        {err && <div className="login__err" style={{ margin: "12px 0" }}>{err}</div>}
        {msg && <div className="ai-saved" style={{ margin: "12px 0", color: msg.ok ? "var(--lc-green-700, #2e7d32)" : "var(--danger)" }}>{msg.text}</div>}

        <Card>
          <Card.Body>
            <AddRow onAdd={add} busy={busy} />
          </Card.Body>
        </Card>

        {window.LedgerGroupSync && <window.LedgerGroupSync />}

        {rows === null && !err && <div className="ai-empty">Loading…</div>}
        {rows && rows.length === 0 && <div className="ai-empty">No one authorized yet.</div>}
        {rows && rows.length > 0 && (
          <table className="access-table">
            <thead>
              <tr>
                <th>Email</th><th>Role</th>
                {APPS.map((a) => <th key={a.key}>{a.label}</th>)}
                <th>Added</th><th></th>
              </tr>
            </thead>
            <tbody>
              {rows.map((r) => {
                const isMe = r.email === meEmail;
                return (
                  <tr key={r.email}>
                    <td>{r.email}{isMe && <span className="access-you"> · you</span>}</td>
                    <td>{r.is_admin ? <Badge>Admin</Badge> : <span className="access-member">Member</span>}</td>
                    {/* One cell per app. Disabled for admins because admin already
                        carries every app server-side — a toggle that can't change
                        what they see would be lying about the state. */}
                    {APPS.map((a) => {
                      if (a.levels && !r.is_admin) {
                        return (
                          <td key={a.key}>
                            <select className="access-level" disabled={busy}
                              value={levelOf(r, a).value}
                              onChange={(e) => setLevel(r, a, e.target.value)}
                              title={a.blurb}>
                              {a.levels.map((l) => <option key={l.value} value={l.value}>{l.label}</option>)}
                            </select>
                          </td>
                        );
                      }
                      // Held, but its base app isn't: the sidebar won't show it.
                      const stranded = !r.is_admin && r[a.key] && a.requires && !r[a.requires];
                      return (
                        <td key={a.key}>
                          <button className="access-btn" disabled={busy || r.is_admin}
                            onClick={() => toggleApp(r, a.key)}
                            title={r.is_admin
                              ? `Admins always have ${a.label}`
                              : (r[a.key] ? `Remove ${a.label}` : `Grant ${a.label}`)}>
                            {/* Each icon in its own keyed span: lucide swaps the <i> for an
                                <svg>, and React would then fail to unmount it on toggle. */}
                            <span key={r.is_admin || r[a.key] ? "on" : "off"} className="access-ico">
                              <i data-lucide={r.is_admin || r[a.key] ? a.icon : "minus"} />
                            </span>
                            {r.is_admin ? "Via admin" : stranded ? `Needs ${(APPS.find((x) => x.key === a.requires) || {}).label}` : (r[a.key] ? "Granted" : "No access")}
                          </button>
                        </td>
                      );
                    })}
                    <td className="access-when">{r.added_at ? new Date(r.added_at).toLocaleDateString() : "—"}</td>
                    <td className="access-actions">
                      <button className="access-btn" disabled={busy || isMe} onClick={() => toggleAdmin(r)}
                        title={isMe ? "You can't change your own admin status" : (r.is_admin ? "Demote to member" : "Make admin")}>
                        <i data-lucide={r.is_admin ? "user" : "shield"} /> {r.is_admin ? "Make member" : "Make admin"}
                      </button>
                      <button className="access-btn access-btn--danger" disabled={busy || isMe} onClick={() => revoke(r)}
                        title={isMe ? "You can't remove yourself" : "Remove access"}>
                        <i data-lucide="user-minus" /> Remove
                      </button>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>
    );
  }

  window.LedgerAccess = Access;
})();
