/* Ward Desk — this hospital's status vocabulary.
   A value that is not listed here does not exist. No screen may invent a status,
   pass a string literal into a Badge, or assign a tone: StatusBadge reads these
   lists and nothing else does.

   Everything computed from other fields is a DERIVED FLAG, not a status: a bed
   that has been waited on too long, a consent that has not been recorded, a dose
   that is due. Those live in the *_FLAGS blocks below with the clock injected,
   and a record carries one status and any number of flags.

   Tone grouping, one question, product-wide: WHO IS THIS WAITING ON.
     warning  — waiting on this hospital, and somebody here has to act
     info     — running normally, nobody is owed anything
     success  — closed well
     danger   — a failure event, not a deliberate state
     neutral  — closed, or parked on purpose
*/

/* Where a patient is being seen. Stored on the patient, moved by admission and
   discharge; "deceased" is recorded here because every downstream list must stop
   offering clinical actions on the record. */
const PATIENT_STATUS = [
  { id: "outpatient", label: "Outpatient", tone: "neutral" },
  { id: "inpatient", label: "Inpatient", tone: "info" },
  { id: "day_case", label: "Day case", tone: "info" },
  { id: "discharged", label: "Discharged", tone: "success" },
  { id: "deceased", label: "Deceased", tone: "neutral" },
];

/* One episode of care on a ward, in the order it moves.
   awaiting_bed is warning because the bed is this hospital's to find;
   fit_for_discharge is warning for the same reason — the ward is holding a bed
   for somebody who no longer needs it. */
const ADMISSION_STATUS = [
  { id: "awaiting_bed", label: "Awaiting bed", tone: "warning" },
  { id: "on_ward", label: "On ward", tone: "info" },
  { id: "fit_for_discharge", label: "Fit for discharge", tone: "warning" },
  { id: "discharged", label: "Discharged", tone: "success" },
  { id: "transferred", label: "Transferred out", tone: "neutral" },
];

/* A procedure. requested is warning: it is sitting on a scheduler's desk.
   cancelled is neutral, not danger — a cancelled list is a decision, not a
   failure — while abandoned is danger, because the procedure started and could
   not be finished. */
const INTERVENTION_STATUS = [
  { id: "requested", label: "Requested", tone: "warning" },
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "in_theatre", label: "In theatre", tone: "info" },
  { id: "recovery", label: "In recovery", tone: "info" },
  { id: "completed", label: "Completed", tone: "success" },
  { id: "abandoned", label: "Abandoned", tone: "danger" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
];

/* A prescription on a chart. awaiting_verification and on_hold are both warning:
   one is owed a pharmacist, the other is owed a prescriber's decision. */
const MEDICATION_STATUS = [
  { id: "awaiting_verification", label: "Awaiting verification", tone: "warning" },
  { id: "active", label: "Active", tone: "info" },
  { id: "on_hold", label: "On hold", tone: "warning" },
  { id: "completed", label: "Course finished", tone: "success" },
  { id: "stopped", label: "Stopped", tone: "neutral" },
];

/* Stock of a formulary line. Deliberately absent: "expiring" — that is derived
   from the earliest batch date and is a flag, not a stored state. */
const STOCK_STATUS = [
  { id: "in_stock", label: "In stock", tone: "success" },
  { id: "low", label: "Low", tone: "warning" },
  { id: "out", label: "Out of stock", tone: "danger" },
  { id: "discontinued", label: "Discontinued", tone: "neutral" },
];

/* Employment state of anybody with a badge — doctors and every other employee
   read one field, because the directory screens are two views of one list. */
const STAFF_STATUS = [
  { id: "onboarding", label: "Onboarding", tone: "info" },
  { id: "active", label: "Active", tone: "success" },
  { id: "on_leave", label: "On leave", tone: "neutral" },
  { id: "suspended", label: "Suspended", tone: "warning" },
  { id: "left", label: "Left", tone: "neutral" },
];

/* An invoice for one episode of care, in the order it moves. awaiting_coding and
   disputed are both warning: one is owed a coder here, the other is owed an
   answer to the payer's query. Deliberately absent because it is DERIVED:
   "overdue" — a due date in the past with a balance outstanding. */
const INVOICE_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "awaiting_coding", label: "Awaiting coding", tone: "warning" },
  { id: "issued", label: "Issued", tone: "info" },
  { id: "part_paid", label: "Part paid", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
  { id: "disputed", label: "Disputed", tone: "warning" },
  { id: "written_off", label: "Written off", tone: "neutral" },
];

/* One movement of money against an invoice. failed is danger — the money did not
   arrive; refunded is neutral, because sending money back is a decision. */
const PAYMENT_STATUS = [
  { id: "pending", label: "Pending", tone: "info" },
  { id: "cleared", label: "Cleared", tone: "success" },
  { id: "failed", label: "Failed", tone: "danger" },
  { id: "refunded", label: "Refunded", tone: "neutral" },
];

/* An import or an export job. needs_review is warning — rows were parsed but
   somebody has to accept or fix them; failed is danger. */
const TRANSFER_STATUS = [
  { id: "queued", label: "Queued", tone: "neutral" },
  { id: "running", label: "Running", tone: "info" },
  { id: "needs_review", label: "Needs review", tone: "warning" },
  { id: "completed", label: "Completed", tone: "success" },
  { id: "failed", label: "Failed", tone: "danger" },
];

/* A contract with a payer. Deliberately absent because it is DERIVED:
   "expiring" — an end date inside the next 30 days. */
const CONTRACT_STATUS = [
  { id: "live", label: "Live", tone: "success" },
  { id: "expired", label: "Expired", tone: "neutral" },
];

/* ── Classification lists — read through labelOf, never through a Badge ───── */
const WARDS = [
  { value: "beaumont", label: "Beaumont — general surgery" },
  { value: "linden", label: "Linden — acute medicine" },
  { value: "corvid", label: "Corvid — cardiology" },
  { value: "aster", label: "Aster — day surgery" },
  { value: "hollis", label: "Hollis — older people" },
];
const SPECIALTIES = [
  { value: "general_surgery", label: "General surgery" },
  { value: "cardiology", label: "Cardiology" },
  { value: "acute_medicine", label: "Acute medicine" },
  { value: "anaesthetics", label: "Anaesthetics" },
  { value: "geriatrics", label: "Geriatric medicine" },
  { value: "radiology", label: "Radiology" },
];
const JOB_FAMILIES = [
  { value: "medical", label: "Medical" },
  { value: "nursing", label: "Nursing" },
  { value: "pharmacy", label: "Pharmacy" },
  { value: "imaging", label: "Imaging" },
  { value: "admin", label: "Administration" },
  { value: "facilities", label: "Facilities" },
];
const DRUG_ROUTES = [
  { value: "oral", label: "Oral" },
  { value: "iv", label: "Intravenous" },
  { value: "sc", label: "Subcutaneous" },
  { value: "topical", label: "Topical" },
  { value: "inhaled", label: "Inhaled" },
];
const THEATRES = [
  { value: "t1", label: "Theatre 1" },
  { value: "t2", label: "Theatre 2" },
  { value: "t3", label: "Theatre 3" },
  { value: "day", label: "Day-case suite" },
];
const PRIORITIES = [
  { value: "routine", label: "Routine" },
  { value: "urgent", label: "Urgent" },
  { value: "emergency", label: "Emergency" },
];
const ADMISSION_SOURCES = [
  { value: "ed", label: "Emergency department" },
  { value: "elective", label: "Elective list" },
  { value: "gp", label: "GP referral" },
  { value: "transfer", label: "Transfer from another trust" },
];
const TRANSFER_KINDS = [
  { value: "import", label: "Import" },
  { value: "export", label: "Export" },
];
const IMPORT_SOURCES = [
  { value: "pas_csv", label: "Patient administration system — CSV" },
  { value: "lab_hl7", label: "Pathology — HL7 feed" },
  { value: "rota_xlsx", label: "Rota system — XLSX" },
  { value: "formulary_csv", label: "Formulary supplier — CSV" },
];
const SHIFT_TYPES = [
  { value: "day", label: "Day, 07:00–15:00" },
  { value: "late", label: "Late, 14:00–22:00" },
  { value: "night", label: "Night, 21:00–07:30" },
];

const PAYERS = [
  { value: "nhs_commissioner", label: "NHS commissioner" },
  { value: "private_insurer", label: "Private insurer" },
  { value: "self_pay", label: "Self-pay" },
  { value: "overseas", label: "Overseas visitor" },
];
const BILL_LINE_KINDS = [
  { value: "procedure", label: "Procedure" },
  { value: "bed", label: "Bed day" },
  { value: "medication", label: "Medication" },
  { value: "other", label: "Other" },
];
/* The chase ladder, in the order it escalates. One step per contact; the next
   step is derived from the last one, never stored. */
const CHASE_STEPS = [
  { value: "reminder", label: "Reminder emailed" },
  { value: "statement", label: "Statement sent" },
  { value: "call", label: "Called the payer" },
  { value: "final_notice", label: "Final notice" },
  { value: "referred", label: "Referred to the debt agency" },
];
const CHASE_OUTCOMES = [
  { value: "no_reply", label: "No reply" },
  { value: "awaiting", label: "Awaiting reply" },
  { value: "promised", label: "Payment promised" },
  { value: "queried", label: "Queried the bill" },
];

const PAYMENT_METHODS = [
  { value: "remittance", label: "Commissioner remittance" },
  { value: "bank_transfer", label: "Bank transfer" },
  { value: "card", label: "Card" },
];

const STAFF_ROLES = [
  { value: "clinician", label: "Clinician" },
  { value: "ward_manager", label: "Ward manager" },
  { value: "pharmacist", label: "Pharmacist" },
  { value: "coordinator", label: "Coordinator" },
  { value: "read_only", label: "Read only" },
];

/* What a role may do. The screens read these maps, never a name or a job title. */
const ROLE_CAN_VERIFY_MEDICATION = { clinician: false, ward_manager: false, pharmacist: true, coordinator: false, read_only: false };
const ROLE_CAN_MOVE_ADMISSION = { clinician: true, ward_manager: true, pharmacist: false, coordinator: true, read_only: false };
const ROLE_CAN_RECORD_CONSENT = { clinician: true, ward_manager: false, pharmacist: false, coordinator: false, read_only: false };
const ROLE_CAN_BILL = { clinician: false, ward_manager: true, pharmacist: false, coordinator: true, read_only: false };
const ROLE_CAN_IMPORT = { clinician: false, ward_manager: true, pharmacist: true, coordinator: true, read_only: false };

/* ── Derived flags — computed at render, never stored, clock injected ─────── */
const DAY_MS = 864e5;
const HOUR_MS = 36e5;

const ADMISSION_FLAGS = {
  /* A bed that has been waited on for more than four hours is the hospital's
     own breach clock, not a status. */
  bedWaitBreached: (a, now) => a.status === "awaiting_bed" && now - a.requestedAt > 4 * HOUR_MS,
  /* Fit for discharge yesterday or earlier: the bed is being held. */
  dischargeHeld: (a, now) => a.status === "fit_for_discharge" && now - a.fitAt > DAY_MS,
  lengthOfStayDays: (a, now) => Math.max(0, Math.round(((a.dischargedAt || now) - a.admittedAt) / DAY_MS)),
  overExpectedStay: (a, now) => !a.dischargedAt && ADMISSION_FLAGS.lengthOfStayDays(a, now) > a.expectedStayDays,
};

const INTERVENTION_FLAGS = {
  /* Consent is a record, not a status: a scheduled procedure without it cannot
     start, and that is what the theatre list is asking. */
  consentMissing: (i) => (i.status === "scheduled" || i.status === "requested") && !i.consentAt,
  isToday: (i, now) => !!i.scheduledAt && new Date(i.scheduledAt).toDateString() === new Date(now).toDateString(),
  unscheduled: (i) => i.status === "requested" && !i.scheduledAt,
};

const MEDICATION_FLAGS = {
  doseDue: (o, now) => o.status === "active" && !!o.nextDoseAt && o.nextDoseAt - now <= HOUR_MS,
  doseMissed: (o, now) => o.status === "active" && !!o.nextDoseAt && now - o.nextDoseAt > HOUR_MS,
  awaitingPharmacy: (o) => o.status === "awaiting_verification",
};

const PATIENT_FLAGS = {
  hasAllergyAlert: (p) => (p.allergies || []).length > 0,
  reviewOverdue: (p, now) => p.status === "inpatient" && !!p.lastReviewAt && now - p.lastReviewAt > DAY_MS,
};

/* A shift is understaffed when fewer people are rostered onto it than the ward
   establishment asks for. The establishment is data (fixtures), not an enum, and
   the shortfall is derived at render — never stored. */
const SHIFT_FLAGS = {
  understaffed: (rostered, establishment) => rostered.length < establishment,
  shortfall: (rostered, establishment) => Math.max(0, establishment - rostered.length),
};

/* Overdue is derived from the due date and the balance, never stored: an invoice
   carries a status and any number of flags. */
const INVOICE_FLAGS = {
  overdue: (invoice, balance, now) => balance > 0 && !!invoice.dueAt && invoice.dueAt < now && ["issued", "part_paid", "disputed"].indexOf(invoice.status) > -1,
  ageDays: (invoice, now) => (invoice.issuedAt ? Math.max(0, Math.round((now - invoice.issuedAt) / DAY_MS)) : 0),
  billable: (invoice) => ["issued", "part_paid", "paid", "disputed"].indexOf(invoice.status) > -1,
};

/* Contracts and the prices under them. Both flags are computed at render. */
const CONTRACT_FLAGS = {
  expiringSoon: (contract, now) => contract.status === "live" && !!contract.expiresAt && contract.expiresAt - now <= 30 * DAY_MS,
  daysLeft: (contract, now) => (contract.expiresAt ? Math.round((contract.expiresAt - now) / DAY_MS) : null),
};

/* What the chase ladder says to do next. A disputed invoice is never chased —
   the query is answered first — so it is not in the worklist at all. */
function nextChaseStep(lastStep) {
  if (!lastStep) return CHASE_STEPS[0];
  const index = CHASE_STEPS.findIndex((s) => s.value === lastStep);
  return index > -1 && index < CHASE_STEPS.length - 1 ? CHASE_STEPS[index + 1] : null;
}

function chaseTransitions(row, viewer) {
  const may = ROLE_CAN_BILL[viewer.staffRole];
  const next = nextChaseStep(row.lastStep);
  if (!next) return [];
  const gate = may ? {} : { disabled: true, disabledReason: "Chasing a payer is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  return [Object.assign({ id: "chase", label: next.label, primary: true, step: next.value }, gate)];
}

const FORMULARY_FLAGS = {
  belowReorder: (d) => d.onHand <= d.reorderAt && d.stock !== "discontinued",
};

/* ── Transitions — one function per entity, called by every surface ────────
   The set derives from the current status. A transition that does not apply is
   omitted; one that exists but the signed-in person may not take is disabled
   with the reason stated (AGENTS §1.2). One transition is primary: the record's
   normal path. */
function admissionTransitions(admission, viewer) {
  const may = ROLE_CAN_MOVE_ADMISSION[viewer.staffRole];
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Only a ward manager, a coordinator or a clinician can move an admission. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (admission.status === "awaiting_bed") return [gate({ id: "assign_bed", label: "Assign bed", primary: true }), gate({ id: "transfer_out", label: "Transfer out" })];
  if (admission.status === "on_ward") return [gate({ id: "mark_fit", label: "Mark fit for discharge", primary: true }), gate({ id: "transfer_out", label: "Transfer out" })];
  if (admission.status === "fit_for_discharge") return [gate({ id: "discharge", label: "Discharge", primary: true })];
  return [];
}

function interventionTransitions(intervention, viewer) {
  const consent = ROLE_CAN_RECORD_CONSENT[viewer.staffRole]
    ? {}
    : { disabled: true, disabledReason: "Consent is recorded by the clinician taking it. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  const out = [];
  if (intervention.status === "requested") out.push({ id: "schedule", label: "Schedule", primary: true });
  if (intervention.status === "scheduled") {
    if (!intervention.consentAt) out.push(Object.assign({ id: "record_consent", label: "Record consent", primary: true }, consent));
    else out.push({ id: "start", label: "Start in theatre", primary: true });
  }
  if (intervention.status === "in_theatre") out.push({ id: "to_recovery", label: "Move to recovery", primary: true });
  if (intervention.status === "recovery") out.push({ id: "complete", label: "Complete", primary: true });
  if (["requested", "scheduled"].indexOf(intervention.status) > -1) out.push({ id: "cancel", label: "Cancel procedure" });
  if (intervention.status === "in_theatre") out.push({ id: "abandon", label: "Record as abandoned", destructive: true });
  return out;
}

function medicationTransitions(order, viewer) {
  const verifyGate = ROLE_CAN_VERIFY_MEDICATION[viewer.staffRole]
    ? {}
    : { disabled: true, disabledReason: "Only a pharmacist can verify an order. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." };
  if (order.status === "awaiting_verification") return [Object.assign({ id: "verify", label: "Verify order", primary: true }, verifyGate)];
  if (order.status === "active") return [{ id: "record_dose", label: "Record dose", primary: true }, { id: "hold", label: "Put on hold" }, { id: "stop", label: "Stop course", destructive: true }];
  if (order.status === "on_hold") return [{ id: "resume", label: "Resume", primary: true }, { id: "stop", label: "Stop course", destructive: true }];
  return [];
}

/* Billing transitions. Writing an invoice off is irreversible and opens a
   confirm dialog; everything else applies with an undo toast. Only a ward manager
   or a coordinator may bill (ROLE_CAN_BILL). */
function invoiceTransitions(invoice, viewer, balance) {
  const may = ROLE_CAN_BILL[viewer.staffRole];
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Billing is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (invoice.status === "draft") return [gate({ id: "send_for_coding", label: "Send for coding", primary: true })];
  if (invoice.status === "awaiting_coding") return [gate({ id: "issue", label: "Issue invoice", primary: true })];
  if (invoice.status === "issued" || invoice.status === "part_paid") {
    return [gate({ id: "record_payment", label: "Record payment", primary: true }), gate({ id: "dispute", label: "Mark disputed" }), gate({ id: "write_off", label: "Write off " + (balance != null ? "the balance" : "invoice"), destructive: true })];
  }
  if (invoice.status === "disputed") return [gate({ id: "resolve", label: "Resolve dispute", primary: true }), gate({ id: "write_off", label: "Write off the balance", destructive: true })];
  return [];
}

function transferTransitions(job) {
  if (job.status === "needs_review") return [{ id: "review_rows", label: "Review rows", primary: true }];
  if (job.status === "failed") return [{ id: "retry_job", label: "Run again", primary: true }];
  if (job.status === "completed" && job.kind === "export") return [{ id: "download_file", label: "Download", primary: true }];
  return [];
}

/* Access to the badge system. Suspending is consequential and reversible, so it
   raises an undo toast rather than a confirm dialog; nothing here deletes a
   person, because an employee who has left keeps their record. */
function staffTransitions(person, viewer) {
  const may = ROLE_CAN_MOVE_ADMISSION[viewer.staffRole] && viewer.staffRole !== "clinician";
  const gate = (t) => Object.assign({}, t, may ? {} : { disabled: true, disabledReason: "Changing somebody's access is a ward manager or coordinator job. Your role is " + labelOf(STAFF_ROLES, viewer.staffRole) + "." });
  if (person.id === viewer.id) return [];
  if (person.status === "onboarding") return [gate({ id: "activate", label: "Mark active", primary: true })];
  if (person.status === "active") return [gate({ id: "suspend", label: "Suspend access" })];
  if (person.status === "suspended") return [gate({ id: "restore", label: "Restore access", primary: true })];
  if (person.status === "on_leave") return [gate({ id: "returned", label: "Mark returned", primary: true })];
  return [];
}

/* Registry — what a status field means, for the reader of this file. */
const STATUS_FIELDS = [
  { entity: "patient", field: "status", means: "where this person is being seen", values: PATIENT_STATUS },
  { entity: "admission", field: "status", means: "where one episode of ward care has got to", values: ADMISSION_STATUS },
  { entity: "intervention", field: "status", means: "where one procedure has got to", values: INTERVENTION_STATUS },
  { entity: "medicationOrder", field: "status", means: "whether a prescription may be given", values: MEDICATION_STATUS },
  { entity: "formularyLine", field: "stock", means: "whether the pharmacy can dispense it", values: STOCK_STATUS },
  { entity: "staff", field: "status", means: "whether this employee is working", values: STAFF_STATUS },
  { entity: "transfer", field: "status", means: "where an import or export job has got to", values: TRANSFER_STATUS },
  { entity: "invoice", field: "status", means: "where the bill for one episode of care has got to", values: INVOICE_STATUS },
  { entity: "payment", field: "status", means: "whether the money has arrived", values: PAYMENT_STATUS },
  { entity: "contract", field: "status", means: "whether a payer's price list still applies", values: CONTRACT_STATUS },
];

/* Coverage — values with no fixture yet: patient.deceased, intervention.abandoned,
   staff.left, transfer.queued, invoice.written_off, payment.refunded, contract.expired. They are reachable in the running product (an
   abandoned procedure and a queued job are both one commit away) but no seeded
   record carries them, so no screen claims a count for them. */

Object.assign(window, {
  PATIENT_STATUS, ADMISSION_STATUS, INTERVENTION_STATUS, MEDICATION_STATUS, STOCK_STATUS, STAFF_STATUS, TRANSFER_STATUS, INVOICE_STATUS, PAYMENT_STATUS, CONTRACT_STATUS,
  WARDS, SPECIALTIES, JOB_FAMILIES, DRUG_ROUTES, THEATRES, PRIORITIES, ADMISSION_SOURCES, TRANSFER_KINDS, IMPORT_SOURCES, STAFF_ROLES, SHIFT_TYPES, PAYERS, BILL_LINE_KINDS, PAYMENT_METHODS, CHASE_STEPS, CHASE_OUTCOMES,
  ROLE_CAN_VERIFY_MEDICATION, ROLE_CAN_MOVE_ADMISSION, ROLE_CAN_RECORD_CONSENT, ROLE_CAN_IMPORT, ROLE_CAN_BILL,
  ADMISSION_FLAGS, INTERVENTION_FLAGS, MEDICATION_FLAGS, PATIENT_FLAGS, FORMULARY_FLAGS, SHIFT_FLAGS, INVOICE_FLAGS, CONTRACT_FLAGS,
  admissionTransitions, interventionTransitions, medicationTransitions, transferTransitions, staffTransitions, invoiceTransitions, chaseTransitions, nextChaseStep,
  STATUS_FIELDS, DAY_MS, HOUR_MS,
});
