// The six workspaces.
//
// WP-U07 builds the SHELL: every workspace opens on what is waiting, the
// requester's deal list works with the pipeline rail as an open deal's header,
// and every remaining pane renders an honest empty state naming the package it
// lands in. The detailed panes arrive in WP-U08 through WP-U14.
//
// The rule for this file: a pane either reads a real endpoint or says it is not
// built. There is no third option, and in particular there are no example rows.
// The v4 concept mockup has a complete set of invented data and none of it is
// imported here.

const { useState } = React;

// ── The pipeline rail, per deal ──────────────────────────────────────────
// v3 drew this as one global state, so two deals at two stages shared a rail and
// the screen could only be telling the truth about one of them. Under decision
// U8 it is the header of an OPEN DEAL and takes its stage from that deal.
const STAGES = ['Intake', 'Manifest', 'Forge', 'Validate', 'Dossier'];

// The three statuses the record allows. `cw.agreement` carries
//   check (status in ('negotiating','executed','terminated'))
// and this list is the screen's copy of it — kept here so the reachable-stage
// set below can be DERIVED rather than remembered.
const AGREEMENT_STATUSES = ['negotiating', 'executed', 'terminated'];

// Which stage a deal has reached, derived from its recorded status rather than
// held anywhere. A stored stage is a fact that starts drifting the moment
// anything else changes.
//
// THREE STATUSES, THREE ANSWERS, and the third is the one this got wrong until
// 2026-08-21. Until then anything that was not `executed` fell through to
// Manifest, so a TERMINATED deal — dead, closed, nobody will move it again —
// was drawn standing at an early stage of the pipeline, which reads as work
// somebody still owes. Three of the portfolio's forty-eight were sitting there.
//
// `null` is the honest third answer: a terminated deal is not at Manifest, and
// it is not past the end either. Past-the-end draws every stage done, which
// would say it completed a pipeline it never finished. It LEFT the rail.
function stageOf(deal) {
  if (deal.status === 'executed') return STAGES.length;      // past the end
  if (deal.status === 'terminated') return null;             // left the rail
  if (deal.status === 'negotiating') return 3;               // Validate
  // AND NOTHING ELSE GETS A STAGE. This used to fall through to Manifest,
  // which is how the terminated deals ended up standing there. A status this
  // screen does not recognise is a status it cannot place, and placing it
  // anyway — at the first stage, where it reads as "just started" — is the
  // same untruth waiting for the next value somebody adds to the record.
  return null;                                               // not placeable
}

// A deal is LIVE only while it is still moving. Both other outcomes are closed
// — one successfully, one not — and neither belongs in a count of open work.
// Asked through stageOf so there is ONE rule about where a deal stands; a
// second `status !== 'executed'` anywhere else is how the six sites this
// replaced came to disagree with each other.
function isLive(deal) {
  const at = stageOf(deal);
  return at !== null && at < STAGES.length;
}

// Which rail positions the derivation can actually produce.
//
// DERIVED, NOT LISTED. stageOf is asked about every status the record allows,
// so a status added to the vocabulary — or a new branch in stageOf — changes
// this set on its own, rather than leaving a card printing a number nobody
// measured until somebody remembers to come back here.
//
// TODAY THE ANSWER IS ONE STAGE: {Validate}. That is not a bug in this line,
// it is what the derivation is worth. `cw.agreement.status` is three values
// wide, and Intake, Forge and Dossier are facts about runs, manifests and
// dossiers — other tables, which this screen does not read. So four of the
// five cards can never hold a deal, and until 2026-08-21 all four printed a
// confident `0`.
//
// `shell.jsx` already states the rule, for the navigation rack: "a rack
// showing 0 for an area it has not measured would be stating a fact it does
// not hold, so an unmeasured area shows nothing at all". Kept at the rack,
// broken at the strip.
const REACHABLE_STAGES = new Set(
  AGREEMENT_STATUSES
    .map((status) => stageOf({ status }))
    .filter((at) => at !== null && at < STAGES.length)
);

function PipelineRail({ deal }) {
  const at = stageOf(deal);
  // A deal that is not on the rail is drawn struck rather than complete, and
  // SAYS SO IN A WORD — colour alone would be the design set's first broken
  // rule. Two ways to be off it, and they are not the same news: terminated is
  // an outcome the record knows, an unrecognised status is this screen's own
  // ignorance, and each says which it is.
  const left = at === null;
  const ended = deal.status === 'terminated';
  return (
    <div className={`pipe${left ? ' pipe-left' : ''}`} data-testid="pipeline-rail"
         data-deal={deal.agreement_id} data-stage={left ? 'off-the-rail' : at}>
      {STAGES.map((s, i) => (
        <React.Fragment key={s}>
          {i > 0 && <div className="pipe-sep" />}
          <div className={`pipe-stage${!left && i < at ? ' done' : ''}${!left && i === at ? ' here' : ''}`}>
            <span className="pipe-dot" />
            {s}
          </div>
        </React.Fragment>
      ))}
      {left && (ended
        ? <span className="chip chip-gone" style={{ marginLeft: 10 }}>terminated</span>
        : <span className="chip chip-unknown" style={{ marginLeft: 10 }}>
            no stage for “{deal.status}”
          </span>)}
    </div>
  );
}

// ── The router ───────────────────────────────────────────────────────────
// Every entry is either a real pane or an honest note about where it lands.
// Nothing here renders invented rows.
const PANES = {
  // EVERY ROLE, AND THE ONE EVERY ROLE OPENS ON. One pane, six workspaces —
  // and unlike `notices` or `sourcing` there is not even a scoping question
  // to answer, because it holds no read of its own: it draws the waiting
  // derivation the digest already uses, the tab rail's own counts, and one
  // composition taken from a read this role holds a tab onto. A role sees
  // its own desk because its own tab set and its own grants are what the
  // page is made of.
  'home':      (me) => <HomePane me={me} />,

  // Requester
  'my-deals':  (me) => <MyDealsPane me={me} />,
  'intake':    (me) => <IntakePane me={me} />,
  'negotiate': (me) => <NegotiatePane me={me} />,
  // The deal room (0079): one pane, three roles. The scoping is the deal-room
  // tables' own policies — a requester is answered the rooms of deals they
  // own and Legal every room, from the same render.
  'deal-room': (me) => <DealRoomPane me={me} />,
  'vendors':   () => <FrictionPane />,
  // WHO WE HOLD PAPER WITH (0111). ONE PANE, FIVE ROLES, the deal room's
  // reason: the scoping is cw.supplier's own read policy for the record and
  // cw.agreement's read_own policy for the deal counts beside it, so a
  // requester's figures count their own deals and Legal's count every one,
  // from the same render. It takes the identity because the CURATING controls
  // are drawn only for the role holding 0111's insert — an affordance, not a
  // permission, and the database refuses either way.
  'suppliers': (me) => <SuppliersPane me={me} />,
  'my-record': (me) => <MyRecordPane me={me} />,
  // OB-11/OB-15: one pane, whoever holds the tab — the scoping is the
  // obligation table's own policy, so a requester sees their book and Legal
  // admin the whole one, from the same render.
  // Takes the identity now: a waiting row that names a deal opens it, and only
  // where this role holds the pane it would open. Affordances, not permissions
  // — the database refuses regardless, and a link landing on a refusal is
  // worse than no link.
  'obligations': (me) => <ObligationsPane me={me} />,

  // Sourcing (SRC-1 through SRC-4). ONE PANE, FIVE ROLES, the deal room's
  // reason: the scoping is cw.sourcing_run's own read policy — a requester is
  // answered the documents they built and the ones on deals they own, Legal
  // and Audit every document, from the same render. The uncompeted register
  // inside it renders for the roles that hold its grant and DRAWS NOTHING for
  // the ones that do not, which is not the same as drawing an empty one.
  'sourcing': (me) => <SourcingPane me={me} />,

  // The competition (0123-0125, SRC-5). ONE PANE, FOUR ROLES, and the same
  // reason as sourcing above: the scoping is cw.sourcing_event's own read
  // policy, so a requester is answered the competitions they opened and the
  // ones on deals they own, while procurement and Legal see every one — from
  // one render. The acts inside it are drawn only for the roles that hold
  // them, which is not a permission decision this pane makes: it is the same
  // list 0124 and 0125 name in their policies.
  'competitions': (me) => <SourcingEventPane me={me} />,

  // Legal reviewer
  'review-desk':  (me) => <ReviewDeskPane me={me} />,
  'tickets':      (me) => <TicketsPane me={me} />,
  'approvals':    (me) => <OverridesPane me={me} />,
  // The Legal reviewer's and (by owner decision NI-1) the Legal admin's, from
  // one pane. The scoping is the negotiation family's own read policies — both
  // roles see every deal — so there is nothing here for a second copy to get
  // subtly different.
  'negotiations': (me) => <NegotiationsDeskPane me={me} />,
  'routing':      (me) => <RoutePane me={me} />,
  // The expert panel (0090, ADR-0013). ONE PANE, THREE ROLES, the deal
  // room's reason: the scoping is cw.consultation's own read policy, so
  // a seat holder is answered what their seat admits and Legal the whole
  // queue, from the same render. What differs is the affordances.
  'consultations': (me) => <PanelDeskPane me={me} />,
  'holds':        (me) => <HoldsPane me={me} />,

  // The Library Builder (ADR-0010, 0102). ONE PANE, THREE ROLES — the deal
  // room's reason, and here the grant is exactly the audience:
  // cw.library_draft_register is granted to legal_reviewer, legal_admin and
  // the auditor. It takes the identity because the DRAFTING form is drawn only
  // for the two roles holding the insert on cw.clause_draft; the auditor reads
  // what was drafted and asks for nothing. An affordance, not a permission —
  // the database refuses either way.
  'builder':    (me) => <LibraryBuilderPane me={me} />,

  // Legal admin
  'library':    () => <LibraryPane />,
  'ladders':    () => <LaddersPane />,
  'governance': () => <GovernancePane />,
  'retention':  () => <RetentionPane />,

  // Legal admin and auditor share this one; the grant behind its endpoints is
  // the control, and a refusal renders as the database's sentence.
  // TAKES THE IDENTITY since 2026-08-24, for the same reason the auditor's
  // pane does: every report on it can now be exported, and an export carries
  // WHO took it — `me.person`, the account the doorway bound the request to.
  'reporting':  (me) => <ReportingPane me={me} />,

  // NC-16 (0049), reached 2026-08-24. TAKES THE IDENTITY, and for one reason
  // only: `cw.portfolio_run` fences a requester to runs they created or deals
  // they own and answers everybody else over every run. The pane says which
  // of the two it is showing, because "12 agreements carry this clause" read
  // as a company total when it is one person's own deals is exactly the wrong
  // conclusion to draw from a recall. It is a SENTENCE, not a permission —
  // the view's own WHERE clause decides, whatever the screen says.
  'portfolio':  (me) => <PortfolioPane me={me} />,

  // AI-7 (0105). Held by everyone cw.model_call's read policy admits, which is
  // every role but the viewer — 0066 withheld that one deliberately, because a
  // viewer is outside the process entirely and asks nothing of anybody. The
  // VIEWS do the scoping, so the same component serves a requester reading
  // their own calls and an auditor reading all of them.
  'ai-use':     (me) => <AiUsePane me={me} />,

  // Auditor
  // Takes the identity now: an export carries WHO took it and WHEN, inside
  // the file, because a spreadsheet that has left the building has to answer
  // that on its own (Mike, 2026-08-22).
  'the-record':     (me) => <TheRecordPane me={me} />,
  'quality':        () => <QualityPane />,
  'origin-mix':     () => <OriginMixPane />,
  'access-history': (me) => <AccessHistoryPane me={me} />,

  // DEPARTURES FROM THE MASTER (0006/0012/0019). ONE PANE, FIVE ROLES, the
  // reading room's reason: the scoping is cw.sow_override's own read policy —
  // a requester is answered the statements of work they own, Legal and Audit
  // every one, a viewer what was shared with them — from the same render. The
  // Administrator holds no grant on the family at all and has no tab.
  'departures': (me) => <DeparturesPane me={me} />,

  // Viewer
  'reading-room': (me) => <ReadingRoomPane me={me} />,

  // Administrator
  'people':   (me) => <PeopleAndAccessConsole me={me} />,
  'settings': (me) => <SettingsPane me={me} />,
  // Takes the identity now: the administrator's surfaces carry raise controls
  // (NT-3) and the notices addressed to them, and both need to know who is
  // looking. Affordances, not permissions — cw.notice_route refuses regardless.
  'health':   (me) => <HealthPane me={me} />,
  'watchers': () => <WatchersPane />,
  // Offered to EVERY role, because since 0098 every role can raise a notice
  // and receive one, and somebody who cannot see what became of their own
  // message cannot tell "answered" from "ignored".
  'notices': (me) => <NoticesPane me={me} />,
  'panel':    (me) => <PanelSeatsPane me={me} />,
  'workflow': (me) => <WorkflowDesignPane me={me} />,
  'onboarding': (me) => <WorkflowOnboardingPane me={me} />,
  'contract-onboarding': (me) => <ContractOnboardingPane me={me} />,
};

// Render a pane ONLY if it belongs to this role's tab set.
//
// This is the URL half of the principle. Hiding a tab is cosmetic — anybody can
// type an address — so a route outside the role's set resolves to a refusal
// having fetched nothing at all. Note what it does NOT do: it does not call the
// endpoint and render the refusal that comes back. There is no request, so
// there is no possibility of data arriving and being hidden on screen.
function Workspace({ me, tab }) {
  const allowed = WORKSPACES[me.role].tabs.some((t) => t.key === tab);
  if (!allowed) {
    return (
      <div className="sheet-main">
        <Refused
          what={`“${tab}” is not part of your workspace.`}
          role={me.role}
        />
      </div>
    );
  }
  const pane = PANES[tab];
  if (!pane) {
    return (
      <div className="sheet-main">
        <NotBuiltYet what="This pane does not exist." />
      </div>
    );
  }

  // ── Notices, wherever you are (NT-4) ────────────────────────────────────
  // Rendered HERE, once, above whatever pane the role opened — rather than
  // added to each pane, which is how five copies of one panel start disagreeing
  // about what "open" means. It draws nothing at all when nothing is open, so
  // a quiet system stays quiet, and it never draws a zero.
  //
  // The scoping is cw.notice's own read policy: a role that HOLDS the read and
  // has had nothing raised to it is answered an empty list rather than
  // somebody else's business.
  //
  // IT NOW SAYS THAT OF EVERY ROLE, AND IT IS TRUE OF EVERY ROLE. It was false
  // of one until 0098: a viewer was REFUSED here, 403, on every workspace they
  // opened, because their grant did not include the notice view at all.
  //
  // Mike's decision of 2026-08-22 — everyone can send and receive messages —
  // ended that, so a viewer is now answered the same empty list as anybody
  // else with nothing waiting. It is worth two lines of history because the
  // paragraph this replaces had ALREADY been wrong once, in the other
  // direction, and was rewritten at length to be right. A comment about who is
  // refused is the thing the next reader trusts instead of checking, and it
  // goes stale the day somebody changes a grant.
  //
  // NoticesWaiting still names the 403 as its own case rather than folding it
  // into "any failure draws nothing" — which was hiding a REAL failure for the
  // roles that do receive notices, and which is why the branch stays even with
  // nobody expected to reach it.
  //
  // The identity is passed to panes that need to know WHO is looking — the
  // people console shows a countersign button only to a Legal admin, and no
  // revoke button against the viewer's own row. Those are affordances, not
  // permissions: the database refuses the acts regardless, and a pane that got
  // this wrong would offer a button that fails rather than leak anything.
  // THE SHEET ON THE DESK (design set, 2026-08-10). The workspace container
  // is the dark desk; whatever pane the role opened is laid on it as one
  // cream sheet. Wrapped HERE, once, so every pane — including the ones not
  // yet redrawn to the design set — arrives on paper rather than on wood.
  return (
    <div className="sheet-main">
      {tab !== 'notices' && <NoticesWaiting me={me} />}
      {pane(me)}
    </div>
  );
}
