// WHO WE HOLD PAPER WITH.
//
// WHAT THIS IS. Until `0112` a supplier was a name somebody typed on a deal.
// `0045` said so in its own comment — friction is grouped verbatim, so a
// misspelling was a different supplier — and the consequence was bigger than
// an untidy report: the question "which OTHER contracts do we hold with this
// company?" had no answer, because the set had no definition.
//
// THIS IS THE SCREEN THAT DID NOT EXIST. Thirty-four panes, and not one of
// them let a person press a supplier and see their contracts. `vendors` is a
// flat league table of negotiation friction per typed name and stops there.
//
// THREE ANSWERS, AND THE THIRD IS THE POINT. Every deal on this screen is
// `linked` (somebody said so), `matched` (its typed name reduces to a known
// one) or `unresolved` — and unresolved is DRAWN, loudly, never filtered out.
// A supplier page that quietly dropped the deals it could not place would
// answer "nothing else is affected" when it means "we did not look", which is
// the most dangerous sentence this feature could produce.
//
// WHAT IT DOES NOT DO. It does not merge two suppliers, because a merge is a
// different act with a different record and `0112` deliberately grants nobody
// DELETE on a supplier. It does not read contract text, and nothing here is a
// gate: which company a deal is with binds nobody until a person says it does.

const { useState, useMemo } = React;

// WHO MAY DO WHICH ACT, read back from `0112`'s policies AS 0126 LEFT THEM.
// Curating the record — creating a supplier, adding a spelling it answers to —
// was the Legal admin's alone on ADR-0014's reasoning: this record decides
// which contracts are read together, so it needs a named owner. Mike moved it
// to procurement as well on 2026-08-30, both halves, because the role that
// decides who bids could not name a company nobody had bought from without
// asking a lawyer first. Legal keeps the act; this is a second hand, not a
// swap. Resolving a DEAL is wider still, because a requester knows who they
// are buying from.
//
// Affordances, not permissions. The database refuses regardless, and a control
// that is always refused teaches people to stop pressing.
const MAY_CURATE = ['legal_admin', 'procurement'];
const MAY_LINK = ['requester', 'legal_reviewer', 'legal_admin'];
// READING THEIR PAPER (0114). The same three, and not by coincidence: 0114's
// `build_paragraph_index` policy names Legal and a requester on their own deal,
// because the build reads approved language on a named deal and records what
// was read — which is contract work. THE ADMINISTRATOR IS ABSENT, and that is
// ADR-0011 rather than an oversight: they switch the feature on and set the
// allowance, and decide nothing inside any workflow.
const MAY_BUILD = ['requester', 'legal_reviewer', 'legal_admin'];

const RESOLUTION_INK = {
  linked: 'chip-ok',
  matched: 'chip-std',
  unresolved: 'chip-high',
};

// How a deal came to be attached to this supplier, in a sentence rather than a
// word. A person deciding whether to trust the set needs to know the
// difference between "somebody said so" and "the spelling matched".
const RESOLUTION_SAYS = {
  linked: 'somebody resolved this deal to this supplier by hand',
  matched: 'the counterparty name on the deal reduces to this supplier’s own '
         + 'name or one of its recorded spellings',
  unresolved: 'this deal’s counterparty matches no supplier record — it is in '
            + 'no supplier’s set, and no cross-contract answer covers it',
};

function SuppliersPane({ me }) {
  const acts = useActs();
  const suppliers = usePane(() => API.suppliers());
  const deals = usePane(() => API.supplierDeals());
  const aliases = usePane(() => API.supplierAliases());
  const unresolved = usePane(() => API.supplierUnresolved());

  // The supplier you have open lives in the address, so "look at this one" is
  // a link somebody can send. `useAddressedRecord`'s reason, kept at a new
  // site rather than re-argued.
  const [open, setOpen] = useAddressedRecord('suppliers');

  const mayCurate = MAY_CURATE.includes(me.role);
  const mayLink = MAY_LINK.includes(me.role);
  const mayBuild = MAY_BUILD.includes(me.role);

  const loading = [suppliers, deals, aliases, unresolved]
    .some((p) => p.status === 'loading');
  const failed = [suppliers, deals, aliases, unresolved]
    .find((p) => p.status === 'failed');

  const reloadAll = () => {
    suppliers.reload(); deals.reload(); aliases.reload(); unresolved.reload();
  };

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

  const openRow = open
    ? suppliers.rows.find((s) => String(s.supplier_id) === String(open))
    : null;

  if (openRow) {
    return (
      <OneSupplier
        me={me} supplier={openRow} acts={acts} mayCurate={mayCurate}
        mayLink={mayLink} mayBuild={mayBuild}
        onClose={() => setOpen(null)} onDone={reloadAll}
        deals={deals.rows.filter(
          (d) => String(d.supplier_id) === String(openRow.supplier_id))}
        aliases={aliases.rows.filter(
          (a) => String(a.supplier_id) === String(openRow.supplier_id))} />
    );
  }

  return (
    <TheRegister
      me={me} acts={acts} mayCurate={mayCurate} mayLink={mayLink}
      suppliers={suppliers.rows} deals={deals.rows}
      unresolved={unresolved.rows} onOpen={setOpen} onDone={reloadAll} />
  );
}

// ── The register ──────────────────────────────────────────────────────────
function TheRegister({ me, acts, mayCurate, mayLink, suppliers, deals,
                       unresolved, onOpen, onDone }) {
  const [showNew, setShowNew] = useState(false);

  // THE HONEST DENOMINATOR, computed here and drawn beside every count. The
  // register says how many deals it has PLACED and how many it has not, and
  // the second number is a control that opens the worklist.
  const placed = deals.filter((d) => d.resolution !== 'unresolved').length;
  const unplaced = deals.length - placed;

  const filter = useListFilter(suppliers, {
    fields: ['name', 'reference', 'note'],
    view: 'suppliers',
  });

  return (
    <div>
      <PaneHead
        title="Suppliers"
        sub="Who we hold paper with — and which deals are, and are not, attached to one."
        right={mayCurate ? (
          <ActButton onClick={() => setShowNew((v) => !v)}>
            {showNew ? 'cancel' : 'new supplier'}
          </ActButton>
        ) : null} />

      <TileStrip tiles={[
        { label: 'suppliers', n: suppliers.length },
        { label: 'deals placed', n: placed,
          to: placed ? () => showSection('placed-deals') : null,
          describe: `show the ${placed} deals attached to a supplier` },
        // DRAWN EVEN WHEN IT IS NOUGHT, unlike every other figure on this
        // screen, because nought unplaced deals is the fact somebody most
        // wants confirmed before trusting a cross-contract answer.
        { label: 'deals not placed', n: unplaced,
          to: unplaced ? () => showSection('unresolved-work') : null,
          describe: `show the ${unplaced} deals matching no supplier record` },
      ]} />

      {showNew && mayCurate && (
        <NewSupplier acts={acts} onDone={() => { setShowNew(false); onDone(); }} />
      )}

      <ListFilter filter={filter} placeholder="find a supplier"
        testid="supplier-filter" />

      <WaitingList
        order="given"
        items={filter.shown.map((s) => ({
          key: String(s.supplier_id),
          title: s.name,
          sub: `${Number(s.deals)} deals · ${Number(s.executed)} signed · `
             + `${Number(s.linked)} resolved by hand · `
             + `${Number(s.aliases)} other spellings`
             + (s.reference ? ` · ${s.reference}` : ''),
          at: null,
        }))}
        onOpen={(key) => onOpen(key)}
        empty={<Empty kicker="suppliers"
          line="No supplier records yet."
          sub={mayCurate
            ? 'Create one, or open the unplaced deals below and work from the names already on them.'
            : 'A Legal admin creates these.'} />} />

      <section id="unresolved-work">
        <PanelHead
          title="Deals matching no supplier"
          sub="Grouped by comparison key, so four spellings of one company arrive as one piece of work." />
        {unresolved.length === 0 ? (
          <Empty kicker="unplaced deals"
            line="Every deal is attached to a supplier record." />
        ) : (
          <WaitingList
            order="given"
            items={unresolved.map((u) => ({
              key: u.counterparty_key,
              title: u.example_name,
              sub: `${Number(u.deals)} deals (${Number(u.executed)} signed)`
                 + (u.spellings && u.spellings.length > 1
                    ? ` · spelled ${u.spellings.length} ways: ${u.spellings.join(' · ')}`
                    : ''),
              at: null,
              chips: <span className="chip chip-high">unplaced</span>,
            }))}
            empty={null} />
        )}
        {!mayLink && (
          <p className="note">
            Attaching a deal to a supplier is done by the person whose deal it
            is, or by Legal.
          </p>
        )}
        {mayLink && unresolved.length > 0 && suppliers.length > 0 && (
          <AttachDeals
            acts={acts} suppliers={suppliers}
            deals={deals.filter((d) => d.resolution === 'unresolved')}
            onDone={onDone} />
        )}
      </section>
    </div>
  );
}

// ── One supplier ──────────────────────────────────────────────────────────
function OneSupplier({ me, supplier, deals, aliases, acts, mayCurate, mayLink,
                       mayBuild, onClose, onDone }) {
  const signed = deals.filter((d) => d.status === 'executed');
  const live = signed.filter(
    (d) => !d.term_end || new Date(d.term_end) >= new Date());
  const masters = deals.filter((d) => d.agreement_kind === 'master');
  const sows = deals.filter((d) => d.agreement_kind === 'sow');

  return (
    <div>
      <PaneHead
        title={supplier.name}
        kicker="supplier"
        sub={`Created by ${supplier.created_by}`
           + (supplier.reference ? ` · ${supplier.reference}` : '')}
        right={<ActButton onClick={onClose}>back to suppliers</ActButton>} />

      {supplier.note && <p className="note">{supplier.note}</p>}

      <TileStrip tiles={[
        { label: 'deals', n: deals.length },
        { label: 'signed', n: signed.length },
        { label: 'still in force', n: live.length },
        // Masters and statements of work are drawn only when there are any:
        // a company with neither should not be shown two noughts suggesting
        // the shape is missing.
        ...(masters.length ? [{ label: 'masters', n: masters.length }] : []),
        ...(sows.length ? [{ label: 'work orders', n: sows.length }] : []),
      ]} />

      <PanelHead
        title="Their paper"
        sub="Every deal attached to this supplier, and how it came to be attached." />
      <WaitingList
        order="given"
        items={deals.map((d) => ({
          key: d.agreement_id,
          title: `${d.agreement_id} — ${d.counterparty}`,
          sub: `${d.status}`
             + (d.sector ? ` · ${d.sector}` : '')
             + (d.value_usd ? ` · ${money(d.value_usd)}` : '')
             + (d.executed_on ? ` · signed ${d.executed_on}` : '')
             + (d.term_end ? ` · runs to ${d.term_end}` : '')
             + (d.agreement_kind && d.agreement_kind !== 'standalone'
                ? ` · ${d.agreement_kind}` : ''),
          at: null,
          chips: (
            <>
              <span className={`chip ${RESOLUTION_INK[d.resolution]}`}
                title={RESOLUTION_SAYS[d.resolution]}>
                {d.resolution}
              </span>
              {/* DETACHING IS OFFERED ONLY WHERE THERE IS A LINK TO DETACH.
                  A `matched` deal is here because its NAME reduces to this
                  supplier, and there is no row to delete — a button that
                  looked like it would undo that would be lying about what
                  the act does. The way to stop a name matching is to change
                  the supplier record or its spellings, not to press this. */}
              {mayLink && d.resolution === 'linked' && (
                <ActButton onClick={() => acts.run(`unlink-${d.agreement_id}`,
                  async () => {
                    await API.unlinkSupplier({ agreement_id: d.agreement_id });
                    onDone();
                  })}>detach</ActButton>
              )}
            </>
          ),
        }))}
        empty={<Empty kicker="their paper"
          line="No deal is attached to this supplier yet."
          sub="Deals attach either by their typed name matching, or by somebody resolving them by hand." />} />

      <section>
        <PanelHead
          title="Names this supplier answers to"
          sub="The spellings that mean this company. Its own name needs no entry here." />
        {aliases.length === 0 ? (
          <Empty kicker="other spellings"
            line="None recorded — only this supplier’s own name matches." />
        ) : (
          <WaitingList
            order="given"
            items={aliases.map((a) => ({
              key: a.alias_key,
              title: a.alias,
              sub: `added by ${a.added_by} · compares as “${a.alias_key}”`,
              at: null,
              chips: mayCurate ? (
                <ActButton onClick={() => acts.run(`drop-${a.alias_key}`,
                  async () => {
                    await API.removeSupplierAlias({ alias_key: a.alias_key });
                    onDone();
                  })}>remove</ActButton>
              ) : null,
            }))}
            empty={null} />
        )}
        {mayCurate && (
          <NewAlias supplier={supplier} acts={acts} onDone={onDone} />
        )}
      </section>

      <TheirOtherPaper
        supplier={supplier} deals={deals} acts={acts}
        mayBuild={mayBuild} onDone={onDone} />
    </div>
  );
}

// ── What else we hold with this company, and how much of it we read ───────
//
// THE QUESTION STEP 1 MADE ASKABLE (0114). A supplier record defines the set
// "our contracts with this company"; this is what is IN it — which paragraphs
// of the OTHER contracts say close to the same thing as a paragraph on this
// one.
//
// IT WARNS. IT NEVER GATES. Mike's decision, and it is absolute: nothing on
// this panel refuses anybody anything, no control here is disabled because of
// a finding, and a screen that refused to proceed because of one of these
// would be the defect rather than the protection. Legal owns the decision;
// this owns nothing.
//
// THE COUNT IS THE POINT, NOT THE LIST. `looked_at` is drawn FIRST and
// loudest, before any finding, because "no other contract says anything like
// this" and "we did not read four of their nine contracts" are the same
// screen otherwise — and the second is the sentence that gets somebody sued.
// It is drawn even when nothing is indexed at all, which is the case a panel
// that only appeared once there was something to show would hide completely.
function TheirOtherPaper({ supplier, deals, acts, mayBuild, onDone }) {
  // EVERY HOOK BEFORE ANY RETURN. One hook after an early return blanks every
  // component in the file, and it shows on the render after the data lands —
  // the one nobody watches. `hook-order.test.mjs` checks this file.
  const plan = usePane(() => API.crossContractPlan(supplier.supplier_id),
                       [supplier.supplier_id]);
  const [openDeal, setOpenDeal] = useState('');
  const [refused, setRefused] = useState(null);

  const built = plan.body;

  // GATED ON "NOTHING TO SHOW YET", NEVER ON "LOADING". A panel that unmounts
  // while it refetches takes the confirmation of the act somebody just ran
  // down with it — the act works, the chain records it, and the person is
  // shown nothing (2026-08-24).
  if (plan.status === 'loading' && !built) return <Loading />;
  if (plan.status === 'failed') return <LoadFailed reason={plan.reason} />;
  if (!built) return null;

  const seen = built.looked_at || {};

  const build = () => acts.run(`index-${supplier.supplier_id}`, async () => {
    setRefused(null);
    const r = await API.buildCrossContractIndex(
      { supplier_id: supplier.supplier_id });
    // THE DOORWAY'S OWN SENTENCE, VERBATIM. With the feature switched off the
    // database refuses in words that name the setting and who turns it on;
    // rephrasing it here would send somebody to ask for a grant that is not
    // the problem.
    if (!r.ok) { setRefused(r.reason); return; }
    plan.reload();
    onDone();
  });

  return (
    <section>
      <PanelHead
        title="Their other paper"
        sub="Which paragraphs of this supplier’s other contracts say close to the same thing — and how much of their paper that was worked out from." />

      <HowMuchWasRead seen={seen} />

      <TileStrip tiles={[
        { label: 'paragraphs', n: built.paragraphs },
        // DRAWN EVEN AT NOUGHT, unlike most figures here: nought paragraphs
        // waiting is the fact somebody most wants confirmed before trusting
        // anything below it.
        { label: 'not yet read', n: built.paragraphs_to_read },
        // The vendor half (0121), drawn at nought for the same reason.
        ...(built.vendor ? [{
          label: 'their markup not yet read',
          n: built.vendor.paragraphs_to_read }] : []),
        ...(built.allowance ? [{
          label: 'allowance left today',
          n: built.allowance.paragraphs_left }] : []),
      ]} />

      {built.vendor && !built.vendor.enabled && (
        <p className="note">
          Reading the counterparty’s own markup is switched off for this
          company, separately: their {built.vendor.paragraphs} paragraph
          {built.vendor.paragraphs === 1 ? '' : 's'} of proposed changes
          {built.vendor.paragraphs === 1 ? ' is' : ' are'} not read and not
          sent anywhere. An Administrator turns it on with the
          <code> cross_contract_vendor_paper </code> setting.
        </p>
      )}

      {!built.enabled && (
        <p className="note">
          Searching contract text by meaning is switched off for this company.
          Nothing has been read and nothing has been sent to a model provider —
          so what is below is what the tags Legal attached to each clause say,
          and nothing else. An Administrator turns it on with the
          <code> cross_contract_search </code> setting.
        </p>
      )}

      {mayBuild && (
        <div className="form-block">
          {/* THE COUNT BEFORE THE ACT, WHICH IS THE WHOLE SHAPE OF THIS
              BUTTON. The paragraph count is on the screen before anybody
              presses it, and pressing it is a named act recorded on the
              chain — never a trickle that happens by drift. */}
          <p className="note">
            This reads {built.paragraphs_to_read} paragraph
            {built.paragraphs_to_read === 1 ? '' : 's'} of this supplier’s
            contracts
            {built.vendor && built.vendor.enabled
              ? ` and ${built.vendor.paragraphs_to_read} paragraph${built.vendor.paragraphs_to_read === 1 ? '' : 's'} of their own markup`
              : ''} out to the model provider
            {built.allowance
              ? `, at most ${built.allowance.paragraphs_per_build} in one run`
              : ''}. It is recorded, and it can be run again — a paragraph
            already read at its current wording is skipped.
          </p>
          {refused && <Refused what="read this supplier’s paper" reason={refused} />}
          <ActButton onClick={build}
            disabled={!built.paragraphs_to_read
              && !(built.vendor && built.vendor.enabled
                   && built.vendor.paragraphs_to_read)}>
            read {built.paragraphs_to_read
                  + (built.vendor && built.vendor.enabled
                     ? built.vendor.paragraphs_to_read : 0)} paragraph
            {built.paragraphs_to_read
              + (built.vendor && built.vendor.enabled
                 ? built.vendor.paragraphs_to_read : 0) === 1 ? '' : 's'}
          </ActButton>
        </div>
      )}

      <label>See what else touches one of these deals
        <select value={openDeal} data-testid="echo-deal"
          onChange={(e) => setOpenDeal(e.target.value)}>
          <option value="">choose a deal…</option>
          {deals.map((d) => (
            <option key={d.agreement_id} value={d.agreement_id}>
              {d.agreement_id} — {d.counterparty}
            </option>
          ))}
        </select>
      </label>

      {openDeal && <CrossContractEchoes agreementId={openDeal} />}
    </section>
  );
}

// ── Creating one ──────────────────────────────────────────────────────────
function NewSupplier({ acts, onDone }) {
  const [name, setName] = useState('');
  const [reference, setReference] = useState('');
  const [note, setNote] = useState('');
  const [refused, setRefused] = useState(null);

  const save = () => acts.run('new-supplier', async () => {
    setRefused(null);
    const r = await API.createSupplier({
      name, reference: reference || null, note: note || null });
    // THE DATABASE'S OWN SENTENCE, shown verbatim. The unique key on the
    // comparison form is what refuses a second "Acme Inc." beside "Acme, Inc.",
    // and a rephrasing here would teach a rule the database does not have.
    if (!r.ok) { setRefused(r.reason); return; }
    setName(''); setReference(''); setNote('');
    onDone();
  });

  return (
    <section className="form-block">
      <PanelHead title="New supplier"
        sub="A record two spellings of one company can both point at." />
      <label>Name
        <input value={name} onChange={(e) => setName(e.target.value)}
          placeholder="Acme, Inc." data-testid="supplier-name" />
      </label>
      <label>Reference <span className="hint">optional — your own vendor code</span>
        <input value={reference} onChange={(e) => setReference(e.target.value)} />
      </label>
      <label>Note <span className="hint">optional</span>
        <input value={note} onChange={(e) => setNote(e.target.value)} />
      </label>
      {refused && <Refused what="create this supplier" reason={refused} />}
      <ActButton onClick={save} disabled={!name.trim()}>create supplier</ActButton>
    </section>
  );
}

function NewAlias({ supplier, acts, onDone }) {
  const [alias, setAlias] = useState('');
  const [refused, setRefused] = useState(null);

  const save = () => acts.run('new-alias', async () => {
    setRefused(null);
    const r = await API.addSupplierAlias({
      supplier_id: supplier.supplier_id, alias });
    if (!r.ok) { setRefused(r.reason); return; }
    setAlias('');
    onDone();
  });

  return (
    <div className="form-block">
      <label>Another spelling
        <input value={alias} onChange={(e) => setAlias(e.target.value)}
          placeholder="Acme Widgets Division" data-testid="supplier-alias" />
      </label>
      {refused && <Refused what="record this spelling" reason={refused} />}
      <ActButton onClick={save} disabled={!alias.trim()}>record spelling</ActButton>
    </div>
  );
}

// ── Attaching a deal that matched nothing ─────────────────────────────────
// THE ACT THE WORKLIST WAS DESCRIBING AND COULD NOT PERFORM.
// `a-built-thing-has-a-way-in.test.mjs` named `linkSupplier` and
// `unlinkSupplier` as built and reachable from no screen — the defect this
// repository finds most often, caught here before the branch left the ground.
//
// WHY A DEAL AT A TIME, and not "attach all four of these". The worklist
// groups by comparison key, so the four rows under one key are four deals
// whose counterparty names reduce to the same thing — which is EVIDENCE that
// they are one company, never proof. Two genuinely different companies can
// reduce to the same key, and a blanket button would file both under one
// supplier with one press and no reading. The deal is what somebody can
// actually check.
function AttachDeals({ acts, suppliers, deals, onDone }) {
  const [supplierId, setSupplierId] = useState('');
  const [agreementId, setAgreementId] = useState('');
  const [refused, setRefused] = useState(null);

  const attach = () => acts.run('attach', async () => {
    setRefused(null);
    const r = await API.linkSupplier({
      agreement_id: agreementId, supplier_id: Number(supplierId) });
    if (!r.ok) { setRefused(r.reason); return; }
    setAgreementId('');
    onDone();
  });

  return (
    <div className="form-block">
      <PanelHead title="Attach a deal to a supplier"
        sub="For a deal whose counterparty name matches no record — a rename, a division, a company that was bought." />
      <label>Deal
        <select value={agreementId} data-testid="attach-deal"
          onChange={(e) => setAgreementId(e.target.value)}>
          <option value="">choose a deal…</option>
          {deals.map((d) => (
            <option key={d.agreement_id} value={d.agreement_id}>
              {d.agreement_id} — {d.counterparty}
            </option>
          ))}
        </select>
      </label>
      <label>Supplier
        <select value={supplierId} data-testid="attach-supplier"
          onChange={(e) => setSupplierId(e.target.value)}>
          <option value="">choose a supplier…</option>
          {suppliers.map((s) => (
            <option key={s.supplier_id} value={s.supplier_id}>{s.name}</option>
          ))}
        </select>
      </label>
      {refused && <Refused what="attach this deal" reason={refused} />}
      <ActButton onClick={attach} disabled={!agreementId || !supplierId}>
        attach deal
      </ActButton>
    </div>
  );
}
