/* ============================================================================
   cand-header.jsx — the candidate record's IDENTITY HEADER (avatar, name, stage
   and match chips, workspace pin, and the qualifying-fact line) and the two
   header action groups that ride in the Topbar: the prev/next + record-id
   navigator, and the list popover / Actions menu / stage-driven primary button.

   Split out of screens-core.jsx's CandidateDetailScreen. Loaded AFTER
   screens-core.jsx; AddToListPopover and the DS come from the shared global
   scope at render time.

   Topbar renders `after` and `actions` straight into its own markup
   (shell.jsx:2598) - it does not inspect them - so passing a component element
   where a fragment used to sit changes the React tree and not the DOM.

   Every state setter arrives as a prop under its ORIGINAL NAME so the moved
   markup is byte-identical to what it replaced. Nothing here writes to the
   store; the handlers all live in cand-record.jsx.

   No inline `style` of the kind the budget counts. The one inline object in the
   file sets three CSS CUSTOM PROPERTIES carrying the match tier (`--e8-cand-
   header-fit*`), which is record data rather than a style decision and is the
   documented exemption; every layout, size and colour declaration lives in
   app/cand-header.css.
   ============================================================================ */

/* The Topbar `after` slot: walk the queue (same go() as J/K) + the copyable record id. */
function CandHeaderNav({ c, inQueue, isMobile, go, showToast }) {
  return (
    <React.Fragment>
      {/* R124: prev/next moved into the header (deliberately reversing R111's
          review-bar-only rule); the queue decision bar keeps NO duplicate navigator.
          Outside a queue these walk the full roster - same go() as J/K. */}
      {/* `e8-cand-header-nav` carries no look of its own - it exists so cand-header.css can
          grow the HIT AREA of these two on the touch shell without a bare `.e8-rec-nav`
          override, which would reach the client/contact/consultant/job records too. Same
          anchoring rule as the pin wrapper below. */}
      <button type="button" className="e8-rec-nav e8-cand-header-nav" title={'Previous candidate' + (inQueue ? ' (K)' : '')} aria-label="Previous candidate" onClick={() => go(-1)}>
        <span className="material-symbols-outlined" aria-hidden="true">chevron_left</span>
      </button>
      <button type="button" className="e8-rec-nav e8-cand-header-nav" title={'Next candidate' + (inQueue ? ' (J)' : '')} aria-label="Next candidate" onClick={() => go(1)}>
        <span className="material-symbols-outlined" aria-hidden="true">chevron_right</span>
      </button>
      {/* Copyable record id - the REAL c.id; the model has no external candidate id. */}
      {!isMobile ? (
        <button type="button" className="e8-rec-id tnum" title="Click to copy the record id"
          onClick={() => {
            try {
              navigator.clipboard.writeText(c.id)
                .then(() => showToast('Copied ' + c.id))
                .catch(() => showToast(c.id));
            } catch (e) { showToast(c.id); }
          }}>
          {c.id}
          <span className="material-symbols-outlined" aria-hidden="true">content_copy</span>
        </button>
      ) : null}
    </React.Fragment>
  );
}

/* The Topbar `actions` slot: add-to-list popover, the Actions menu, and the ONE primary
   button the current stage allows. */
function CandHeaderActions({
  c, D, listMeta, convo, matchJob, linkedInUrl, canReassign, placement, placementCid,
  listOpen, setListOpen, cap, showToast, setPresent, setPsOpen, setFilesMode,
  setReassignOpen, setDebriefFor, openSubmit,
}) {
  return (
    <React.Fragment>
      {/* Membership lists: playlist_add opens the checkbox popover (mockup). Also in Actions. */}
      <span className="e8-clist-wrap">
        <DSc.Button
          className="e8-cand-header-act"
          variant="ghost" size="sm" icon="playlist_add" iconOnly
          title="Add to list" aria-label="Add to list" aria-expanded={listOpen}
          onClick={() => setListOpen((o) => !o)}
        />
        <AddToListPopover candIds={[c.id]} open={listOpen} onClose={() => setListOpen(false)} showToast={showToast} />
      </span>
      {/* R124: exactly two workflow actions - the stage-dependent primary + Actions.
          Everything else (note, present, message, LinkedIn, CV toolkit) lives in the
          Actions menu with its existing handlers. */}
      {/* The wrapper is the only hook this module has on the Actions trigger: `DSc.MenuButton`
          (components/ats-menu/Menu.jsx:80) takes no `className` and renders its own
          `.e8-menubtn-wrap` span, which every menu in the app shares - so styling that bare
          would resize 100+ triggers. `display: inline-flex` in the CSS keeps it the same flex
          item `.e8-topbar-actions` had before. */}
      <span className="e8-cand-header-actwrap">
        <DSc.MenuButton
          label="Actions"
          icon="bolt"
          size="sm"
          variant="secondary"
          items={[
            { icon: 'note_add', label: 'Add note', onClick: () => cap.open({ target: 'note', entityLabel: c.name, refType: 'candidate', refId: c.id }) },
            { icon: 'slideshow', label: 'Present to contact', onClick: () => setPresent(true) },
            { separator: true },
            { icon: 'support_agent', label: 'Prescreen call', onClick: () => setPsOpen(true) },
            { icon: 'call', label: 'Call', onClick: () => navigate(convo ? 'inbox/' + convo.id : 'voice') },
            { icon: 'mail', label: 'Message', onClick: () => navigate(convo ? 'inbox/' + convo.id : 'inbox') },
            { icon: 'campaign', label: 'Add to sequence', onClick: () => showToast(c.name + ' enrolled in "Memphis JVM re-engage"', 'Open', () => navigate('sequences')) },
            { icon: 'bookmark_add', label: 'Add to list', onClick: () => setListOpen(true) },
            { separator: true },
            { icon: 'auto_awesome', label: 'Generate tailored CV', onClick: () => showToast('Tailored CV for ' + (matchJob ? matchJob.title : 'the best-fit role') + ' - live CV drafting is a prototype stub') },
            { icon: 'upload_file', label: 'Update with new CV', onClick: () => setFilesMode('add') },
            { icon: 'outgoing_mail', label: 'Request updated CV', onClick: () => showToast('CV request drafted to ' + c.name + ' - review in Approvals', 'Open approvals', () => navigate('approvals')) },
            { icon: 'sync', label: 'Check for profile updates', onClick: () => showToast('Checking LinkedIn for profile changes…') },
            ...(linkedInUrl ? [{ icon: 'open_in_new', label: 'View LinkedIn profile', onClick: () => window.open(linkedInUrl, '_blank', 'noreferrer') }] : []),
            { separator: true },
            { icon: 'person_add', label: 'Add as contact', onClick: () => showToast('Added ' + c.name + ' as a contact') },
            /* Same permission-aware action the header chip opens, repeated here for people who
               look for record actions first. */
            ...(canReassign ? [{ icon: 'swap_horiz', label: 'Change owner', onClick: () => setReassignOpen(true) }] : []),
            { icon: 'archive', label: 'Archive candidate', danger: true, onClick: () => {
              const prev = listMeta.status || 'Sourced';
              if (prev === 'Archived') { showToast(c.name + ' is already archived'); return; }
              window.E8Store.set('candidates', c.id, { status: 'Archived' });
              showToast(c.name + ' archived', 'Undo', () => {
                window.E8Store.set('candidates', c.id, { status: prev });
              });
            } },
          ]}
        />
      </span>
      {/* R111 B1: state through affordance - the stage governs which action is primary,
          so the button can never offer what the stage forbids. */}
      {(() => {
        if (placement && placementCid) {
          return <DSc.Button className="e8-cand-header-act" variant="primary" size="sm" icon="badge" onClick={() => navigate('consultant/' + placementCid)}>View engagement</DSc.Button>;
        }
        const iInterview = (D.submissionStages || []).indexOf('Interview');
        const iOffer = (D.submissionStages || []).indexOf('Offer');
        const ivSub = (D.submissions || []).find((s) => s.candId === c.id && s.open !== false && (s.stage | 0) === iInterview);
        if (ivSub) {
          const roundNo = (ivSub.interview && ivSub.interview.round) || 1;
          const current = window.e8SubDebriefFor ? window.e8SubDebriefFor(ivSub, roundNo)
            : (ivSub.debrief && (ivSub.debrief.round || 1) === roundNo ? ivSub.debrief : null);
          return current
            ? <DSc.Button className="e8-cand-header-act" variant="primary" size="sm" icon="work" onClick={() => navigate('job/' + ivSub.jobId + '/submissions')}>View interview</DSc.Button>
            : <DSc.Button className="e8-cand-header-act" variant="primary" size="sm" icon="record_voice_over" onClick={() => setDebriefFor({ sub: ivSub, round: roundNo })}>Log debrief</DSc.Button>;
        }
        const offerSub = (D.submissions || []).find((s) => s.candId === c.id && s.open !== false && (s.stage | 0) === iOffer);
        if (offerSub) return <DSc.Button className="e8-cand-header-act" variant="primary" size="sm" icon="handshake" onClick={() => navigate('submissions/offers')}>View offer</DSc.Button>;
        return <DSc.Button className="e8-cand-header-act" variant="primary" size="sm" icon="send" onClick={openSubmit}>Submit to job</DSc.Button>;
      })()}
    </React.Fragment>
  );
}

/* Initials for the identity avatar. The DS owns this logic (components/ats-avatar/Avatar.jsx
   exports initialsOf); we call through DSc when it is there and degrade to the first letter
   rather than crashing the whole record if the bundle ever moves. The DS `Avatar` itself is NOT
   used for the big mark: it writes width/height/font-size INLINE, which no class rule can beat,
   and this header has to size the avatar against the CONTAINER (40 narrow / 48 wide). The 20px
   owner avatar that used to justify keeping DSc.Avatar around left with the owner chip. */
function candHeaderInitials(name) {
  const s = (name || '').trim();
  if (!s) return '?';
  if (DSc && DSc.initialsOf) return (DSc.initialsOf(s) || s[0]).toUpperCase();
  const parts = s.split(/\s+/);
  return ((parts[0][0] || '') + (parts.length > 1 ? (parts[parts.length - 1][0] || '') : '')).toUpperCase();
}

/* The score, as a control rather than an ornament.
   PIECE 2. A bare number is not an answer - "94" only means something with the reasons behind
   it, and the reasons already exist twice on this record (the Overview "Why we matched" panel,
   and the rail). Neither is on screen when the header is. So the chip IS the door: click it and
   the weighted reasons open against it, with a link down to the full panel when the Overview tab
   is the one mounted (it is not when you are on Experience/Skills/Messages, which is exactly
   when a header-level explanation earns its keep).

   DSc.WhyPopover is the DS's explainability surface - portal, Escape, scroll/resize dismiss and
   viewport clamping all already correct - so this does not hand-roll a popover.

   The band label ("Very strong match") is TEXT, not just a tint: the tier colour is decoration
   on top of a word, so the band survives greyscale, dark mode and colour blindness. */
function CandHeaderScore({ dm, dmTier, hasMatch }) {
  const [anchor, setAnchor] = React.useState(null);
  const [hasFull, setHasFull] = React.useState(false);
  const btnRef = React.useRef(null);
  const close = React.useCallback(() => setAnchor(null), []);
  React.useEffect(() => {
    if (!anchor) return;
    const outside = (ev) => {
      if (btnRef.current && btnRef.current.contains(ev.target)) return;
      if (ev.target.closest && ev.target.closest('.e8-whypop')) return;
      setAnchor(null);
    };
    document.addEventListener('mousedown', outside);
    return () => document.removeEventListener('mousedown', outside);
  }, [anchor]);

  /* Never scored: say so plainly. An empty score chip would read as a zero. */
  if (!hasMatch) {
    return <span className="e8-nomatch-chip" title="No match run yet - score them from the Matching card below">Not matched yet</span>;
  }

  const tier = dm.tier || 3;
  const band = dmTier.label || 'match';
  const scored = dm.score != null && dm.score !== '';
  /* e8TopMatch yields either {label, weight} rows or, for a synthesized best-match, bare
     strings. WhyPopover prints `+weight`, so an undefined weight would render "+NaN". */
  const reasons = (dm.reasons || [])
    .filter(Boolean)
    .map((r) => (typeof r === 'string' ? { label: r, weight: 2 } : { label: r.label, weight: r.weight == null ? 2 : r.weight }));
  const jobLabel = dm.job ? ((dm.job.id ? dm.job.id + ' · ' : '') + (dm.job.title || '')) : null;

  const toggle = (ev) => {
    if (anchor) { setAnchor(null); return; }
    /* Resolved at OPEN time, not at render: the Overview tab mounts and unmounts under us. */
    setHasFull(!!document.querySelector('.e8-rfr'));
    setAnchor(ev.currentTarget.getBoundingClientRect());
  };

  return (
    <React.Fragment>
      <button
        type="button"
        ref={btnRef}
        className="e8-cand-header-score"
        /* NO `aria-haspopup`. It said "dialog" and that was a promise the DOM does not keep:
           `DSc.WhyPopover` (components/ats-badge/Badge.jsx:110) renders a bare
           `<div class="e8-whypop">` - no `role`, no accessible name - and it neither moves
           focus into itself nor traps it, so it is a DISCLOSURE, not a dialog. A screen reader
           was told a dialog would open and then never met one. `aria-expanded` alone is the
           honest description of what this button does. The better fix is one level down and
           NOT in this module: `role="dialog"` + `aria-labelledby` on `.e8-whypop` (and an id on
           its title row) would let this button say `aria-haspopup="dialog"` truthfully and let
           `aria-controls` point at it - that is a DS change, and it needs an `app/ds-live.js`
           `?v=` bump to ship. Reported rather than worked around here. */
        aria-expanded={!!anchor}
        aria-label={(scored ? 'Match score ' + dm.score + ', ' : '') + band + (dm.job ? ' for ' + dm.job.title : '') + ' - why?'}
        title={dm.job ? 'Fit vs ' + dm.job.title + ' - open the reasons' : 'Match score - open the reasons'}
        style={{
          '--e8-cand-header-fit': 'var(--ui-tier-' + tier + ')',
          '--e8-cand-header-fitbg': 'var(--ui-tier-' + tier + '-tint)',
          '--e8-cand-header-fitbr': 'var(--ui-tier-' + tier + '-br)',
        }}
        onClick={toggle}
      >
        {scored ? <span className="e8-cand-header-scoren tnum">{dm.score}</span> : null}
        <span className="e8-cand-header-scoreband">{band}</span>
        <span className="material-symbols-outlined e8-cand-header-scoreic" aria-hidden="true">{anchor ? 'expand_less' : 'expand_more'}</span>
      </button>
      {anchor ? (
        <DSc.WhyPopover
          anchor={anchor}
          onClose={close}
          title={scored ? 'Why ' + dm.score + ' · ' + band : 'Why ' + band}
          reasons={reasons.length ? reasons : [{ label: 'Ranked by the matching engine - no per-criterion detail on this record yet.', weight: 2 }]}
          footnote={
            <span className="e8-cand-header-whyfoot">
              {/* The rows are WEIGHTS, and they never summed to the headline. WhyPopover prints
                  each reason as `+3 / -1`, which reads as a signed ledger, so "Why 94" over
                  +3 +2 +2 +1 -1 invited the reader to add up 7 and find it 87 short. There is no
                  baseline to name: `top.score` comes from the ranking row (E8Match.candidateJobs)
                  and the weights come from e8ReasonWeight (screens-core.jsx:2686), which grades a
                  reason 3/2/1 by TIER and -1 for a caveat-flavoured line. They are two different
                  quantities, not a total and its parts, so this says which is which rather than
                  inventing a "Base 87" the engine never computed. */}
              <span className="e8-cand-header-whynote">
                {scored
                  ? 'Signal weights, not points - ' + dm.score + ' is the overall fit from the matching engine.'
                  : 'Signal weights - what moved this match, strongest first.'}
              </span>
              <span className="e8-cand-header-whyrow">
                {jobLabel ? <span className="e8-cand-header-whyjob">{jobLabel}</span> : null}
                {hasFull ? (
                  <button
                    type="button"
                    className="e8-cand-header-whylink"
                    onClick={() => {
                      const el = document.querySelector('.e8-rfr');
                      setAnchor(null);
                      /* No `behavior: 'smooth'`. Two reasons, one of them measured: smooth
                         scrolling is silently a no-op in the automation this record is verified
                         in (scrollTop never moved; the same call with the default behaviour
                         landed the panel at y=123), so the control would have shipped
                         unverifiable. And an instant jump needs no prefers-reduced-motion
                         guard, because there is no motion to reduce. */
                      if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' });
                    }}
                  >
                    See the full breakdown
                  </button>
                ) : null}
              </span>
            </span>
          }
        />
      ) : null}
    </React.Fragment>
  );
}

/* The in-page identity header.
   PIECE 1. One block: avatar, name, the chips that state where this person stands, and the two
   or three facts that qualify them. It was four fragments at 390 - a 56px avatar mid-left with
   the name ABOVE it, the score chip between them and the owner floating off the avatar's right -
   because a single flex row was wrapping with no plan and `.e8-owner-id`'s `margin-left:auto`
   kept shoving the owner to the far edge of whatever line it landed on.

   The fix is structural, not a media query: a 2-column grid (mark | everything else) whose
   second column is a real column. The mark can never end up beside the wrong line, and the
   wrapping now happens INSIDE the chip row and INSIDE the fact row, where wrapping is fine.

   OWNERSHIP IS NOT IN THIS HEADER ANY MORE. §2.2 is explicit - "owner/recruiter is metadata, not
   a headline: it belongs in the rail" - and the chip was costing more than metadata should: a
   161x26 control (below the 44px touch minimum, and the only sub-44 thing left here besides the
   pin) that forced the fact line onto a THIRD row at 390. It is not lost, and nothing is
   duplicated to replace it: the rail's Details pane already renders `Owner` as a fact
   (`app/cand-rail.jsx:79`), and the reassign path it carried is still one click away in the
   header's own Actions menu, under the same `canReassign` gate (`CandHeaderActions`, "Change
   owner"). So the affordance survives, the metadata sits where the reference doc puts it, and
   the header gets a row back.

   `Screening ·` (a separator with nothing after it) is gone with the subline: stage is a chip on
   the name line and the facts join themselves, so no separator can be orphaned. */
function CandIdentityHeader({ c, headerStage, hasMatch, dm, dmTier }) {
  /* `listMeta`, `canReassign` and `setReassignOpen` are still passed by cand-record.jsx and are
     deliberately not read here - they existed for the owner chip. Left in the call site because
     that file belongs to another module; extra props on a component are inert. */
  const facts = [
    (c.title && c.company) ? c.title + ' at ' + c.company : (c.title || c.company || null),
    c.location,
    c.years ? c.years + 'y experience' : null,
  ].filter(Boolean);
  return (
    <div className="e8-cand-header">
      <span className="e8-avatar e8-cand-header-av" aria-hidden="true">{candHeaderInitials(c.name)}</span>
      <div className="e8-cand-header-main">
        <div className="e8-cand-header-idline">
          <h1 className="e8-cand-header-name">{c.name}</h1>
          {headerStage ? (
            <span className="e8-cand-header-stage" title={'Record status - ' + headerStage + ' · it governs which action the header offers'}>{headerStage}</span>
          ) : null}
          <CandHeaderScore dm={dm} dmTier={dmTier} hasMatch={hasMatch} />
          {/* The wrapper is the only hook this module has on the pin: WorkspacePinButton
              (shell.jsx:44) renders a DS ghost icon button, and `.e8-btn-icononly.e8-btn-sm` is
              24x24 (components/ats-button/Button.jsx). 24px is not a touch target. The wrapper
              lets cand-header.css grow the HIT AREA on the touch shell without a bare
              `.e8-btn` override that would reach every other button in the app. */}
          <span className="e8-cand-header-pin">
            <WorkspacePinButton type="candidate" id={c.id} compact />
          </span>
        </div>
        <div className="e8-cand-header-meta">
          {/* Separators are CSS `::before` on every fact after the first, so there is no
              separator element that can outlive its content - the `Screening ·` defect was
              exactly that, a hard-coded `·` between two spans where the second had wrapped
              away. A generated one cannot be orphaned: no fact, no separator. The remaining
              half of that defect - a separator that survives its own LINE, so a wrap opened
              line 2 with "· 8y experience" - is handled in the CSS, which paints the dot
              OUTSIDE the fact's box and lets the row clip it. See cand-header.css. */}
          {facts.map((f, i) => <span className="e8-cand-header-fact" key={i}>{f}</span>)}
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { CandHeaderNav, CandHeaderActions, CandHeaderScore, CandIdentityHeader });
