// One obligation, open — and the acts that close, evidence or move it.
//
// WHAT WAS MISSING. The post-signature half of this product was a book people
// could read and could not touch. Four acts have existed behind the API since
// `0037`/`0039`/`0050`, every one of them tested, and the obligations pane
// rendered nothing but lists. PRODUCT.md §4 ranked it second of nine gaps:
// *"The post-signature half of the product is a register people can read and
// cannot satisfy, acknowledge, reassign or waive from."*
//
// ── THE FOUR ACTS ARE NOT INTERCHANGEABLE, and the screen must not let
//    them look it ──────────────────────────────────────────────────────────
//
//   RECORD IT AS DONE (satisfied) closes the duty. The note is mandatory —
//   `satisfaction_needs_note` refuses a blank one — because the attestation
//   says what was actually done, and bytes with no sentence beside them are
//   not evidence anybody can act on a year later.
//
//   RECORD A COUNTERPARTY ACKNOWLEDGEMENT (counterparty_ack) DOES NOT CLOSE
//   ANYTHING. The state view reads acts in ('satisfied','waived') and has
//   never read this one. A person pressing a button called "acknowledge" will
//   assume the duty is discharged, so the screen says otherwise in the same
//   breath — twice, before and after. It needs a document, because an
//   acknowledgement with no document is the bare flag OB-06 exists to refuse.
//
//   HAND IT TO SOMEBODY ELSE (reassigned) names a PERSON, never a team inbox
//   (`0037`'s constraint). The current owner is the last reassignment.
//
//   WAIVE IT (waived) closes a duty that was NOT done, and is the only one
//   that needs an authority from outside this screen: an override request,
//   socialised, its window run, decided by a Legal reviewer who did not open
//   it, whose finding names `obligation:<id>` and was APPROVED. A proposal
//   authorises nothing (`0039`). So this screen looks for that approval and
//   says what it found — it never offers a button that could only be refused.
//
// ── AND ASSERTING BREACH IS ABSENT, DELIBERATELY ───────────────────────────
//
// D-1, settled by the owner: the system computes and reports OVERDUE, which is
// arithmetic. Breach is a consequential legal claim a named person makes about
// a real counterparty. There is no endpoint, so there is nothing here to wire,
// and this comment is why a future session should not add one to be tidy.

const { useState: useActState, useMemo: useActMemo } = React;

// ── Who may act, stated once beside the policy it comes from ──────────────
//
//     0050  record_act on cw.obligation_act
//           satisfied / reassigned / counterparty_ack →
//               legal_reviewer, legal_admin,
//               or requester on a deal cw.owns_agreement() answers for
//           waived → legal_reviewer, legal_admin
//           breach_asserted → legal_admin (and no endpoint exists)
//
// THE ROLE HALF IS GATED HERE; THE ROW HALF IS NOT, and the difference is
// deliberate. An auditor or an administrator can never record any of these —
// a whole-role fact, so offering them the controls would be an affordance
// whose only possible outcome is a refusal. Whether a REQUESTER owns this
// particular deal is a per-row fact this screen cannot compute without
// building a second copy of `cw.owns_agreement`, so it does not try: the act
// is offered, and a requester reaching for a colleague's obligation is
// refused by the database in the database's own words.
const OBLIGATION_ACTORS = ['requester', 'legal_reviewer', 'legal_admin'];
const WAIVER_ACTORS = ['legal_reviewer', 'legal_admin'];

function mayActOnObligations(me) {
  return OBLIGATION_ACTORS.includes(me && me.role);
}
function mayWaive(me) {
  return WAIVER_ACTORS.includes(me && me.role);
}

// The finding an override must name before a waiver is authorised. `0039`
// builds this string in SQL; it is built here identically so the screen can
// look for the approval rather than discover its absence as a refusal.
function waiverFindingRef(obligationId) {
  return 'obligation:' + obligationId;
}

// ── A refusal, in the database's own sentence ─────────────────────────────
function ActRefusal({ reason }) {
  if (!reason) return null;
  return (
    <div className="panel p-3 mt-3" style={{ borderColor: 'var(--danger)' }}
         data-testid="act-refusal" role="alert">
      <div className="tag" style={{ color: 'var(--danger)' }}>refused</div>
      <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
        {reason}
      </div>
    </div>
  );
}

// ── The evidence chooser ──────────────────────────────────────────────────
//
// SCOPED TO THIS DEAL IN THE LIST AS WELL AS IN THE DATABASE. `0050`'s guard
// refuses a document received for another agreement — "evidence answers for
// the deal it arrived on" — and a chooser offering the others would spend that
// refusal on somebody's honest mistake. The guard still decides; this only
// stops the screen inviting the failure.
function EvidenceChooser({ documents, documentsFailed, agreementId, value,
                          onChange, required, label }) {
  const mine = useActMemo(
    () => (documents || []).filter(
      (d) => String(d.agreement_id) === String(agreementId)),
    [documents, agreementId]);

  // "I COULD NOT READ THE LIST" IS NOT "THERE ARE NONE", and the first draft of
  // this component said the second for both. Caught by driving it against a
  // service that had not restarted: the read answered `no such endpoint`, the
  // pane passed an empty array, and the screen told the reader in confident
  // prose that no document had ever been received for their deal. That is the
  // never-describe-a-refusal-as-a-population defect, one component over.
  if (documentsFailed) {
    return (
      <div className="caption mt-1.5" data-testid="evidence-unreadable">
        The list of received documents could not be read, so this screen cannot
        say what there is to cite. <span className="font-mono">{documentsFailed}</span>
        {required && <> An acknowledgement must name a document, so it cannot be
          recorded until this reads.</>}
      </div>
    );
  }

  if (mine.length === 0) {
    return (
      <div className="caption mt-1.5" data-testid="no-evidence">
        {required
          ? <>No document has been received for <span className="font-mono">{agreementId}</span>,
              and an acknowledgement must cite one — a bare flag is exactly what
              the record refuses. A counterparty's paper reaches the system
              through the negotiation, and this act becomes available the moment
              one has.</>
          : <>No document has been received for <span className="font-mono">{agreementId}</span>,
              so there is nothing to attach. Your attestation below closes the
              duty on its own; evidence is optional.</>}
      </div>
    );
  }

  return (
    <div className="mt-1.5">
      <select className="w-full font-mono" style={{ padding: '6px 8px' }}
              aria-label={label}
              data-testid="evidence-choose"
              value={value}
              onChange={(e) => onChange(e.target.value)}>
        <option value="">{required ? 'choose the document…' : 'no document'}</option>
        {mine.map((d) => (
          <option key={d.document_id} value={String(d.document_id)}>
            {(d.filename || 'unnamed')} · {d.byte_count} bytes · received{' '}
            {String(d.received_at).slice(0, 10)} by {d.received_by}
          </option>
        ))}
      </select>
      {/* THE FINGERPRINT, NOT ONLY THE NAME. A filename is the caller's own
          claim about their file (0047 records it as claimed and rewrites
          nothing); the hash is the system's arithmetic. Anybody checking a
          year later needs the second. */}
      {value !== '' && (() => {
        const d = mine.find((x) => String(x.document_id) === String(value));
        return d ? (
          <div className="caption mt-1 font-mono">
            {String(d.sha256).slice(0, 16)} · {d.content_type || 'unknown type'}
          </div>
        ) : null;
      })()}
    </div>
  );
}

// ── The four acts ─────────────────────────────────────────────────────────

const OBLIGATION_ACT_CHOICES = [
  { key: 'satisfy', label: 'Record it as done',
    line: 'Closes the duty. Your attestation says what was actually done.' },
  { key: 'ack', label: 'Record a counterparty acknowledgement',
    line: 'Evidence that they acknowledged it. Does NOT close the duty.' },
  { key: 'reassign', label: 'Hand it to somebody else',
    line: 'Names a person, never a team. The duty is unchanged.' },
  { key: 'waive', label: 'Waive it',
    line: 'Closes a duty that was not done. Needs an approved override.' },
];

function ObligationActs({ me, obligation, documents, documentsFailed, people,
                          approvals, onDone }) {
  const id = obligation.obligation_id;
  const [chosen, setChosen] = useActState(null);
  // RETAINED PER OBLIGATION. A note is the whole attestation and a pane
  // unmounts the moment somebody clicks the rack; keying on the obligation
  // stops one duty's note appearing under another's (S319, and the key is the
  // part that idiom is easy to get wrong).
  const [note, setNote] = useRetainedState(`obligation:${id}:note`, '');
  const [document, setDocument] = useRetainedState(`obligation:${id}:doc`, '');
  const [owner, setOwner] = useRetainedState(`obligation:${id}:owner`, '');
  const [override, setOverride] = useActState('');
  const [refused, setRefused] = useActState(null);

  const canWaive = mayWaive(me);
  // AN APPROVED OVERRIDE NAMING EXACTLY THIS OBLIGATION, or none. Read off the
  // finding rows the caller was actually answered — a request they cannot see
  // does not exist for them, and the database will say so if they somehow name
  // one anyway.
  const authorised = useActMemo(() => {
    const ref = waiverFindingRef(id);
    return (approvals || []).filter(
      (f) => String(f.finding_ref) === ref && f.decision === 'approved');
  }, [approvals, id]);

  const offered = OBLIGATION_ACT_CHOICES.filter((a) => a.key !== 'waive' || canWaive);

  const ready = (() => {
    if (chosen === 'satisfy') return note.trim().length > 0;
    if (chosen === 'ack') return document !== '';
    if (chosen === 'reassign') return owner.trim().length > 0;
    if (chosen === 'waive') return note.trim().length > 0 && override !== '';
    return false;
  })();

  async function perform() {
    setRefused(null);
    let r;
    if (chosen === 'satisfy') {
      r = await API.satisfyObligation({
        obligation_id: id, note: note.trim(),
        ...(document ? { document_ref: Number(document) } : {}),
      });
    } else if (chosen === 'ack') {
      r = await API.ackObligation({
        obligation_id: id, document_ref: Number(document),
        ...(note.trim() ? { note: note.trim() } : {}),
      });
    } else if (chosen === 'reassign') {
      r = await API.reassignObligation({
        obligation_id: id, new_owner: owner.trim(),
        ...(note.trim() ? { note: note.trim() } : {}),
      });
    } else {
      r = await API.waiveObligation({
        obligation_id: id, note: note.trim(), override_ref: Number(override),
      });
    }
    if (!r.ok) { setRefused(r.reason); return; }
    // CLEARED THROUGH THE SETTERS, never by discarding the retained keys:
    // `useRetainedState`'s setter writes the retained copy inside a state
    // updater, so a discard followed by a set puts the value straight back.
    setNote(''); setDocument(''); setOwner('');
    setOverride(''); setChosen(null);
    onDone(chosen, r.body);
  }

  return (
    <div className="panel p-4 mt-6" data-testid="obligation-acts">
      <PanelHead
        title="What you can record against this"
        sub="Every act is appended with your name on it and cannot be taken back — a closed obligation takes no further act." />

      <div className="flex gap-2 mt-2 flex-wrap">
        {offered.map((a) => (
          <button key={a.key} type="button"
                  className={`btn btn-sm${chosen === a.key ? ' btn-primary' : ''}`}
                  data-testid={`act-${a.key}`}
                  aria-pressed={chosen === a.key}
                  onClick={() => {
                    setChosen(chosen === a.key ? null : a.key);
                    setRefused(null);
                  }}>
            {a.label.toLowerCase()}
          </button>
        ))}
      </div>

      {chosen === null && (
        <div className="caption mt-3">
          {/* THE ONE THAT SURPRISES PEOPLE, SAID BEFORE THEY PRESS IT. */}
          Recording an acknowledgement is evidence, not closure — the duty stays
          open and its date keeps running. Only "done" and "waived" close an
          obligation, and both are permanent.
          {!canWaive && <> Waiving is Legal's act and is not offered here.</>}
        </div>
      )}

      {chosen !== null && (
        <div className="panel-2 p-3 mt-3" data-testid="act-form">
          <div className="tag">{OBLIGATION_ACT_CHOICES.find((a) => a.key === chosen).label}</div>
          <div className="text-[12.5px] mt-1.5"
               style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            {chosen === 'satisfy' && (
              <>This closes the obligation permanently, with your name and the
                time on it. Say what was done — the record keeps your sentence,
                and a year from now it is the only thing that explains the
                closure.</>)}
            {chosen === 'ack' && (
              <><strong>This does not close the duty.</strong> It records that
                the counterparty acknowledged something, against the document
                that proves it. The obligation stays open and its date keeps
                running.</>)}
            {chosen === 'reassign' && (
              <>The duty itself is unchanged — its wording, its date and its
                clause all stay. What changes is who answers for it, and the
                handover is on the record.</>)}
            {chosen === 'waive' && (
              <>This closes an obligation that was <em>not</em> done. It is
                authorised by an override that was socialised, waited out its
                window and was approved finding by finding — never by this
                screen.</>)}
          </div>

          {/* ── The note ─────────────────────────────────────────────── */}
          {chosen !== 'ack' ? (
            <div className="mt-3">
              <div className="section-label">
                {chosen === 'satisfy' ? 'What was done'
                  : chosen === 'reassign' ? 'Why (optional)'
                  : 'Why this is being waived'}
              </div>
              <textarea className="mt-1.5 w-full" rows={3}
                        aria-label={chosen === 'satisfy' ? 'What was done'
                          : chosen === 'reassign' ? 'Why this is being handed over'
                          : 'Why this obligation is being waived'}
                        data-testid="act-note"
                        value={note}
                        onChange={(e) => { setNote(e.target.value); setRefused(null); }} />
              {chosen !== 'reassign' && (
                <div className="caption mt-1.5">
                  Required. The record refuses a blank one — a closure nobody
                  explained is a closure nobody can answer for.
                </div>
              )}
            </div>
          ) : (
            <div className="mt-3">
              <div className="section-label">A note (optional)</div>
              <textarea className="mt-1.5 w-full" rows={2}
                        aria-label="A note about the acknowledgement"
                        data-testid="act-note"
                        value={note}
                        onChange={(e) => { setNote(e.target.value); setRefused(null); }} />
            </div>
          )}

          {/* ── The evidence ─────────────────────────────────────────── */}
          {(chosen === 'satisfy' || chosen === 'ack') && (
            <div className="mt-3">
              <div className="section-label">
                {chosen === 'ack' ? 'The document that proves it'
                  : 'A received document as evidence (optional)'}
              </div>
              <EvidenceChooser
                documents={documents}
                documentsFailed={documentsFailed}
                agreementId={obligation.agreement_id}
                value={document}
                required={chosen === 'ack'}
                label={chosen === 'ack' ? 'The document that proves the acknowledgement'
                  : 'A received document to cite as evidence'}
                onChange={(v) => { setDocument(v); setRefused(null); }} />
            </div>
          )}

          {/* ── The new owner ────────────────────────────────────────── */}
          {chosen === 'reassign' && (
            <div className="mt-3">
              <div className="section-label">Who holds it now</div>
              <ObligationOwnerChooser
                people={people}
                value={owner}
                current={obligation.owner_person}
                onChange={(v) => { setOwner(v); setRefused(null); }} />
            </div>
          )}

          {/* ── The authority ────────────────────────────────────────── */}
          {chosen === 'waive' && (
            <div className="mt-3">
              <div className="section-label">The approval that authorises it</div>
              {authorised.length === 0 ? (
                <div className="caption mt-1.5" data-testid="no-waiver-authority">
                  Nothing you can read approves waiving this obligation. A waiver
                  rides the override machinery rather than having a second
                  approval system of its own: somebody raises an override whose
                  finding is{' '}
                  <span className="font-mono">{waiverFindingRef(id)}</span>, the
                  watchers are told, the review window runs, and a Legal reviewer
                  who did not raise it approves that finding. Until then there is
                  nothing here to press, and pressing anyway would be refused in
                  those words.
                </div>
              ) : (
                <>
                  <select className="w-full font-mono" style={{ padding: '6px 8px' }}
                          aria-label="The approved override that authorises this waiver"
                          data-testid="waiver-authority"
                          value={override}
                          onChange={(e) => { setOverride(e.target.value); setRefused(null); }}>
                    <option value="">choose the approval…</option>
                    {authorised.map((f) => (
                      <option key={f.request_id} value={String(f.request_id)}>
                        override {f.request_id} · approved by {f.decided_by}
                        {f.decided_at ? ` · ${String(f.decided_at).slice(0, 10)}` : ''}
                      </option>
                    ))}
                  </select>
                  <div className="caption mt-1.5">
                    {authorised.length === 1 ? 'One approval names' : `${authorised.length} approvals name`}
                    {' '}this obligation. The record checks the same thing again
                    when you press: a proposal is not an approval.
                  </div>
                </>
              )}
            </div>
          )}

          <ActRefusal reason={refused} />

          <div className="flex items-center gap-3 mt-4 flex-wrap">
            <ActButton className="btn btn-primary" data-testid="act-perform"
                       disabled={!ready}
                       onClick={perform}>
              {chosen === 'satisfy' ? 'record it as done'
                : chosen === 'ack' ? 'record the acknowledgement'
                : chosen === 'reassign' ? 'hand it over'
                : 'record the waiver'}
            </ActButton>
            <button type="button" className="btn"
                    onClick={() => { setChosen(null); setRefused(null); }}>
              cancel
            </button>
            <span className="caption">
              {chosen === 'ack'
                ? 'The obligation stays open afterwards.'
                : chosen === 'reassign'
                ? 'The duty is unchanged; only who answers for it moves.'
                : 'Permanent. A closed obligation takes no further act.'}
            </span>
          </div>
        </div>
      )}
    </div>
  );
}

// ── Naming a person ───────────────────────────────────────────────────────
//
// A PERSON, NEVER A TEAM INBOX — `0037`'s constraint, and the reason is that a
// duty owned by an address is a duty nobody is answerable for. The chooser
// reads the account register, which every signed-in role may read, and falls
// back to typing rather than blocking: a refused register would otherwise make
// a handover impossible for a reason that has nothing to do with the handover.
function ObligationOwnerChooser({ people, value, current, onChange }) {
  const rows = (people || []).filter((p) => p.state === 'active');

  if (rows.length === 0) {
    return (
      <div>
        <input type="text" className="mt-1.5 w-full font-mono"
               placeholder="name@clausewerk"
               aria-label="Who holds this obligation now"
               data-testid="owner-typed"
               value={value}
               onChange={(e) => onChange(e.target.value)} />
        <div className="caption mt-1.5">
          The account register did not answer, so the name is typed. The record
          still checks it: a handover to somebody who does not exist is refused.
        </div>
      </div>
    );
  }

  return (
    <div>
      <select className="w-full font-mono mt-1.5" style={{ padding: '6px 8px' }}
              aria-label="Who holds this obligation now"
              data-testid="owner-choose"
              value={value}
              onChange={(e) => onChange(e.target.value)}>
        <option value="">choose a person…</option>
        {rows.map((p) => (
          <option key={p.person} value={p.person}>
            {p.person} — {p.display_name}
            {p.unit ? ` · ${p.unit}` : ''}
            {p.effective_role ? ` · ${p.effective_role}` : ' · no effective role'}
          </option>
        ))}
      </select>
      {current && (
        <div className="caption mt-1.5">
          Held today by <span className="font-mono">{current}</span>.
        </div>
      )}
    </div>
  );
}

// ── One obligation, open ──────────────────────────────────────────────────
function ObligationRecord({ me, obligation, documents, documentsFailed, people,
                            approvals, onClose, onActed }) {
  if (!obligation) {
    return (
      <div>
        <PaneHead title="Obligation" kicker="the book"
                  sub="Nothing on the book answers to that reference." />
        <Empty
          kicker="not found"
          line="No obligation you can read has that reference."
          sub="It may belong to a deal outside your grant, or the address may be
               stale. Nothing was hidden from this screen — the book simply did
               not answer with it."
          action={<button type="button" className="btn" onClick={onClose}>back to the book</button>} />
      </div>
    );
  }

  const o = obligation;
  const closed = Boolean(o.closed_as);

  return (
    <div>
      <PaneHead
        kicker={`obligation ${o.obligation_id}${o.entitlement ? ' · owed to us' : ''}`}
        title={o.agreement_id}
        sub={o.summary}
        right={<button type="button" className="btn" onClick={onClose}>back to the book</button>} />

      {/* ── WHERE IT CAME FROM, ADJACENT ALWAYS ─────────────────────────
          An obligation answers to the wording that created it. The clause and
          its version travel with every row of this book for that reason, and a
          record view that dropped them would be the one screen where somebody
          cannot check. */}
      <div className="panel p-4 mt-6">
        <PanelHead title="What it is"
                   sub="Declared on the clause record when the agreement was executed — never extracted from prose." />
        <div className="grid gap-3 mt-2"
             style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))' }}>
          <ObligationFact label="the wording that created it"
                value={`${o.clause_id}@v${o.version}`} mono />
          <ObligationFact label="kind" value={o.kind} />
          <ObligationFact label="occurrence" value={o.occurrence > 0 ? `#${o.occurrence}` : 'one only'} />
          <ObligationFact label="due" value={o.due_on || 'no date yet'} mono />
          <ObligationFact label="notice period"
                value={o.lead_days === null || o.lead_days === undefined
                  ? 'none' : `${o.lead_days} days`} />
          <ObligationFact label="survives termination" value={o.survives ? 'yes' : 'no'} />
          <ObligationFact label="evidence required" value={o.evidence || 'none declared'} />
          <ObligationFact label="who answers for it"
                value={o.entitlement ? 'the counterparty owes us'
                  : (o.owner_person || 'NOBODY')}
                mono={!o.entitlement} />
        </div>
        {!o.due_on && (
          <div className="caption mt-3">
            {/* AN ABSENCE WITH ITS REASON, never a blank. */}
            Anchored to an event that has not happened — a termination date,
            usually. It is on the book and it has no date, which is a fact
            rather than a gap in the record.
          </div>
        )}
      </div>

      {/* ── WHERE IT STANDS ─────────────────────────────────────────────
          Closed states are RECORDED ACTS with a name on them, and this says
          whose. Nothing closes silently in this product; a screen that showed
          "closed" without the person would be the one place it did. */}
      <div className="panel p-4 mt-6" data-testid="obligation-state">
        <PanelHead title="Where it stands"
                   sub="pending, due and overdue are computed on every read; overdue is arithmetic and never breach." />
        <div className="flex items-center gap-3 mt-2 flex-wrap">
          {stateChip(o)}
          {o.closed_at && (
            <span className="caption">
              on {String(o.closed_at).slice(0, 10)}
            </span>
          )}
          {o.breach_asserted_by && (
            <span className="chip chip-err" data-testid="breach-asserted">
              breach asserted by {o.breach_asserted_by}
            </span>
          )}
        </div>
        <div className="caption mt-3">
          {/* THE LIMIT OF WHAT THIS SCREEN CAN SHOW, said rather than implied.
              The book answers with the obligation's STATE — what closed it and
              who — and there is no read anywhere that returns the full list of
              acts recorded against one obligation. The audit record has them
              all; saying so is better than a screen that looks complete. */}
          Every act recorded against this obligation is on the audit record with
          the actor and the time. This screen shows what the book carries — how
          it stands, and who closed it if anybody has; the full sequence is the
          Auditor's chain.
        </div>
      </div>

      {closed ? (
        <div className="mt-6">
          <Empty
            kicker="closed"
            line={`This obligation was ${o.closed_as} by ${o.closed_by}.`}
            sub="A decision is not revisited. The record refuses any further act
                 on a closed obligation, and it refuses it in the database rather
                 than on this screen — so nothing here can quietly reopen it." />
        </div>
      ) : mayActOnObligations(me) ? (
        <ObligationActs
          me={me} obligation={o} documents={documents}
          documentsFailed={documentsFailed}
          people={people} approvals={approvals} onDone={onActed} />
      ) : (
        <div className="mt-6">
          <Empty
            kicker="not yours to record"
            line="Recording against an obligation is the work of Legal, or of the requester whose deal it is."
            sub="Your grant reads the whole book — deliberately, because colleagues
                 cover for each other — and writes none of it. What was recorded,
                 by whom and when is above." />
        </div>
      )}
    </div>
  );
}

// One stated fact. A label and a value, and a value that is missing says so
// rather than rendering an empty box beside a label that promises something.
function ObligationFact({ label, value, mono }) {
  return (
    <div>
      <div className="caption">{label}</div>
      <div className={`text-[13px] mt-0.5${mono ? ' font-mono' : ''}`}
           style={{ color: 'var(--ink)', wordBreak: 'break-word' }}>
        {value === null || value === undefined || value === '' ? '—' : String(value)}
      </div>
    </div>
  );
}

// ── The duties nobody holds ───────────────────────────────────────────────
//
// `cw.obligation_unowned` (0037) — "absence of an owner rendered as a gap,
// never as calm". It has had a grant and a read since that migration and no
// screen, so the one duty most likely to pass its date in silence was the one
// nothing pointed at. Each row OPENS the obligation, where it can be handed to
// somebody — a list of what is wrong that you can act from.
function UnownedObligations({ me, rows, onOpen }) {
  const filter = useListFilter(rows, {
    view: 'obligations:unowned',
    fields: ['obligation_id', 'agreement_id', 'kind'],
  });
  const canAct = mayActOnObligations(me);

  return (
    <div>
      <PanelHead
        title="Nobody holds these"
        sub="An obligation with no owner is the one that goes past its date without anybody noticing. Shown as a gap rather than left in the book to be found."
        right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="obligations-unowned"
                  placeholder="search by obligation, agreement, or kind" />
      {rows.length === 0 ? (
        <Empty
          kicker="all held"
          line="Every obligation on the book has somebody answering for it."
          sub="An entitlement — something the counterparty owes us — has no owner
               by its nature and is not counted here." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="obligations" />
      ) : (
        <div className="panel">
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>obligation</th><th>agreement</th><th>kind</th>
                <th>due</th><th>owner</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((r) => (
                <tr key={r.obligation_id}
                    {...(canAct
                      ? openableRow(() => onOpen(r.obligation_id),
                                    `open obligation ${r.obligation_id} to give it an owner`)
                      : {})}>
                  <td className="font-mono">{r.obligation_id}</td>
                  <td className="font-mono">{r.agreement_id}</td>
                  <td>{r.kind}</td>
                  <td className="font-mono">{r.due_on || '—'}</td>
                  <td className="caption">{canAct ? 'nobody · hand it over →' : 'nobody'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── Which agreements the record says are finished ─────────────────────────
//
// NOBODY MARKS A DEAL CLOSED (0038). The record derives it, and an obligation
// that survives termination with no anchor date BLOCKS — fail-closed, because
// "we think it is finished" and "nothing is outstanding" are different claims
// and only the second is checkable. So this panel has no button on it, and
// that absence is the design rather than an omission.
function CloseableAgreements({ rows }) {
  const marked = useActMemo(
    () => (rows || []).map((r) => ({
      ...r,
      standing: r.closeable ? 'nothing outstanding' : 'still carrying duties',
    })), [rows]);
  const filter = useListFilter(marked, {
    view: 'obligations:finished',
    fields: ['agreement_id'],
    facet: 'standing',
  });
  const ready = (rows || []).filter((r) => r.closeable).length;

  return (
    <div>
      <PanelHead
        title="What the record says is finished"
        sub="Derived, never declared. An obligation that survives termination with no anchor date blocks a close — the record fails closed rather than guessing."
        right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="obligations-closeable"
                  placeholder="search by agreement" facetLabel="any standing" />
      {(rows || []).length === 0 ? (
        <Empty
          kicker="nothing to weigh"
          line="No agreement has obligations to weigh yet."
          sub="Close eligibility is computed from the book; an empty book makes
               the question unanswerable rather than answering it yes." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="agreements" />
      ) : (
        <>
          <div className="caption mb-2">
            {ready} of {rows.length} carry nothing outstanding. Nothing here
            closes anything — this is what the record computes, and closing a
            deal is a lifecycle act elsewhere.
          </div>
          <div className="panel">
            <table className="ledger w-full">
              <thead>
                <tr>
                  <th>agreement</th><th>surviving and open</th><th>the record says</th>
                </tr>
              </thead>
              <tbody>
                {filter.shown.map((r) => (
                  <tr key={r.agreement_id}>
                    <td className="font-mono">{r.agreement_id}</td>
                    <td className="font-mono">{r.surviving_open}</td>
                    <td>
                      {r.closeable
                        ? <Status state="effective">nothing outstanding</Status>
                        : <Status state="pending">still carrying duties</Status>}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </>
      )}
    </div>
  );
}
