// Raising what you observed, and receiving it (NT-3, NT-4).
//
// Mike, 2026-08-05: "the admin should have a lot of abilities to notify
// different user types based on data they observe. Things have to be escalated
// somewhere."
//
// TWO COMPONENTS AND ONE RULE. `RaiseNotice` is a control that sits BESIDE the
// row it is about, pre-filled with that row's reference — so the citation
// cannot be mistyped and cannot be invented. `NoticesWaiting` is the other
// end: an open notice addressed to you or your role, in the workspace you
// already open, acknowledged as its own act with its own words.
//
// WHAT IS NOT HERE, AND MUST NOT ARRIVE:
//
//   · A compose box. There is no way on this screen to raise a notice about
//     nothing — every entry point passes a subject kind and a reference it
//     read off a row. The database refuses one that does not resolve, so a
//     free-text form would be a form that mostly fails; the reason it is
//     absent is better than that.
//
//   · A bell, a badge, or a red dot. An open notice appears in the waiting
//     panel and the daily digest, from one derivation. A bell would compete
//     with the waiting list and win, which is how the waiting list stops being
//     read.
//
//   · An acknowledge-all. One notice, one act, one reason — the same argument
//     the override findings settled.

const { useState } = React;

// ── Raising one ───────────────────────────────────────────────────────────
// `subject` is {kind, ref, about} — `about` being what to call it on screen.
// `routes` is GET /notice-routes, used ONLY to decide what to offer. It is not
// a permission check: the database refuses an unrouted notice whatever this
// renders, and the reason the list is consulted at all is that offering a
// button which always fails is worse than offering none.
function RaiseNotice({ me, subject, routes, onRaised }) {
  const [open, setOpen] = useState(false);
  const [to, setTo] = useState('');
  const [note, setNote] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);
  const [done, setDone] = useState(false);

  const available = (routes ?? []).filter(
    (r) => r.raiser_role === me.role && r.subject_kind === subject.kind);

  // No route from this role for this kind of thing: no control. Not a disabled
  // one — a disabled button is a promise the system will not keep, and there
  // is nothing here for the person to do about it.
  if (available.length === 0) return null;

  if (done) {
    return (
      <span className="caption" data-testid="notice-raised">
        raised · it is now on their waiting list
      </span>
    );
  }

  if (!open) {
    return (
      <button className="btn btn-sm" data-testid="raise-notice"
              onClick={() => { setOpen(true); setTo(available[0].to_role); }}>
        raise this
      </button>
    );
  }

  return (
    <div className="panel-2 p-3 mt-2" data-testid="raise-notice-form">
      <div className="section-label">Raise “{subject.about}”</div>

      {/* WRAPPING, AND A FLOOR UNDER THE NOTE. A hard-coded width eventually
          crowds its NEIGHBOUR, which is the half of S326 nothing was watching
          for: at 375px the 180px select left the note field — the one carrying
          the whole message — measuring 27 pixels. Nothing overflowed, so every
          "past the right edge" sweep called it clean. `flex-wrap` plus a
          min-width means the note drops to its own line rather than being
          crushed, and the rule cannot be forgotten at a new call site the way
          an inline width can. */}
      <div className="flex gap-2 items-end mt-2 flex-wrap">
        <div style={{ width: 180, flex: '0 1 180px' }}>
          <label className="caption">To</label>
          <select aria-label="To" className="mt-1 w-full font-mono" style={{ padding: '4px 8px' }}
                  data-testid="notice-to"
                  value={to} onChange={(e) => setTo(e.target.value)}>
            {available.map((r) => (
              <option key={r.to_role} value={r.to_role}>{r.to_role}</option>
            ))}
          </select>
        </div>
        {/* `flex: 1 1 220px`, NOT `flex-1`. `flex-1` is basis `0%`, so the
            browser decides the field "fits" however narrow the row is and
            never wraps it — then `min-width` forces it wide again and the
            panel grows a scrollbar. Giving it a real basis is what makes
            `flex-wrap` above actually fire. */}
        <div style={{ flex: '1 1 220px', minWidth: 220 }}>
          <label className="caption">What you want them to know</label>
          <input aria-label="What you want them to know" className="mt-1 w-full" data-testid="notice-note"
                 value={note} onChange={(e) => setNote(e.target.value)} />
        </div>
      </div>

      {/* WHY THIS ROUTE EXISTS, in the words the migration recorded. It is not
          decoration: it is the answer to "why am I telling THEM about this",
          and it was written by whoever added the pair. */}
      <div className="caption mt-2">
        {(available.find((r) => r.to_role === to) || {}).why}
      </div>

      {error && (
        <div className="text-[12.5px] mt-2" style={{ color: 'var(--danger)' }}>
          {error}
        </div>
      )}

      <div className="flex gap-2 mt-3">
        <ActButton className="btn btn-primary" disabled={busy || !note.trim()}
                data-testid="send-notice"
                onClick={async () => {
                  setBusy(true); setError(null);
                  const r = await API.raiseNotice({
                    to_role: to,
                    subject_kind: subject.kind,
                    subject_ref: subject.ref,
                    note: note.trim(),
                  });
                  setBusy(false);
                  if (!r.ok) { setError(r.reason); return; }
                  setOpen(false); setDone(true); setNote('');
                  if (onRaised) onRaised();
                }}>
          ✓ raise it
        </ActButton>
        <button className="btn" onClick={() => { setOpen(false); setError(null); }}>
          cancel
        </button>
      </div>

      <div className="caption mt-2">
        This warns them. It blocks nothing and approves nothing — it lands on
        their waiting list, with your name on it, until somebody acknowledges it.
      </div>
    </div>
  );
}

// ── Receiving one ─────────────────────────────────────────────────────────
// Rendered at the top of a workspace, above whatever that workspace opens on,
// and ONLY when something is actually open. No empty panel: a heading with
// nothing under it teaches people to skip the heading.
function NoticesWaiting({ me }) {
  const pane = usePane(() => API.notices());
  const [busy, setBusy] = useState(null);
  const [notes, setNotes] = useState({});
  const [error, setError] = useState(null);

  // A refused read shows nothing rather than an error at the top of every
  // workspace: notices are an addition to a screen that already worked, and a
  // failure here must not swallow the pane behind it. The one place that
  // WOULD be dishonest — showing zero — is not what this does; it shows
  // nothing at all, and the count is absent rather than wrong.
  //
  // BUT A REFUSED READ AND A BROKEN ONE ARE NOT THE SAME NEWS, and this drew
  // the same nothing for both.
  //
  // A 403 IS THE RECORD WORKING, AND NOBODY SHOULD GET ONE NOW. Until 0098
  // this branch had a named occupant: the viewer held no grant on the notice
  // view, so their every workspace refused this read, and the comment here
  // said so at length. Mike's decision of 2026-08-22 — everyone can send and
  // receive messages — took that away, and the paragraph explaining it went
  // with the grant.
  //
  // THE BRANCH STAYS, because what it distinguishes is still true and is
  // still worth distinguishing: a refusal is the system saying no, and a
  // broken read is the system failing. If a role is ever fenced out of the
  // message record again, this draws nothing rather than shouting about a
  // fault. What it must NEVER do is draw a zero — see below.
  if (pane.status === 'failed' && pane.http === 403) return null;
  if (pane.status === 'loading') return null;

  // ANY OTHER FAILURE IS DIFFERENT, and the silence was hiding it. This panel
  // is the only place an escalation is put in front of the person it was
  // raised to. Drawn as nothing, a broken read means somebody is not told a
  // thing that was raised to them and NOBODY LEARNS — on the one surface whose
  // whole job is to tell you. A legal_admin or an auditor, who genuinely do
  // receive notices, would simply stop seeing them.
  //
  // It still does not swallow the pane behind it, and it still never shows a
  // zero: one quiet line, saying the thing could not be read and what that
  // does NOT mean.
  if (pane.status === 'failed') {
    return (
      <div className="panel p-3 mb-6" data-testid="notices-unreadable"
           style={{ borderColor: 'var(--danger)' }}>
        <div className="tag" style={{ color: 'var(--danger)' }}>not read</div>
        <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)' }}>
          Anything raised with you could not be read just now, so this is not
          “nothing is waiting” — it is “we do not know”. The daily digest reads
          the same derivation and is unaffected.
          {pane.reason && <> <span className="font-mono">{pane.reason}</span></>}
        </div>
      </div>
    );
  }
  if (pane.status !== 'loaded') return null;

  const mine = pane.rows.filter(
    (n) => n.state === 'open'
        && (n.to_person === me.person || n.to_role === me.role));
  if (mine.length === 0) return null;

  return (
    <div className="panel p-4 mb-6" data-testid="notices-waiting">
      <PanelHead
        title={mine.length === 1 ? 'Somebody raised something with you'
                                 : `${mine.length} things were raised with you`}
        sub="Observed by a colleague and sent here. Nothing is blocked by it — it stays until somebody says they have seen it." />

      {error && (
        <div className="text-[12.5px] mb-2" style={{ color: 'var(--danger)' }}>
          {error}
        </div>
      )}

      {mine.map((n) => (
        <div className="panel-2 p-3 mb-2" key={n.notice_id} data-testid="notice">
          <div className="flex items-baseline justify-between gap-3">
            <div className="min-w-0">
              <span className="font-mono text-[12.5px]">
                {n.subject_kind}: {n.subject_ref}
              </span>
              <span className="ml-3 caption">
                raised by {n.raised_by} · {since(n.raised_at)}
              </span>
            </div>
            <Status state="pending">open</Status>
          </div>

          {/* Their own words, in the idiom that means a person said this. */}
          <div className="panel-2 p-3 mt-2 relative">
            <span className="font-serif" style={{
              position: 'absolute', left: 6, top: -6, fontSize: 34,
              color: 'var(--accent)', opacity: .55, lineHeight: 1 }}>“</span>
            <div className="font-serif italic"
                 style={{ fontSize: 15, lineHeight: 1.6, paddingLeft: 22 }}>
              {n.note}
            </div>
          </div>

          <div className="flex gap-2 items-end mt-3">
            <div className="flex-1">
              <label className="caption">What you did about it (optional)</label>
              <input aria-label="What you did about it (optional)" className="mt-1 w-full" style={{ padding: '4px 8px' }}
                     data-testid={`ack-note-${n.notice_id}`}
                     value={notes[n.notice_id] || ''}
                     onChange={(e) => setNotes({ ...notes, [n.notice_id]: e.target.value })} />
            </div>
            <ActButton className="btn" disabled={busy === n.notice_id}
                    data-testid={`acknowledge-${n.notice_id}`}
                    onClick={async () => {
                      setBusy(n.notice_id); setError(null);
                      const r = await API.acknowledgeNotice({
                        notice_id: n.notice_id,
                        note: (notes[n.notice_id] || '').trim() || null,
                      });
                      setBusy(null);
                      if (!r.ok) { setError(r.reason); return; }
                      pane.reload();
                    }}>
              ✓ I have seen this
            </ActButton>
          </div>
        </div>
      ))}
    </div>
  );
}

// ── The place a notice lives ───────────────────────────────────────────────
// WHAT WAS MISSING, and it was missing for everybody: you could raise a notice
// and never learn what became of it. `NoticesWaiting` above draws the ones
// that are OPEN and addressed to YOU, as a banner over whatever pane you
// happened to open — and the moment somebody acknowledges one it is gone from
// every screen in the application.
//
// THE RECORD KEPT ALL OF IT THE WHOLE TIME. `GET /notices` already answers
// "your own raised notices, the ones addressed to you or your role, and Legal
// and the Auditor in full", and it already carries `state`, `acknowledged_by`,
// `acknowledged_at` and `acknowledgement_note`. The screen was reading that
// and throwing away everything except one slice of it. So this pane needed no
// endpoint, no read and no migration — it needed somewhere to put what had
// already been fetched.
//
// IT MATTERS MORE SINCE 0098. When only a few internal roles could raise a
// notice, "raise it and hope" was a small hole. Now that a viewer — a
// counterparty's counsel or an insurer — can put something on Legal's waiting
// list, a person who cannot see what became of their own message has no way to
// tell "answered" from "ignored", and will raise it again.
//
// THREE SETS, ALL DERIVED, NONE LISTED. Asked of each row rather than
// enumerated, so a new notice state or a new address kind lands in the right
// place on its own instead of falling silently into the wrong one — S312's
// defect, and S330's.
function NoticesPane({ me }) {
  const pane = usePane(() => API.notices());
  // Unsent work survives a change of tab. An acknowledgement note is somebody's
  // prose about what they did, and a click on the rack used to destroy it.
  // In memory, never in storage — this names a subject and a person.
  const [notes, setNotes] = useRetainedState('notice-ack-notes', {});
  const [error, setError] = useState(null);

  // EVERY HOOK ABOVE EVERY EARLY RETURN. A hook after `if (…) return <Loading/>`
  // blanks the pane on the render AFTER the data lands — the one nobody
  // watches (S318). `useListFilter` copes with `rows` being undefined.
  const filter = useListFilter(pane.rows, {
    view: 'notices:all',
    fields: ['subject_ref', 'subject_kind', 'raised_by', 'to_role', 'note',
             'acknowledged_by', 'acknowledgement_note'],
    facet: 'subject_kind',
  });

  if (pane.status === 'loading') return <Loading />;
  if (pane.status === 'failed') return <LoadFailed reason={pane.reason} />;

  // WHO A NOTICE IS FOR, asked of the row. A notice reaches you by naming you
  // or by naming your role, and both are addresses — reading only one of them
  // would quietly hide half of somebody's post.
  const addressedToMe = (n) => n.to_person === me.person || n.to_role === me.role;
  const raisedByMe = (n) => n.raised_by === me.person;

  const waiting  = pane.rows.filter((n) => n.state === 'open' && addressedToMe(n));
  const mine     = pane.rows.filter(raisedByMe);
  const unanswered = mine.filter((n) => n.state === 'open');
  const answered   = mine.filter((n) => n.state !== 'open');

  // A FIGURE LEADS TO WHAT IT COUNTED (S333). Each of these narrows the one
  // list below to exactly the rows it counted, and pressing it again clears
  // the narrowing — so the number and the rows are the same set by
  // construction rather than by coincidence.
  // Registered as the tile renders, so a saved view can put the focus back.
  const focus = (key, label, test) => {
    const f = filter.focusable(key, label, test);
    return () => filter.focusOn(f);
  };

  return (
    <div>
      <PaneHead
        title="Notices"
        kicker="raised, received, answered"
        sub="Everything raised to you, and everything you raised — including what became of it. A notice blocks nothing and approves nothing." />

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

      {/* Every one of these is a MEASURED number, so every one of them drills —
          including a zero, which is a fact somebody established rather than a
          gap (S333). */}
      <div className="mb-4 grid gap-3" data-testid="notice-figures"
           style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(170px, 1fr))' }}>
        <StatBox label="waiting on you" n={waiting.length}
                 to={focus('waiting', 'waiting on you',
                           (n) => n.state === 'open' && addressedToMe(n))}
                 describe={`show the ${waiting.length} waiting on you`} />
        <StatBox label="you raised" n={mine.length}
                 to={focus('mine', 'you raised', raisedByMe)}
                 describe={`show the ${mine.length} you raised`} />
        <StatBox label="yours, no answer yet" n={unanswered.length}
                 to={focus('unanswered', 'yours, no answer yet',
                           (n) => raisedByMe(n) && n.state === 'open')}
                 describe={`show the ${unanswered.length} of yours with no answer yet`} />
        <StatBox label="yours, answered" n={answered.length}
                 to={focus('answered', 'yours, answered',
                           (n) => raisedByMe(n) && n.state !== 'open')}
                 describe={`show the ${answered.length} of yours that were answered`} />
      </div>

      <div className="panel p-4">
        <PanelHead
          title="The message record"
          sub="Newest first. You see what you raised, what was addressed to you or your role — and Legal and the Auditor see it in full."
          right={<FilterCount filter={filter} />} />

        <ListFilter filter={filter} testid="notices"
                    placeholder="search a reference, a person or the words"
                    facetLabel="any subject" />

        {filter.total === 0 && (
          <div className="caption mt-3" data-testid="notices-empty">
            Nothing has been raised to you and you have raised nothing. This is a
            measured nought, not a read that failed — a failed read says so.
          </div>
        )}

        {filter.total > 0 && filter.shown.length === 0 && (
          <NoMatch kicker="no notice matches" noun="notice" />
        )}

        {filter.shown.map((n) => {
          const canAck = n.state === 'open' && addressedToMe(n);
          return (
            <div className="panel-2 p-3 mb-2" key={n.notice_id} data-testid="notice-row">
              <div className="flex items-baseline justify-between gap-3 flex-wrap">
                <div className="min-w-0">
                  {/* WHICH KIND OF REFERENCE, always. One column carries several
                      kinds, and a bare `9` under the same heading as `AG-26-041`
                      is S316's finding. */}
                  <span className="font-mono text-[12.5px]">
                    {n.subject_kind}: {n.subject_ref}
                  </span>
                  <span className="ml-3 caption">
                    {raisedByMe(n) ? 'you raised this' : `raised by ${n.raised_by}`}
                    {' · '}{since(n.raised_at)}
                    {' · to '}<span className="font-mono">{n.to_person || n.to_role}</span>
                  </span>
                </div>
                {n.state === 'open'
                  ? <Status state="pending">open</Status>
                  : <Status state="neutral">answered</Status>}
              </div>

              {/* Their own words, in the idiom that means a person said this. */}
              <div className="panel-2 p-3 mt-2 relative">
                <span className="font-serif" aria-hidden="true" style={{
                  position: 'absolute', left: 6, top: -6, fontSize: 34,
                  color: 'var(--accent)', opacity: .55, lineHeight: 1 }}>“</span>
                <div className="font-serif italic"
                     style={{ fontSize: 15, lineHeight: 1.6, paddingLeft: 22 }}>
                  {n.note}
                </div>
              </div>

              {/* WHAT BECAME OF IT — the whole reason this pane exists. Absent
                  while it is open, because "nobody has answered yet" is not an
                  answer and must not be drawn as one. */}
              {n.state !== 'open' && (
                <div className="caption mt-2" data-testid="notice-outcome">
                  Seen by <span className="font-mono">{n.acknowledged_by}</span>
                  {n.acknowledged_at ? <> · {since(n.acknowledged_at)}</> : null}
                  {n.acknowledgement_note
                    ? <> — “<span className="font-serif italic">{n.acknowledgement_note}</span>”</>
                    : <> — no note was left</>}
                </div>
              )}

              {canAck && (
                <div className="flex gap-2 items-end mt-3 flex-wrap">
                  <div className="flex-1" style={{ minWidth: 200 }}>
                    <label className="caption">What you did about it (optional)</label>
                    <input aria-label="What you did about it (optional)"
                           className="mt-1 w-full" style={{ padding: '4px 8px' }}
                           data-testid={`pane-ack-note-${n.notice_id}`}
                           value={notes[n.notice_id] || ''}
                           onChange={(e) => setNotes({ ...notes, [n.notice_id]: e.target.value })} />
                  </div>
                  <ActButton className="btn"
                             data-testid={`pane-acknowledge-${n.notice_id}`}
                             onClick={async () => {
                               setError(null);
                               const r = await API.acknowledgeNotice({
                                 notice_id: n.notice_id,
                                 note: (notes[n.notice_id] || '').trim() || null,
                               });
                               if (!r.ok) { setError(r.reason); return; }
                               pane.reload();
                             }}>
                    ✓ I have seen this
                  </ActButton>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}
