/* Recruitment Desk — this product's status vocabulary.
   A value not listed here does not exist. A screen that wants one stops and
   asks; it never passes a string literal into a badge, and it never adds a
   value because a fixture would look better with it.

   Larkspur Health runs one hiring process for every requisition — that is the
   point of the desk. The seven ordered stages below ARE the stepper, in this
   order, everywhere an application appears.

   Shape matters here: a status field is `{id, label, tone}` and is read with
   `statusValue(ENUM, id)` — or `statusWords` for the same label mid-sentence.
   A classification list is `{value, label}` and is read with `labelOf`. Passing
   a status enum to `labelOf` matches nothing and hands back the raw id, which
   is how "DBS not_started" once reached a reader. */

/* ── Application stage ────────────────────────────────────────────────────
   Means: how far this application has got, and — because the tone grouping
   answers the only question a reviewer asks at a queue — who owes the next
   move. Grouped that way deliberately:
     info    · ours to move. The desk is the blocker.
     warning · out of our hands but on the clock: referees, the DBS service,
               the candidate holding an offer. Deliberate states needing
               attention, never failures.
     success · hired.
     neutral · settled, nothing owed by anybody.
   Deliberately ABSENT because they are derived, never stored: stalled (days in
   stage against the stage's own service level), unopened (no first review),
   overdue references, offer expiring. A record carries one stage and any number
   of derived flags — see APPLICATION_FLAGS. */
const APPLICATION_STAGE = [
  { id: "applied", label: "Applied", tone: "info" },
  { id: "screening", label: "Screening", tone: "info" },
  { id: "assessment", label: "Assessment", tone: "info" },
  { id: "panel", label: "Panel", tone: "info" },
  { id: "checks", label: "Checks", tone: "warning" },
  { id: "offer", label: "Offer", tone: "warning" },
  { id: "hired", label: "Hired", tone: "success" },
  { id: "declined", label: "Declined", tone: "neutral" },
  { id: "withdrawn", label: "Withdrawn", tone: "neutral" },
];
/* The route through the process, in order — the stepper reads this, not the
   whole enum, because declined and withdrawn are ends rather than steps. */
const APPLICATION_PATH = ["applied", "screening", "assessment", "panel", "checks", "offer", "hired"];
const STAGE_OURS = ["applied", "screening", "assessment", "panel"];
const STAGE_ELSEWHERE = ["checks", "offer"];
const STAGE_SETTLED = ["hired", "declined", "withdrawn"];
/* What each step is for, in the words a reviewer would use. The stepper and the
   stage note both read these; nothing describes a stage twice. */
const STAGE_MEANS = {
  applied: "Application in, nobody has read it yet",
  screening: "CV and answers read, screening call booked",
  assessment: "Written exercise or clinical scenario with the candidate",
  panel: "Panel interview and scorecards",
  checks: "Two references, right to work and DBS",
  offer: "Offer out, waiting on the candidate",
  hired: "Accepted and handed to onboarding",
};

/* ── Requisition status ───────────────────────────────────────────────────
   Means: whether this seat may be filled right now. `on_hold` is warning
   because it is a deliberate pause somebody has to come back to; `closed` is
   neutral because it is settled. Aging and under-supplied are derived. */
const REQUISITION_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "open", label: "Open", tone: "info" },
  { id: "on_hold", label: "On hold", tone: "warning" },
  { id: "filled", label: "Filled", tone: "success" },
  { id: "closed", label: "Closed", tone: "neutral" },
];

/* ── Interview status ─────────────────────────────────────────────────────
   Means: the state of one booked conversation. `no_show` is the only danger in
   the product: it is a failure event, not a state anybody chose. */
const INTERVIEW_STATUS = [
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "awaiting_feedback", label: "Awaiting feedback", tone: "warning" },
  { id: "complete", label: "Complete", tone: "success" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
  { id: "no_show", label: "No show", tone: "danger" },
];

/* ── Right to work ────────────────────────────────────────────────────────
   Means: what we hold on this candidate's right to work in the UK. Nothing
   here is a judgement on the person; it is the state of our paperwork. */
const RIGHT_TO_WORK = [
  { id: "verified", label: "Verified", tone: "success" },
  { id: "documents_seen", label: "Documents seen", tone: "info" },
  { id: "outstanding", label: "Outstanding", tone: "warning" },
  { id: "needs_sponsorship", label: "Needs sponsorship", tone: "warning" },
];

/* ── DBS ──────────────────────────────────────────────────────────────────
   Means: where this candidate's disclosure check has got to. It is a status
   field like any other, so it lives here rather than as a loose string on the
   record: `flagged` is warning, not danger, because a disclosure is a
   conversation to have and not a failure event. */
const DBS_STATUS = [
  { id: "not_started", label: "Not started", tone: "neutral" },
  { id: "submitted", label: "With the service", tone: "info" },
  { id: "clear", label: "Clear", tone: "success" },
  { id: "flagged", label: "Disclosure to discuss", tone: "warning" },
];

/* ── Reference status ─────────────────────────────────────────────────────
   Means: the state of one reference request. `refused` is danger — a referee
   declining to answer stops the hire. */
const REFERENCE_STATUS = [
  { id: "requested", label: "Requested", tone: "info" },
  { id: "chased", label: "Chased", tone: "warning" },
  { id: "received", label: "Received", tone: "success" },
  { id: "refused", label: "Refused", tone: "danger" },
];

/* Id unions, for the reader: an application's stage is one of
   APPLICATION_STAGE[].id; a requisition's status one of REQUISITION_STATUS[].id;
   and so on. Nothing in this product carries a status not in this file. */

/* ── Classification lists — read through labelOf, never through a Badge ── */
const DEPARTMENTS = [
  { value: "nursing", label: "Nursing" },
  { value: "therapies", label: "Therapies" },
  { value: "medical", label: "Medical" },
  { value: "digital", label: "Digital" },
  { value: "operations", label: "Operations" },
];
const SITES = [
  { value: "riverside", label: "Riverside Hospital" },
  { value: "eastgate", label: "Eastgate Clinic" },
  { value: "community", label: "Community teams" },
  { value: "hq", label: "Larkspur House" },
];
const SOURCES = [
  { value: "careers_site", label: "Careers site" },
  { value: "nhs_jobs", label: "Jobs board" },
  { value: "referral", label: "Colleague referral" },
  { value: "agency", label: "Agency" },
  { value: "pool", label: "Talent pool" },
  { value: "event", label: "Careers event" },
];
const EMPLOYMENT_TYPES = [
  { value: "permanent", label: "Permanent" },
  { value: "fixed_term", label: "Fixed term" },
  { value: "bank", label: "Bank" },
];
/* Which professional register a SEAT requires — a property of the requisition,
   not of the person, and the reason compliance can tell a nurse with no NMC
   number from a ward clerk who needs none. `none` stores as null. */
const REGISTERS = [
  { value: "none", label: "No professional register" },
  { value: "NMC", label: "NMC — nursing and midwifery" },
  { value: "HCPC", label: "HCPC — allied health professions" },
];
const DESK_ROLES = [
  { value: "recruiter", label: "Recruiter" },
  { value: "coordinator", label: "Coordinator" },
  { value: "hiring_manager", label: "Hiring manager" },
  { value: "panel", label: "Panel member" },
  { value: "lead", label: "Head of resourcing" },
];
const DECLINE_REASONS = [
  { value: "experience", label: "Not enough relevant experience" },
  { value: "registration", label: "Registration or qualification missing" },
  { value: "assessment", label: "Assessment below the bar" },
  { value: "panel", label: "Panel scored below the bar" },
  { value: "stronger_field", label: "Stronger candidates for the same seat" },
  { value: "money", label: "Expectation outside the band" },
];
const WITHDRAW_REASONS = [
  { value: "another_offer", label: "Took another offer" },
  { value: "timing", label: "Timing no longer works" },
  { value: "travel", label: "Travel or site" },
  { value: "money", label: "Money" },
  { value: "no_contact", label: "Stopped responding" },
];
/* What a panel scores. Four areas, the same four for every requisition, so two
   scorecards can sit side by side on one grid. */
const SCORE_AREAS = [
  { value: "clinical", label: "Clinical or technical" },
  { value: "people", label: "Working with people" },
  { value: "judgement", label: "Judgement under pressure" },
  { value: "values", label: "Larkspur values" },
];
/* Social. Named reactions, not free emoji: three things a reviewer actually
   means, so a count is readable ("4 agree") rather than decorative. */
const REACTIONS = [
  { value: "agree", label: "Agree", icon: "thumbs-up" },
  { value: "watching", label: "Watching", icon: "eye" },
  { value: "question", label: "Question", icon: "circle-help" },
];
const NOTE_KINDS = [
  { value: "note", label: "Note" },
  { value: "decision", label: "Decision" },
  { value: "candidate", label: "From the candidate" },
];
/* What a notification is about. A classification, not a status: a notification
   has one kind for its whole life, and the icon comes from here so no screen
   picks one. */
const NOTIFICATION_KINDS = [
  { value: "mention", label: "Mentions", icon: "at-sign" },
  { value: "panel", label: "Panel and scorecards", icon: "clipboard-pen" },
  { value: "offer", label: "Offers", icon: "file-signature" },
  { value: "compliance", label: "Compliance", icon: "shield-check" },
  { value: "application", label: "New applications", icon: "inbox" },
  { value: "interview", label: "Interviews", icon: "calendar-clock" },
];

/* ── Policy — the constants the arithmetic and the copy both read ─────────
   Editable on /settings/process. It is one object and every screen reads it, so
   changing a service level here moves the queue, the stepper, the badges and the
   compliance screen at once — which is the point of it being one object. */
const RD_POLICY = {
  /* Service level per stage, in whole days. isStalled compares against the
     stage the application is in, never against one number for all of them. */
  stageDays: { applied: 3, screening: 5, assessment: 7, panel: 5, checks: 10, offer: 7 },
  panelScorecards: 2,      // a panel decision needs two, from different people
  referencesRequired: 2,   // NHS standard: two, one the current employer
  offerValidDays: 7,
  agingDays: 45,           // an open requisition older than this is aging
  thinPipeline: 3,         // fewer live applications than this and the seat is at risk
  registrationWarnDays: 60, // NMC or HCPC registration inside this is flagged on /compliance
  offerExpiryWarnDays: 3,   // an offer inside this is on the sidebar badge
};

/* ── Derived flags — computed at render, never stored, never in an enum.
   The clock is injected so a screen cannot quietly use a different now. ──── */
const APPLICATION_FLAGS = {
  isLive: (a) => STAGE_SETTLED.indexOf(a.stage) === -1,
  /* THE act-ability predicate. The row's actions, the section count, the tab
     count and the sidebar badge all call this one function. */
  awaitsUs: (a) => STAGE_OURS.indexOf(a.stage) > -1,
  awaitsThem: (a) => STAGE_ELSEWHERE.indexOf(a.stage) > -1,
  isUnread: (a) => a.stage === "applied" && !a.firstReadAt,
  isStalled: (a, now) => {
    const allowed = RD_POLICY.stageDays[a.stage];
    if (!allowed || STAGE_SETTLED.indexOf(a.stage) > -1) return false;
    return now - a.stageEnteredAt > allowed * 864e5;
  },
  offerExpiring: (a, now) => a.stage === "offer" && !!a.offer && a.offer.expiresAt - now < 3 * 864e5,
};
const INTERVIEW_FLAGS = {
  isBooked: (iv) => iv.status === "scheduled",
  feedbackOutstanding: (iv) => iv.status === "awaiting_feedback",
  isToday: (iv, now) => new Date(iv.at).toDateString() === new Date(now).toDateString(),
  isAhead: (iv, now) => iv.at >= now && iv.status === "scheduled",
};
const REQUISITION_FLAGS = {
  isOpen: (r) => r.status === "open",
  isAging: (r, now) => r.status === "open" && now - r.openedAt > RD_POLICY.agingDays * 864e5,
  isPosted: (advert) => !!advert && advert.postedTo.length > 0,
};
/* Compliance — the three things that have to be true before somebody starts,
   all derived, none of them a status of their own. Registration expiry is the
   only one with a clock, and the clock is injected.

   Registration is the one check that is not owed by everybody: whether a
   registration is required at all is a property of the SEAT (requisition.
   registration), so these predicates take the requisition rather than assuming
   every hire is a registered clinician — a ward clerk with no NMC number is not
   a blocked hire, and reading it off the application alone said he was. */
const COMPLIANCE_FLAGS = {
  rtwOutstanding: (a) => a.rightToWork !== "verified",
  needsSponsorship: (a) => a.rightToWork === "needs_sponsorship",
  dbsOutstanding: (a) => a.dbs !== "clear",
  registrationRequired: (requisition) => !!requisition && !!requisition.registration,
  registrationMissing: (a, requisition) => COMPLIANCE_FLAGS.registrationRequired(requisition) && !a.registrationExpiresAt,
  registrationExpired: (a, now) => !!a.registrationExpiresAt && a.registrationExpiresAt < now,
  registrationExpiring: (a, now) => !!a.registrationExpiresAt && a.registrationExpiresAt >= now
    && a.registrationExpiresAt - now <= RD_POLICY.registrationWarnDays * 864e5,
  /* Is the register this SEAT asks for something somebody still has to act on —
     missing, lapsed, or renewing inside the warn window. A register the seat does
     not ask for is never outstanding, whatever the candidate happens to carry. */
  registrationOutstanding: (a, requisition, now) => COMPLIANCE_FLAGS.registrationRequired(requisition)
    && (COMPLIANCE_FLAGS.registrationMissing(a, requisition)
      || COMPLIANCE_FLAGS.registrationExpired(a, now)
      || COMPLIANCE_FLAGS.registrationExpiring(a, now)),
  /* THE predicate the compliance screen, its recession, its counts and the nav
     badge all share: nothing outstanding on this person. It is the negation of
     the three flags above and nothing else, so a row can never be counted as
     clear and shown as blocked at the same time — which is what once recessed
     the one row on the screen with a live action on it. */
  clearToStart: (a, requisition, now) => !COMPLIANCE_FLAGS.rtwOutstanding(a)
    && !COMPLIANCE_FLAGS.dbsOutstanding(a)
    && !COMPLIANCE_FLAGS.registrationOutstanding(a, requisition, now),
};

/* ── Transitions ──────────────────────────────────────────────────────────
   Named once, after the move, and offered by ONE function per entity that every
   surface calls — the row menu, the detail header, the board card and the bulk
   bar. A transition that does not apply is omitted, never disabled; a
   transition that exists but this reviewer cannot take yet is disabled with the
   reason stated. Exactly one is primary: the record's normal path. */
const STAGE_AFTER = { applied: "screening", screening: "assessment", assessment: "panel", panel: "checks", checks: "offer", offer: "hired" };
const ADVANCE_LABEL = {
  screening: "Move to screening",
  assessment: "Send the assessment",
  panel: "Book the panel",
  checks: "Start the checks",
  offer: "Send the offer",
  hired: "Record acceptance",
};

function applicationTransitions(application, context) {
  const ctx = context || {};
  const next = STAGE_AFTER[application.stage];
  const out = [];
  if (next) {
    const gate = advanceGate(application, ctx);
    out.push({
      id: "advance", to: next, label: ADVANCE_LABEL[next], primary: true,
      disabled: !!gate, disabledReason: gate,
    });
  }
  if (application.stage === "checks" || application.stage === "panel") {
    out.push({ id: "book_interview", label: "Book an interview" });
  }
  if (APPLICATION_FLAGS.isLive(application)) {
    out.push({ id: "decline", label: "Decline", destructive: true });
    out.push({ id: "withdraw", label: "Record a withdrawal" });
  }
  if (application.stage === "declined" && application.wouldReconsider) {
    out.push({ id: "reopen", label: "Reopen for another seat", primary: true });
  }
  return out;
}

/* Why the primary move is not available yet — a visible precondition, stated on
   the disabled control rather than left for the reviewer to guess. */
function advanceGate(application, ctx) {
  const scorecards = ctx.scorecards === undefined ? (application.scorecards || []).length : ctx.scorecards;
  if (application.stage === "panel" && scorecards < RD_POLICY.panelScorecards) {
    return RD_POLICY.panelScorecards + " scorecards are needed before checks start — " + scorecards + " in so far.";
  }
  if (application.stage === "checks") {
    /* One list, one spelling. checksMissing (metrics.js) is what the checks card
       on the record renders, so the reason on the disabled control and the card
       beside it cannot disagree — this branch used to omit the DBS the card
       named, which let an offer look available while a check was outstanding. */
    const missing = checksMissing(application);
    if (missing.length) return "An offer waits on the checks: " + missing.join(" · ") + ".";
  }
  if (application.stage === "offer" && application.offer && !application.offer.acceptedAt) {
    return "Nothing to record until the candidate accepts — the offer is with them until " + new Date(application.offer.expiresAt).toLocaleDateString("en-GB", { day: "numeric", month: "short" }) + ".";
  }
  return null;
}

function requisitionTransitions(requisition) {
  const out = [];
  if (requisition.status === "draft") out.push({ id: "open_req", label: "Open the requisition", primary: true });
  if (requisition.status === "open") {
    out.push({ id: "hold_req", label: "Put on hold" });
    out.push({ id: "close_req", label: "Close without hiring", destructive: true });
  }
  if (requisition.status === "on_hold") out.push({ id: "resume_req", label: "Reopen the requisition", primary: true });
  return out;
}

function interviewTransitions(interview) {
  const out = [];
  if (interview.status === "scheduled") {
    out.push({ id: "record_held", label: "Mark it held", primary: true });
    out.push({ id: "reschedule", label: "Reschedule" });
    out.push({ id: "record_no_show", label: "Record a no show" });
    out.push({ id: "cancel_interview", label: "Cancel the interview", destructive: true });
  }
  if (interview.status === "awaiting_feedback") out.push({ id: "add_scorecard", label: "Add your scorecard", primary: true });
  return out;
}

/* ── The registry, and what the fixture still does not cover ───────────── */
const STATUS_FIELDS = [
  { entity: "application", field: "stage", means: "How far the application has got, and who owes the next move", values: APPLICATION_STAGE },
  { entity: "requisition", field: "status", means: "Whether this seat may be filled right now", values: REQUISITION_STATUS },
  { entity: "interview", field: "status", means: "The state of one booked conversation", values: INTERVIEW_STATUS },
  { entity: "application", field: "rightToWork", means: "What we hold on this candidate's right to work", values: RIGHT_TO_WORK },
  { entity: "reference", field: "status", means: "The state of one reference request", values: REFERENCE_STATUS },
  { entity: "application", field: "dbs", means: "Where the disclosure check has got to", values: DBS_STATUS },
];

/* Coverage. Every value above has at least one fixture row except:
   · requisition.draft — reachable only by creating one on /requisitions/new,
     which is how the fixture stays honest about a state the desk makes itself.
   · interview.cancelled — reachable through the cancel transition.
   · reference.refused — no fixture row; the copy for it exists on the checks
     panel and it is reachable by chasing a refusal in a real system, so it is
     recorded here as uncovered rather than faked.
   · dbs.flagged — no fixture row. A disclosure to discuss is a real state with a
     real screen behind it on /compliance, and inventing one for a demo candidate
     would put an untrue thing on a person's record.
   dbs.submitted sits on one row (Priya Raghunathan, whose checks pack went out
   on the 27th) — the state where the desk is waiting on somebody else, which the
   screen would otherwise never show. */

Object.assign(window, {
  APPLICATION_STAGE, APPLICATION_PATH, STAGE_OURS, STAGE_ELSEWHERE, STAGE_SETTLED, STAGE_MEANS,
  REQUISITION_STATUS, INTERVIEW_STATUS, RIGHT_TO_WORK, REFERENCE_STATUS, DBS_STATUS,
  DEPARTMENTS, SITES, SOURCES, EMPLOYMENT_TYPES, REGISTERS, DESK_ROLES, DECLINE_REASONS, WITHDRAW_REASONS,
  SCORE_AREAS, REACTIONS, NOTE_KINDS, NOTIFICATION_KINDS, RD_POLICY,
  APPLICATION_FLAGS, INTERVIEW_FLAGS, REQUISITION_FLAGS, COMPLIANCE_FLAGS,
  STAGE_AFTER, ADVANCE_LABEL, applicationTransitions, advanceGate, requisitionTransitions, interviewTransitions,
  STATUS_FIELDS,
});
