/* ELEV8 ATS - R138: the job record's rail (spec 2026-08-05-job-record-rail-design.md).
   Its own file because screens-job.jsx is already ~3,500 lines and this is ~350 of them.

   Three panes - Details / Activity / Tasks - rendered inside the shared RecordRail. The rail
   is presentational plus its own write path: RailFact stays dumb, and the commit callbacks
   below own the store write, the snapshot, the Undo, the visible note and the audit entry.

   Namespace `.e8-jrail-*` (grepped free before use). The old `.e8-rail-*` classes stay - the
   kanban column headers and four unmigrated record screens still depend on them. */

const DSjr = window.Stand8DesignSystem_b5c975;

const JOB_RAIL_TABS = [
  { id: 'details', label: 'Details' },
  { id: 'activity', label: 'Activity' },
  { id: 'tasks', label: 'Tasks' },
];

/* Populated, not merely defined. Most job facts resolve empty on the default dataset - 0 of 46
   open jobs have an owner or assignees and only 8 of 46 have a requisition - so a fold labelled
   from the number of DECLARED rows would read "All details · 13" and open onto blanks.

   The dataset writes its own blanks as a DASH, not as null: `JO-10866.rate` is the single
   character "—". Left untreated that reads as a populated value, so the fold over-counts and an
   editable fact offers a dash to edit instead of an "Add rate" invitation. */
const JR_BLANK = /^[\s\-‐-―]*$/;
function jrHas(v) {
  if (v == null || (Array.isArray(v) && v.length === 0)) return false;
  return !(typeof v === 'string' && JR_BLANK.test(v));
}
/* The value a RailFact should EDIT: a dataset dash is an empty field, not a starting point. */
function jrVal(v) {
  return jrHas(v) ? String(v) : '';
}

/* The row stores a display string ("$95/hr"); jobDetails stores a number (95). Both are written
   by the requisition form, so a rail edit has to keep them in step - see jrCommit. */
function jrParseRate(s) {
  const n = Number(String(s == null ? '' : s).replace(/[^0-9.]/g, ''));
  return isFinite(n) && n > 0 ? n : null;
}

function jrAgo(ts) {
  return window.e8AgoLabel ? window.e8AgoLabel(ts) : 'Recently';
}

/* An edit lands in THREE places, and each answers a different question.
   - a NOTE, because that is what the Activity pane reads (timelineFor reads notes, not the audit);
   - an AUDIT entry, the global prose flight recorder;
   - an E8EditLog entry, the STRUCTURED per-field history: which field, from what, to what, by whom.
   The first two are prose and cannot answer "every change to bill rate on this job"; the third can.
   Undo appends a reversal to each rather than deleting the original. */
function jrLogEdit(J, label, fromVal, toVal, field) {
  const D = window.E8DATA || {};
  const noteId = 'n-jedit-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
  window.E8Store.add('notes', {
    id: noteId,
    type: 'field-edit',
    title: label + ' updated',
    points: [(jrHas(fromVal) ? 'Was ' + fromVal : 'Was empty') + ' → ' + toVal],
    refType: 'job', refId: J.id, ts: Date.now(),
    author: (D.user || {}).name || 'You', prov: 'human', when: 'Just now',
  });
  if (window.E8Audit) {
    window.E8Audit.log({
      agent: 'You', prov: 'human', action: 'Edited ' + label.toLowerCase(),
      target: J.id + ' · ' + J.title, route: 'job/' + J.id + '/overview',
    });
  }
  let entryId = null;
  if (window.E8EditLog && field) {
    const rec = window.E8EditLog.record({
      recordType: 'job', recordId: J.id, field: field, label: label,
      from: fromVal, to: toVal,
      by: (D.user || {}).name || 'You', actorType: 'human', source: 'rail',
    });
    entryId = rec ? rec.id : null;
  }
  return { noteId, entryId };
}

/* One commit: the row write, the mirrored jobDetails key where one exists, the note, the audit
   entry, and an Undo that restores absence AS absence.

   The mirror matters. `rate`/`duration`/`openings` live on the job row AND in jobDetails
   (billRate / duration / totalOpenings), and screens-req.jsx writes both on every requisition
   save. Writing only one side lets them drift until the next form save silently reverts the rail
   edit. (The design spec listed Openings as row-only; the requisition form disagrees - it derives
   the row's `openings` from `totalOpenings` - so Openings mirrors too.)

   jobDetails has no per-key delete, so undo of a detail write either drops the whole entry (when
   the job had no requisition before - the common case at 38 of 46 jobs) or replaces it wholesale
   with the pre-edit snapshot. A `setDetail({ key: undefined })` would NOT do: undefined is
   dropped by JSON.stringify, so the op would replay as an empty merge and the edited value would
   come back on reload. */
function jrCommit(opts) {
  const S = window.E8Store;
  if (!S) return;
  const J = opts.J;
  const coll = window.jobCollection ? window.jobCollection(J, opts.status) : 'jobs';
  /* Undo restores what was THERE, so this asks whether the key existed - not whether its value
     meant anything. `jrHas` would be wrong here: rows spell "no bill rate" as the em dash "—",
     and unsetting the key instead of putting the dash back would leave the jobs list rendering a
     blank cell where it used to render a dash. */
  const hadRow = Object.prototype.hasOwnProperty.call(J, opts.rowField);
  const prevRow = J[opts.rowField];
  const prevDetail = opts.detailField ? S.snapshotDetail(J.id) : null;

  S.set(coll, J.id, { [opts.rowField]: opts.rowValue });
  if (opts.detailField) S.setDetail(J.id, { [opts.detailField]: opts.detailValue });
  const { noteId, entryId } = jrLogEdit(J, opts.label, opts.fromDisplay, opts.toDisplay,
    opts.field || opts.rowField);
  if (window.E8Events) window.E8Events.emit('job:updated', { id: J.id });

  if (opts.showToast) {
    opts.showToast(opts.label + ' updated', 'Undo', () => {
      if (hadRow) S.set(coll, J.id, { [opts.rowField]: prevRow });
      else S.unset(coll, J.id, [opts.rowField]);
      if (opts.detailField) {
        S.removeDetail(J.id);
        if (prevDetail) S.setDetail(J.id, prevDetail);
      }
      S.remove('notes', noteId);
      /* The note is removed (it was a transient activity item) but the STRUCTURED history is
         append-only: revert() writes the inverse and marks the original rather than erasing it,
         so "this was set to Remote and then undone" stays answerable. */
      if (window.E8EditLog && entryId) window.E8EditLog.revert('job', J.id, entryId);
      if (window.E8Audit) {
        window.E8Audit.log({
          agent: 'You', prov: 'human', action: 'Undid ' + opts.label.toLowerCase() + ' edit',
          target: J.id + ' · ' + J.title, route: 'job/' + J.id + '/overview',
        });
      }
      if (window.E8Events) window.E8Events.emit('job:updated', { id: J.id });
    });
  }
}

/* ---------- Details pane ---------- */

function JobRailDetails({ J, status, det, contacts, onOpenOverview, showToast }) {
  const editable = status === 'open'; /* closed and filled reqs are read-only - plain values, no dashed affordance */
  const loc = window.jobLocSummary ? window.jobLocSummary(J) : J.location;
  const priority = (window.JOB_PRIORITIES && window.JOB_PRIORITIES[J.priority]) || null;
  const health = J.health || null;

  const commitRate = (next) => {
    const n = jrParseRate(next);
    if (n == null) return;
    jrCommit({
      J: J, status: status, label: 'Bill rate', showToast: showToast,
      rowField: 'rate', rowValue: '$' + n + '/hr',
      detailField: 'billRate', detailValue: n,
      fromDisplay: J.rate, toDisplay: '$' + n + '/hr',
    });
  };
  const commitDuration = (next) => jrCommit({
    J: J, status: status, label: 'Duration', showToast: showToast,
    rowField: 'duration', rowValue: next,
    detailField: 'duration', detailValue: next,
    fromDisplay: J.duration, toDisplay: next,
  });
  const commitOpenings = (next) => {
    const n = parseInt(next, 10);
    if (!isFinite(n)) return;
    jrCommit({
      J: J, status: status, label: 'Openings', showToast: showToast,
      rowField: 'openings', rowValue: n,
      detailField: 'totalOpenings', detailValue: n,
      fromDisplay: J.openings == null ? '' : String(J.openings), toDisplay: String(n),
    });
  };
  const commitPriority = (next) => {
    const n = parseInt(next, 10);
    const label = (window.JOB_PRIORITIES && window.JOB_PRIORITIES[n]) ? window.JOB_PRIORITIES[n].label : String(n);
    jrCommit({
      J: J, status: status, label: 'Priority', showToast: showToast,
      rowField: 'priority', rowValue: n,
      fromDisplay: priority ? priority.label : '', toDisplay: label,
    });
  };

  const commitEmployment = (next) => jrCommit({
    J: J, status: status, label: 'Employment', showToast: showToast,
    rowField: 'employmentType', rowValue: next,
    detailField: 'employmentType', detailValue: next,
    fromDisplay: J.employmentType || J.type || '', toDisplay: next,
  });
  const commitLocation = (next) => jrCommit({
    J: J, status: status, label: 'Location', showToast: showToast,
    rowField: 'location', rowValue: next,
    fromDisplay: J.location || '', toDisplay: next,
  });

  /* Everything the fold holds, declared once so the count and the body cannot disagree. */
  const foldFacts = [
    ['REQ ID', J.reqId],
    ['Job order', J.id],
    ['Tier', J.tierCode ? J.tierCode + ' · ' + (J.tierLabel || 'Comp') : null],
    ['Open / closed', status === 'open' ? 'Open' : 'Closed'],
    ['Aging', (J.day != null && J.days != null) ? 'day ' + J.day + ' of ' + J.days : null],
    ['Hours / week', det && det.hoursPerWeek != null ? String(det.hoursPerWeek) : null],
    ['Shift', det && det.shift],
    ['Work auth', det && det.workAuth],
    ['Background check', det && det.backgroundCheck != null ? (det.backgroundCheck ? 'Required' : 'Not required') : null],
    ['Target start', (det && det.startTarget) || J.start],
  ];
  if (status === 'closed') {
    foldFacts.push(['Outcome', J.closeReason || J.reason], ['Closed', J.closedDate],
      ['Days open', J.daysOpen != null ? String(J.daysOpen) : null]);
  } else if (status === 'filled') {
    foldFacts.push(['Placed', J.consultant], ['Filled', J.filledDate],
      ['Time to fill', J.fillDays != null ? J.fillDays + ' days' : null]);
  }
  const shown = foldFacts.filter(([, v]) => jrHas(v));

  return (
    <React.Fragment>
      {health ? (
        <div className={'e8-jrail-health is-' + (health.tone || 'neutral')}>
          <div className="e8-jrail-health-head">
            <DSjr.Badge tone={health.tone || 'neutral'} dot>{health.label}</DSjr.Badge>
          </div>
          {health.note ? <div className="e8-jrail-health-note">{health.note}</div> : null}
        </div>
      ) : null}

      <div className="e8-rrail-sec">
        <RailFact label="Client" value={jrVal(J.client)} display={<window.JobClientLink job={J} compact />} />
        <RailFact
          label="Bill rate" value={jrVal(J.rate)} editable={editable} onCommit={commitRate}
          validate={(v) => (jrParseRate(v) == null ? 'Enter an hourly rate' : null)}
          placeholder="Add rate"
        />
        <RailFact
          label="Employment" value={jrVal(J.employmentType || J.type)}
          editable={editable} onCommit={commitEmployment}
          options={window.JOB_EMPLOYMENT_TYPES || ['Contract', 'Contract to hire', 'Direct hire']}
          placeholder="Set employment type"
        />
        <RailFact
          label="Duration" value={jrVal(J.duration)} editable={editable} onCommit={commitDuration}
          validate={(v) => (v ? null : 'Enter a duration')}
          placeholder="Add duration"
        />
        <RailFact
          label="Openings" value={J.openings == null ? '' : String(J.openings)}
          display={J.openings != null && J.filled != null ? J.filled + ' of ' + J.openings : null}
          editable={editable} onCommit={commitOpenings}
          validate={(v) => (/^\d+$/.test(v) && Number(v) > 0 ? null : 'Enter a whole number')}
          placeholder="Add openings"
        />
        {status === 'open' ? (
          <RailFact
            label="Priority" value={J.priority == null ? '' : String(J.priority)}
            display={priority ? priority.label : null}
            editable onCommit={commitPriority}
            /* Was a text box you typed 1-4 into, validated with the message "1 urgent - 4 low".
               The options ARE the validation now, so the validate prop is gone with it. */
            options={Object.keys(window.JOB_PRIORITIES || {}).map((k) => ({ value: k, label: window.JOB_PRIORITIES[k].label }))}
            placeholder="Set priority"
          />
        ) : null}
        <RailFact
          label="Location" value={jrVal(loc)}
          editable={editable} onCommit={commitLocation}
          validate={(v) => (v ? null : 'Enter a location')}
          placeholder="Add location"
        />
      </div>

      <RailSection title="Ownership">
        {/* Owner + "Working it" - two separately-editable facts, so the existing block owns them. */}
        <window.JobOwnershipBlock J={J} readOnly={status !== 'open'} />
      </RailSection>

      <RailFold label="All details" count={shown.length}>
        {shown.map(([k, v]) => <RailFact key={k} label={k} value={jrVal(v)} />)}
        {/* Sync is hardcoded theater - `D.job.sync` exists but nothing reads it. Demoted into the
            fold rather than left presenting fake freshness at eye level; wiring it is out of scope. */}
        {status === 'open' ? (
          <div className="e8-jrail-sync">
            {/* Read the record, do not hardcode. `sync` lives on the DETAIL object (D.job), not on
                the jobs row, so `det` is checked first and `J` second - a review asked for J.sync
                and J is the row, which never carries it. Generic fallback when neither does. */}
            <DSjr.Badge tone="success" dot>{((det && det.sync) || J.sync || {}).state || 'Imported'}</DSjr.Badge>
            <span>{((det && det.sync) || J.sync || {}).detail || 'From the initial import'}</span>
          </div>
        ) : null}
      </RailFold>

      <RailSection title="Client contacts" count={contacts.length}>
        {contacts.length ? contacts.map((c) => (
          <div key={c.name} className="e8-jrail-contact">
            <DSjr.Avatar name={c.name} size="sm" />
            <div className="e8-jrail-contact-txt">
              <div className="e8-jrail-contact-n">{c.name}</div>
              <div className="e8-jrail-contact-r" title={c.role}>{c.role}</div>
            </div>
          </div>
        )) : <div className="e8-jrail-empty">No contacts on this account yet.</div>}
      </RailSection>

      {/* Requirements used to live here and duplicated Overview's own Requirements section - its
          third-level fallback even invented a synthetic scope. One home, plus a way to reach it. */}
      <button type="button" className="e8-jrail-link" onClick={onOpenOverview}>
        <span className="material-symbols-outlined" aria-hidden="true">checklist</span>
        Requirements &amp; scope in Overview
      </button>
    </React.Fragment>
  );
}

/* ---------- Activity pane ---------- */

/* The job's real event log. `timelineFor()` is candidate-only and unexported, and the job's own
   JobActivityTimeline is two hardcoded synthetic events - but jobStageEvents(jobId) has been a
   true per-job log with real timestamps and actors all along (the attribution engine reads it).
   Merged with the record's notes, that is the first honest job timeline, and it costs nothing
   because both halves already exist. */
function jobRailEvents(J) {
  const D = window.E8DATA || {};
  const stages = D.submissionStages || [];
  const moves = (window.jobStageEvents ? window.jobStageEvents(J.id) : []).map((e, i) => ({
    id: 'jse-' + e.subId + '-' + i,
    ts: e.at,
    when: jrAgo(e.at),
    actor: e.by || 'System',
    ai: false, prov: 'human',
    title: e.withdrawn ? ('Withdrew ' + e.cand) : ((stages[e.stage] || 'Submitted') + ' · ' + e.cand),
    body: undefined,
    link: e.candId ? 'Open record' : undefined,
    linkTo: e.candId ? 'candidate/' + e.candId : undefined,
  }));
  const notes = window.e8NoteEvents ? window.e8NoteEvents('job', J.id) : [];
  return window.e8MergeEvents ? window.e8MergeEvents(moves.concat(notes)) : moves.concat(notes);
}

function JobRailActivity({ J, onAddNote }) {
  const events = jobRailEvents(J);
  return (
    <React.Fragment>
      {events.length ? <window.Timeline events={events} quiet /> : (
        <div className="e8-jrail-emptywrap">
          <DSjr.EmptyState
            icon="history" title="Nothing has happened yet"
            body="Stage moves and notes on this req land here."
            cta={onAddNote ? <DSjr.Button variant="secondary" size="sm" className="e8-empty-cta" icon="note_add" onClick={onAddNote}>Add the first note</DSjr.Button> : undefined}
          />
        </div>
      )}
      {/* Attribution lives with Activity, not Details: it is provenance rather than a daily read,
          and its window + history disclosure are local state that would reset on every
          Details<->Activity flip if it sat in the other pane. */}
      <window.JobAttributionPanel J={J} />
      <div className="e8-rrail-footnote">edits &amp; audit live here — their one home</div>
    </React.Fragment>
  );
}

/* ---------- The rail ---------- */

function JobRail({ J, status, det, contacts, active, onTab, onOpenOverview, onAddNote, showToast }) {
  return (
    <RecordRail active={active} onTab={onTab} tabs={JOB_RAIL_TABS}>
      {active === 'activity' ? <JobRailActivity J={J} onAddNote={onAddNote} /> : null}
      {active === 'tasks' ? (
        window.RecordTasksRail
          ? <window.RecordTasksRail refType="job" refId={J.id} bare />
          : null
      ) : null}
      {active === 'details' ? (
        <JobRailDetails J={J} status={status} det={det} contacts={contacts}
          onOpenOverview={onOpenOverview} showToast={showToast} />
      ) : null}
    </RecordRail>
  );
}

Object.assign(window, { JobRail, JOB_RAIL_TABS, jobRailEvents });
