// The reading room, from the other side — who can read a contract, and who used
// to.
//
// WHY THIS IS A FILE OF ITS OWN, and it is not a filing preference. `viewer.jsx`
// carries three guards that describe THE WHOLE FILE: it calls exactly three
// reads, it hands out no handler that touches the doorway, and it renders no
// disabled control. Those are ADR-0008's posture written down, and they are
// worth more than the convenience of one file. Putting the sharing surface in
// there would have meant loosening all three — a guard widened to accommodate a
// change is a guard that stops meaning anything. So the viewer's pane stays
// exactly what its guards say, and the other side of the room lives here with
// guards of its own.
//
// WHAT WAS BUILT AND HAD NEVER BEEN REACHED. `0017` created cw.agreement_share,
// granted INSERT and UPDATE on it to both Legal roles, wrote the policies, and
// shipped `POST /shares` and `POST /shares/revoke` through the doorway. **No
// screen ever called either.** So nothing could ever enter a viewer's reading
// room, and the role ADR-0008 created — the audience that makes override
// socialisation mean something — was unusable in principle rather than merely
// empty.
//
// It also granted SELECT on that table to ALL SIX ROLES and served it to
// nobody, so "who can see this agreement, and who used to" had no answer for
// anybody, the Auditor included. `GET /shares` is that answer and is new; it
// reads the TABLE rather than cw.reading_room, because the view carries
// `where s.revoked_at is null` and a withdrawn share is exactly what an auditor
// asks about.
//
// WHO SEES WHAT, and the differences are AFFORDANCES rather than permissions —
// the database refuses the acts regardless:
//
//   legal_reviewer/admin  the whole record, the form that puts a contract in
//                         the room, and the control that takes one out.
//                         `legal_shares` and `legal_unshares` name exactly
//                         these two roles.
//   requester             the shares on their OWN agreements — "who has been
//                         shown my contract" — and no control. They cannot
//                         share: the point of a socialisation audience is that
//                         somebody ELSE decided who should see it.
//   auditor/administrator every share, live and withdrawn, and no control.
//   viewer                never reaches this file at all; they get
//                         `viewer.jsx`, which is the room from the inside.
//
// The scoping is the table's own read_scoped policy. Nothing here passes a
// parameter, which is what keeps that a rule rather than a careful query.

const { useState, useRef } = React;

// WHO HOLDS THE ACT. `legal_shares` and `legal_unshares` (0017) name these two
// roles and nobody else, so these are the only two rails that draw a control.
// Asked of the role rather than of the response, because an affordance offered
// and then refused teaches people to stop pressing — and asked in ONE place, so
// the form and the withdraw control cannot come to disagree about who may act.
const MAY_SHARE = ['legal_reviewer', 'legal_admin'];

// ── The room itself: every share, and the two acts on it ──────────────────
// Drawn for the roles that can read the whole record. What differs between them
// is whether the acts are offered at all — see MAY_SHARE.
function TheRoom({ me, live }) {
  const acts = useActs();
  // The RECORD, not the room: this one carries withdrawn shares, which
  // cw.reading_room cannot show because it holds only what is currently in it.
  const record = usePane(() => API.shares());
  const [error, setError] = useState(null);
  const [form, setForm] = useRetainedState('share', {
    agreement_id: '', shared_with: '', purpose: '' });
  const registerRef = useRef(null);
  const withdrawnRef = useRef(null);
  const filter = useListFilter(record.rows, {
    view: 'reading-room:shares',
    fields: ['agreement_id', 'shared_with', 'shared_by', 'purpose'],
    facet: 'shared_with',
  });

  const mayAct = MAY_SHARE.includes(me.role);
  const rows = record.rows ?? [];
  // THE BOUND, AND WHY IT IS SAID OUT LOUD. `GET /shares` carries `limit 500`
  // because cw.agreement_share is append-only and grows with time — the same
  // bound GET /record and GET /runs carry. Below it nothing is hidden and this
  // says nothing; at it, every figure on this page is counted over a truncated
  // set and would quietly stop being true. Server-side paging is the real
  // answer (PRODUCT.md §4 item 6, approved and not started); until it exists,
  // saying so is the honest half.
  const AT_THE_BOUND = 500;
  const truncated = rows.length >= AT_THE_BOUND;
  const withdrawn = rows.filter((r) => r.revoked_at);
  const standing = rows.filter((r) => !r.revoked_at);
  // COUNTED OVER THE WHOLE RECORD, never over what the filter left. A figure
  // computed from the narrowed set becomes its own total the moment it is
  // pressed (the audit record's machine figure, S363).
  const people = new Set(standing.map((r) => r.shared_with)).size;
  const measured = record.status === 'loaded';

  const reload = () => { record.reload(); live.reload(); };

  const share = async () => {
    setError(null);
    const r = await API.shareAgreement({
      agreement_id: form.agreement_id.trim(),
      shared_with: form.shared_with.trim(),
      purpose: form.purpose.trim(),
    });
    // THE SERVICE'S OWN SENTENCE, unchanged. It says whether the agreement is
    // unknown, not executed, or already shared with that person — all three are
    // actionable, and "could not share" is not.
    if (!r.ok) { setError(r.reason); return; }
    setForm({ agreement_id: '', shared_with: '', purpose: '' });
    discardDraft('share');
    reload();
  };

  const withdraw = (row) => async () => {
    setError(null);
    const r = await API.revokeShare({ share_id: row.share_id });
    if (!r.ok) { setError(r.reason); return; }
    reload();
  };

  const focusOnly = (key, label, test) => {
    // Registered as the tile renders, so a saved view can put the focus back.
    const f = filter.focusable(key, label, test);
    return {
      to: measured ? () => filter.focusOn(f) : null,
      on: filter.focus?.key === key,
    };
  };

  return (
    <div>
      <PaneHead
        title="Reading room"
        kicker={mayAct ? 'Legal' : undefined}
        sub={mayAct
          ? 'Every contract somebody has been shown, who showed it, and why — and the two acts that put one in and take one out.'
          : 'Every contract somebody has been shown, who showed it, and why. Nothing here can be acted on.'}
        right={<ActButton className="btn btn-sm" data-testid="room-reload"
                          onClick={async () => reload()}>read it again</ActButton>} />

      {error && (
        <div className="panel-2 p-3 mt-4" data-testid="room-error">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>refused</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>{error}</div>
        </div>
      )}

      {truncated && (
        <div className="panel-2 p-3 mt-4" data-testid="shares-bounded">
          <div className="tag" style={{ color: 'var(--accent-2)' }}>the oldest are not here</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>
            This record holds the most recent {AT_THE_BOUND} shares, newest
            first — <strong>the figures below are counted over those</strong>,
            not over every share there has ever been.
          </div>
        </div>
      )}

      {/* THREE FIGURES, AND EACH NARROWS THE REGISTER BELOW IT. An unmeasured
          read draws an em-dash and stays inert; a measured NOUGHT still drills,
          because "nobody has been shown anything" is an answer and the list
          that says so plainly is a real destination. */}
      <div className="mt-5 grid gap-3"
           style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))' }}>
        <StatBox label="people with access" n={measured ? people : null}
                 describe={`show the ${standing.length} live shares`}
                 {...focusOnly('live', 'live shares', (r) => !r.revoked_at)} />
        <StatBox label="contracts in the room" n={measured ? standing.length : null}
                 describe={`show the ${standing.length} live shares`}
                 {...focusOnly('live', 'live shares', (r) => !r.revoked_at)} />
        <StatBox label="withdrawn" n={measured ? withdrawn.length : null}
                 describe={`show the ${withdrawn.length} withdrawn shares`}
                 {...focusOnly('withdrawn', 'withdrawn shares', (r) => Boolean(r.revoked_at))} />
      </div>

      {mayAct && (
        <div className="panel p-4 mt-6" ref={withdrawnRef} data-testid="share-form">
          <PanelHead
            title="Show a contract to somebody"
            sub="Sharing is Legal's recorded act, and the reason is not optional — a share nobody stated a purpose for is one nobody can review later." />
          <div className="flex gap-2 flex-wrap mt-1">
            <input className="font-mono" style={{ padding: '6px 9px', minWidth: 150 }}
                   placeholder="AG-26-001" aria-label="Which agreement"
                   data-testid="share-agreement"
                   value={form.agreement_id}
                   onChange={(e) => setForm({ ...form, agreement_id: e.target.value })} />
            <input className="font-mono" style={{ padding: '6px 9px', minWidth: 190 }}
                   placeholder="name@clausewerk" aria-label="Who to show it to"
                   data-testid="share-person"
                   value={form.shared_with}
                   onChange={(e) => setForm({ ...form, shared_with: e.target.value })} />
            <input className="grow" style={{ padding: '6px 9px', minWidth: 220 }}
                   placeholder="why they are being shown it"
                   aria-label="Why they are being shown it"
                   data-testid="share-purpose"
                   value={form.purpose}
                   onChange={(e) => setForm({ ...form, purpose: e.target.value })} />
            <ActButton className="btn btn-primary" data-testid="share-do"
                       disabled={!form.agreement_id.trim() || !form.shared_with.trim()
                                 || !form.purpose.trim()}
                       onClick={share}>
              show it to them
            </ActButton>
          </div>
          <p className="caption mt-2">
            Only a signed agreement can be shown, and only once per person — the
            record refuses the rest and says which. Taking a share away is
            recorded rather than deleted, so the register below keeps it.
          </p>
        </div>
      )}

      <div className="mt-6" ref={registerRef}>
        <PanelHead title="The register"
                   sub="Every share this record holds, live and withdrawn."
                   right={<FilterCount filter={filter} />} />
        {record.status === 'loading' ? <Loading />
          : record.status === 'failed' ? <LoadFailed reason={record.reason} />
          : rows.length === 0 ? (
            <Empty
              kicker="reading room"
              line="Nothing has ever been shared."
              sub="An empty record, not a failed read. When Legal shows a signed
                   agreement to somebody it appears here, with the reason." />
          ) : (
            <>
              <ListFilter filter={filter} testid="shares"
                          placeholder="agreement, person or purpose"
                          facetLabel="everyone" />
              {filter.shown.length === 0
                ? <NoMatch kicker="reading room" noun="share" />
                : (
                  <div className="panel">
                    <table className="ledger">
                      <thead>
                        <tr>
                          <th>Agreement</th><th>Shown to</th><th>Why</th>
                          <th>Shown by</th><th>Standing</th>
                          {mayAct && <th style={{ textAlign: 'right' }}>Take it back</th>}
                        </tr>
                      </thead>
                      <tbody>
                        {filter.shown.map((r) => (
                          <tr key={r.share_id}>
                            <td className="mono">{r.agreement_id}</td>
                            <td className="mono">{r.shared_with}</td>
                            <td>{r.purpose}</td>
                            <td>
                              <span className="mono">{r.shared_by}</span>
                              <span className="caption"> · {String(r.shared_at ?? '').slice(0, 10)}</span>
                            </td>
                            {/* WITHDRAWN IS KEPT, NOT DELETED, so the mark says
                                which and by whom. Struck rather than absent is
                                the desk's rule for anything superseded. */}
                            <td>
                              {r.revoked_at
                                ? <><span className="chip chip-gone">withdrawn</span>
                                    <span className="caption"> by {r.revoked_by} · {String(r.revoked_at).slice(0, 10)}</span></>
                                : <span className="chip chip-ok">in the room</span>}
                            </td>
                            {mayAct && (
                              <td style={{ textAlign: 'right' }}>
                                {r.revoked_at
                                  ? <span className="caption">—</span>
                                  : <ActButton className="btn btn-sm"
                                               data-testid={`withdraw-${r.share_id}`}
                                               onClick={() => acts.run(`w-${r.share_id}`, withdraw(r))}>
                                      take it back
                                    </ActButton>}
                              </td>
                            )}
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                )}
            </>
          )}
      </div>

      <p className="caption mt-3">
        Sharing is a recorded act and so is taking it back — the register keeps
        both, and a withdrawn share is struck rather than removed.
        <strong> There is still no export</strong>, for anybody: showing somebody
        a contract and letting them take a copy away are different acts, and only
        the first was ever decided.
      </p>
    </div>
  );
}
