/**
 * Fabricated demo data, so that a clone of this repo has something on it.
 *
 * **Everything in this file is invented.** The companies do not exist, have
 * never existed, and are named the way they are — Wobbegong, Nonsuch, Pennyfarthing
 * — specifically so that nobody can mistake one for a real business. The
 * coordinates were typed by hand from a general sense of where San Francisco's
 * neighbourhoods are; none of them is anybody's address, and none of them came
 * out of a geocoder.
 *
 * That last point is the licence rule and not a stylistic preference. No real
 * company data ships in this repo, for two reasons that happen to want the same
 * thing. The privacy one is obvious: pipeline status — who is talking to whom,
 * who said no — is private, and a public repo is the wrong place for it. The
 * licence one is sharper and is the subject of ARCHITECTURE.md §3.2 and
 * CONTRACT.md §8: real positions are *geocoded*, and a geocoder built on
 * OpenStreetMap returns ODbL data. Serving a snapshot of those coordinates is
 * Publicly Using a Derivative Database, which drags share-alike onto everything
 * served alongside it, whether or not the rows live in the repo. So real markers
 * arrive at runtime over the API — see `http.ts` — carrying a provenance field
 * the server checks, and the repo itself ships this: fiction, which owes nobody
 * anything.
 *
 * The demo is worth having anyway. A map with no pins on it teaches nobody what
 * the thing is for, and half the interesting behaviour in `markers.ts` — the
 * ghost treatment for a marker that has no real position yet — is invisible
 * without data that exercises it.
 */

import { regionOf, syntheticRoutes, type SimRoute } from "../engine/flights.ts";
import type { Presence } from "../interiors/types.ts";
import type { City, Marker, MarkerPalette } from "../engine/types.ts";

/**
 * A small pipeline, as colours.
 *
 * Five states is about the fewest that still shows why `colorKey` is opaque to
 * the engine: none of these words means anything to `markers.ts`, which looks
 * the key up here and draws whatever it finds. A public sector map would supply
 * an entirely different set against the same renderer.
 */
export const SAMPLE_PALETTE: MarkerPalette = {
  watching: 0x7f8b99,
  applied: 0x4f9cf2,
  talking: 0x3fbf9a,
  offer: 0xf2b134,
  closed: 0xd2544f,
};

/**
 * Where the unplaced markers float: out over the bay, east of the Ferry
 * Building, in a short arc.
 *
 * A marker with `located: false` has no address yet, and the honest thing to do
 * with a position you do not have is to not pretend you have one. Open water is
 * the clearest way to say that on a map — nothing is there, the pin visibly
 * stands on nothing, and it cannot be misread as a building. `markers.ts` gives
 * these a different silhouette and a lower opacity as well, so the tell does not
 * rest on placement alone.
 */
const UNPLACED_LNG = -122.3665;

/**
 * Twenty-two invented companies across San Francisco.
 *
 * Placed by neighbourhood rather than by street: Hayes Valley and SoMa are
 * crowded because that is the fact the map is usually drawing attention to, the
 * Bayview and the Richmond have one each, and three have no position at all.
 * The clustering is the point — a marker layer that only ever gets evenly
 * scattered test data hides every overlap problem it has.
 */
export const SAMPLE_MARKERS: Marker[] = [
  // Hayes Valley and the bowl below Buena Vista.
  {
    id: "sample-thimbleway",
    label: "Thimbleway Systems",
    colorKey: "talking",
    lat: 37.7768,
    lng: -122.4243,
    located: true,
    blurb: "Invented. Model-serving, allegedly, in a Victorian with bad wiring.",
  },
  {
    id: "sample-nonsuch",
    label: "Nonsuch Cartography Co.",
    colorKey: "applied",
    lat: 37.7752,
    lng: -122.4262,
    located: true,
    blurb: "Invented. Maps of places that are not there.",
  },
  {
    id: "sample-marmalade",
    label: "Marmalade Interchange",
    colorKey: "watching",
    lat: 37.7781,
    lng: -122.4218,
    located: true,
    blurb: "Invented. Moves data between two formats nobody uses.",
  },

  // SoMa, where the grid turns forty-six degrees.
  {
    id: "sample-kettle-anvil",
    label: "Kettle & Anvil Compute",
    colorKey: "offer",
    lat: 37.7805,
    lng: -122.4051,
    located: true,
    blurb: "Invented. Sells the shovels, or claims to.",
  },
  {
    id: "sample-ninth-pelican",
    label: "Ninth Pelican Labs",
    colorKey: "applied",
    lat: 37.7784,
    lng: -122.4009,
    located: true,
    blurb: "Invented. There were never eight others.",
  },
  {
    id: "sample-brassbound",
    label: "Brassbound Telemetry",
    colorKey: "closed",
    lat: 37.7822,
    lng: -122.4074,
    located: true,
    blurb: "Invented. Went quiet after the second call.",
  },

  // The Financial District and Jackson Square.
  {
    id: "sample-grimsby-doone",
    label: "Grimsby & Doone Photonics",
    colorKey: "watching",
    lat: 37.7941,
    lng: -122.4008,
    located: true,
    blurb: "Invented. Two surnames and a laser.",
  },
  {
    id: "sample-tugboat",
    label: "Tugboat Actuarial",
    colorKey: "applied",
    lat: 37.7958,
    lng: -122.4032,
    located: true,
    blurb: "Invented. Insurance for things that have already happened.",
  },

  // Mission Bay: landfill, then biotech.
  {
    id: "sample-fogbank",
    label: "Fogbank Freight",
    colorKey: "talking",
    lat: 37.7709,
    lng: -122.3918,
    located: true,
    blurb: "Invented. Logistics, in a building younger than most of the staff.",
  },
  {
    id: "sample-bittern",
    label: "Bittern & Sons Biologics",
    colorKey: "watching",
    lat: 37.7688,
    lng: -122.3894,
    located: true,
    blurb: "Invented. No sons.",
  },

  // The Mission, flat and sunny.
  {
    id: "sample-unlikely-weather",
    label: "Bureau of Unlikely Weather",
    colorKey: "offer",
    lat: 37.7602,
    lng: -122.4151,
    located: true,
    blurb: "Invented. Forecasts nobody asked for.",
  },
  {
    id: "sample-perpetual-bagel",
    label: "Perpetual Bagel Works",
    colorKey: "closed",
    lat: 37.7574,
    lng: -122.4192,
    located: true,
    blurb: "Invented. The name was the whole pitch.",
  },

  // Potrero Hill and Dogpatch, the old industrial edge.
  {
    id: "sample-wobbegong",
    label: "Wobbegong Robotics",
    colorKey: "talking",
    lat: 37.7589,
    lng: -122.4002,
    located: true,
    blurb: "Invented. Named after a carpet shark, for reasons never explained.",
  },
  {
    id: "sample-sourdough-semi",
    label: "Sourdough Semiconductor",
    colorKey: "applied",
    lat: 37.7597,
    lng: -122.3881,
    located: true,
    blurb: "Invented. A fab in a city with no fabs.",
  },
  {
    id: "sample-pennyfarthing",
    label: "Pennyfarthing Power",
    colorKey: "watching",
    lat: 37.7564,
    lng: -122.3973,
    located: true,
    blurb: "Invented. Batteries, uphill.",
  },

  // The north side and the hills.
  {
    id: "sample-lamplighter",
    label: "Lamplighter Aerostatics",
    colorKey: "watching",
    lat: 37.8004,
    lng: -122.4086,
    located: true,
    blurb: "Invented. Airships, which are always about to come back.",
  },
  {
    id: "sample-halfpenny",
    label: "Halfpenny Optics",
    colorKey: "applied",
    lat: 37.7929,
    lng: -122.4147,
    located: true,
    blurb: "Invented. Lenses, four hundred feet above the water they look at.",
  },

  // One each in the parts of the city the map usually forgets.
  {
    id: "sample-cormorant",
    label: "Cormorant Freight Systems",
    colorKey: "talking",
    lat: 37.7357,
    lng: -122.3908,
    located: true,
    blurb: "Invented. The only pin south of Islais Creek, which is the point.",
  },
  {
    id: "sample-tidewrack",
    label: "Tidewrack Instruments",
    colorKey: "watching",
    lat: 37.7802,
    lng: -122.4638,
    located: true,
    blurb: "Invented. Sensors, in the fog, on purpose.",
  },

  // Three with no position yet. See `UNPLACED_LNG`.
  {
    id: "sample-quibble",
    label: "Quibble Quantum",
    colorKey: "applied",
    lat: 37.7965,
    lng: UNPLACED_LNG,
    located: false,
    blurb: "Invented, and unplaced: no address on file, so the map does not invent one.",
  },
  {
    id: "sample-antelope-foundry",
    label: "Antelope Foundry",
    colorKey: "watching",
    lat: 37.7905,
    lng: UNPLACED_LNG,
    located: false,
    blurb: "Invented, and unplaced.",
  },
  {
    id: "sample-mudlark",
    label: "Mudlark Instruments",
    colorKey: "closed",
    lat: 37.7845,
    lng: UNPLACED_LNG,
    located: false,
    blurb: "Invented, and unplaced.",
  },
];

/**
 * Sample traffic over the Bay Area, for when the API is not there to send a
 * flight plan.
 *
 * The corridors are roughly the real ones — arrivals down the peninsula from
 * the north, departures turning out over the Pacific, a slow light aircraft
 * crossing the bay — because that is what makes the sky read as this city's sky
 * rather than as random motion. The callsigns are not: no real operator uses
 * these prefixes, which keeps a demo from looking like a feed of actual
 * traffic. Nothing here is observed, and `flights.ts` explains at length why
 * this project ships a simulator instead of a client for somebody's live data.
 *
 * The unqualified name is a leftover and is kept because the app imports it.
 * Every leg in it is over San Francisco, which is only the right answer for one
 * of the two cities in this build; `sampleRoutesFor` is the entry point that
 * knows the difference.
 */
export const SAMPLE_ROUTES: SimRoute[] = [
  { callsign: "NIMBUS 4", from: [37.95, -122.36], to: [37.66, -122.4], fromAlt: 2400, toAlt: 500, duration: 190 },
  { callsign: "NIMBUS 17", from: [37.93, -122.31], to: [37.65, -122.38], fromAlt: 2100, toAlt: 450, duration: 210 },
  { callsign: "PELICAN 2", from: [37.64, -122.39], to: [37.9, -122.62], fromAlt: 700, toAlt: 5200, duration: 165 },
  { callsign: "PELICAN 31", from: [37.7, -122.21], to: [37.88, -122.55], fromAlt: 1800, toAlt: 6100, duration: 230 },
  { callsign: "CORMORANT 8", from: [37.62, -122.6], to: [37.95, -122.28], fromAlt: 6800, toAlt: 8200, duration: 260 },
  { callsign: "KESTREL 5", from: [37.83, -122.56], to: [37.7, -122.22], fromAlt: 1100, toAlt: 1300, duration: 300 },
  { callsign: "NIMBUS 40", from: [37.96, -122.48], to: [37.63, -122.36], fromAlt: 3100, toAlt: 600, duration: 205 },
];

/**
 * Sample traffic over the Los Angeles basin.
 *
 * The same idea as `SAMPLE_ROUTES` and it exists because that one was being
 * flown over both cities: the SoCal board would come up with a sky whose every
 * aircraft was nearly six hundred kilometres north of it, off the edge of the world
 * and therefore invisible. A city that renders an empty sky looks like a city
 * whose flight layer failed.
 *
 * Again the geography is roughly right and the callsigns are invented. LAX runs
 * west almost always, so the arrivals here come in from the east over the
 * basin and the departures go out over the water before turning; Burbank sits
 * up the valley behind the hills, and the light aircraft is following the coast
 * because in this basin that is what they do.
 */
export const SAMPLE_SOCAL_ROUTES: SimRoute[] = [
  { callsign: "CONDOR 6", from: [34.02, -117.45], to: [33.945, -118.36], fromAlt: 3500, toAlt: 400, duration: 330 },
  { callsign: "CONDOR 21", from: [33.99, -117.29], to: [33.94, -118.33], fromAlt: 4100, toAlt: 450, duration: 360 },
  { callsign: "AVOCET 12", from: [33.945, -118.42], to: [34.15, -118.84], fromAlt: 500, toAlt: 5200, duration: 190 },
  { callsign: "AVOCET 30", from: [33.68, -117.87], to: [33.34, -118.4], fromAlt: 600, toAlt: 4800, duration: 210 },
  { callsign: "CURLEW 3", from: [33.36, -117.3], to: [33.69, -117.86], fromAlt: 4200, toAlt: 500, duration: 235 },
  { callsign: "TOWHEE 9", from: [34.33, -118.82], to: [34.2, -118.36], fromAlt: 3000, toAlt: 450, duration: 175 },
  { callsign: "CONDOR 44", from: [34.36, -118.66], to: [33.32, -117.28], fromAlt: 9200, toAlt: 9800, duration: 300 },
  { callsign: "SANDPIPER 4", from: [33.6, -118.02], to: [34.03, -118.62], fromAlt: 900, toAlt: 1100, duration: 330 },
];

/**
 * The right sample sky for a city, and something defensible for a city nobody
 * has drawn one for.
 *
 * Keyed on `city.id` rather than on position because the hand-placed corridors
 * are the whole value here: knowing that arrivals come down the peninsula and
 * that LAX departs to the west is knowledge about two named places, and there
 * is no way to derive it from a bounding box. What *can* be derived is legs
 * that are at least in the right region, which is what `syntheticRoutes` does
 * and what any third city gets until somebody sits down with a chart.
 */
export function sampleRoutesFor(city: Pick<City, "id" | "center" | "bounds">): SimRoute[] {
  if (city.id === "sf") return SAMPLE_ROUTES;
  if (city.id === "socal") return SAMPLE_SOCAL_ROUTES;
  return syntheticRoutes(regionOf(city));
}

// ---- The office ------------------------------------------------------------

/**
 * A fabricated roster for the reference office, so that "sign in" has something
 * to show in a clone with no server behind it.
 *
 * **Every person here is invented**, by the same rule and for the same reason as
 * the companies above: the names are deliberately unmistakable for anyone's
 * — Quillon, Marchetti, Oyelaran-Pike — and none of them works anywhere. Real
 * occupancy is the most private thing this project touches. It says who was at
 * their desk and, by omission, who was not, which is an attendance record; it
 * arrives at runtime over an authenticated endpoint (`routes/presence.ts`) and
 * it never, ever lands in this repo.
 *
 * The demo is worth having for the same reason the markers are. An empty
 * building teaches nobody what the office is *for*, and the entire presence
 * layer — the figures, the seat binding, the hover card that names somebody —
 * is invisible without data that exercises it. It also makes the tier honest
 * locally: `access.ts` hands a clone `member`, and a member who is shown the
 * same empty room as a stranger has been told the tier means something when it
 * does not.
 *
 * The seat ids are real ids from `offices/lumbridge-hq.ts` and have to be: a
 * presence whose seat is not in the plan is dropped by `presence.ts`, silently
 * and correctly, so a typo here is an invisible absence rather than an error.
 * They are also spread deliberately — two meeting rooms mid-session, a full
 * bench, a half-empty one, three focus booths of which one is taken — because a
 * floor where every desk is occupied reads as a texture rather than as people.
 */
export const SAMPLE_PRESENCE: Presence[] = [
  // Reception, and someone waiting.
  { id: "p-01", seatId: "reception-01", label: "Wren Abaddon", colorKey: "in" },
  { id: "p-02", seatId: "lobby-01", label: "Visitor", colorKey: "guest" },

  // The eng bench, full along the window row and thinning behind it.
  { id: "p-03", seatId: "eng-01", label: "Tobias Quillon", colorKey: "in" },
  { id: "p-04", seatId: "eng-02", label: "Ines Marchetti", colorKey: "in" },
  { id: "p-05", seatId: "eng-03", label: "Dara Oyelaran-Pike", colorKey: "in" },
  { id: "p-06", seatId: "eng-04", label: "Sunniva Holt", colorKey: "in" },
  { id: "p-07", seatId: "eng-05", label: "Casimir Vane", colorKey: "focus" },
  { id: "p-08", seatId: "eng-06", label: "Perpetua Nkemelu", colorKey: "in" },
  { id: "p-09", seatId: "eng-08", label: "Rafferty Osgood", colorKey: "in" },
  { id: "p-10", seatId: "eng-11", label: "Marisol Thibault", colorKey: "focus" },

  // Ops, about half in.
  { id: "p-11", seatId: "ops-01", label: "Anouk Sterling", colorKey: "in" },
  { id: "p-12", seatId: "ops-02", label: "Emeka Farrow", colorKey: "in" },
  { id: "p-13", seatId: "ops-05", label: "Bettina Kovač", colorKey: "in" },
  { id: "p-14", seatId: "ops-07", label: "Xiomara Belfry", colorKey: "focus" },
  { id: "p-15", seatId: "ops-10", label: "Aurelio Pinsent", colorKey: "in" },

  // Design.
  { id: "p-16", seatId: "design-01", label: "Halcyon Reeve", colorKey: "in" },
  { id: "p-17", seatId: "design-03", label: "Zephyrine Mbeki", colorKey: "in" },
  { id: "p-18", seatId: "design-04", label: "Ptolemy Sandoval", colorKey: "in" },

  // One focus booth of three, which is the honest ratio.
  { id: "p-19", seatId: "booth-02", label: "Ottoline Grieve", colorKey: "focus" },

  // Alcatraz, mid-session — six of ten, which is what a real meeting looks like.
  { id: "p-20", seatId: "alcatraz-01", label: "Cormac Dellwood", colorKey: "meeting" },
  { id: "p-21", seatId: "alcatraz-02", label: "Solveig Amankwah", colorKey: "meeting" },
  { id: "p-22", seatId: "alcatraz-03", label: "Ignatius Pell", colorKey: "meeting" },
  { id: "p-23", seatId: "alcatraz-06", label: "Rosalind Achebe", colorKey: "meeting" },
  { id: "p-24", seatId: "alcatraz-07", label: "Vasco Underhill", colorKey: "meeting" },
  { id: "p-25", seatId: "alcatraz-08", label: "Clementine Roux", colorKey: "meeting" },
];

/**
 * `colorKey` -> colour, for the fabricated roster.
 *
 * Four states and no more, because the palette is the legend whether or not one
 * is drawn: a viewer can tell "at their desk" from "heads-down" from "in a
 * meeting" at a glance, and a fifth shade would be a distinction nobody could
 * name. Amber for focus is the interface's own accent doing what it does
 * everywhere else in this app — marking the thing you are meant to notice —
 * and the guest green is the only hue that is not in the office's own materials,
 * because a visitor is the one person in the building who is not part of it.
 */
export const SAMPLE_PRESENCE_PALETTE: Record<string, number> = {
  in: 0x6f9ec4,
  focus: 0xf2b134,
  meeting: 0xc4796f,
  guest: 0x7fb886,
};


// ---- The office, over a day ------------------------------------------------

/**
 * The fabricated roster as it stands at one particular instant: who is in the
 * building, and **which seat each of them is in right now**.
 *
 * `SAMPLE_PRESENCE` is a photograph of one good morning, and a photograph was
 * the whole truth for as long as the office had no clock. It is not any more.
 * The room is lit by the same sun the city is, the house lights come up as that
 * sun goes down, and the robots go on walking the floor after dark — so a
 * building with all twenty-five of these people still sitting in it at one in
 * the morning was the least believable thing in it. Everything else in the scene
 * had learned what time it was; the people had not.
 *
 * The first version of this function fixed half of that: it thinned the roster
 * by hour, so the floor filled and emptied. The other half is that everybody it
 * kept was pinned to one seat from dawn to dusk, and a floor where the same
 * twenty-five silhouettes sit in the same twenty-five chairs for eleven hours is
 * a texture rather than a place. A real floor has people in the meeting rooms
 * when there is a meeting, at the kitchen island at lunch, and in the lounge in
 * the afternoon. So this now answers a harder question than "are you in": it
 * answers "where are you", and a `Presence` binds to a `seatId`, so the whole
 * feature is **choosing seat ids**. Nothing here invents a position; it could
 * not if it wanted to, which is the point of the seat-id split and is argued at
 * length on `Presence` in `interiors/types.ts`.
 *
 * This is deliberately a **pure function of an instant the caller supplies**,
 * not a ticking source. The app already has exactly one clock — the wall clock,
 * or whatever the godmode scrubber has overridden it with — and a module that
 * read `new Date()` for itself would be a second one, correct until the moment
 * somebody drags the scrubber to midnight and finds the desks still full. That
 * is the bug this exists to fix, so it is not one to reintroduce one layer down.
 *
 * ### What replaced "always a subset of SAMPLE_PRESENCE"
 *
 * It used to be one. Returning a filtered `SAMPLE_PRESENCE` bought two hard
 * properties for nothing — never more people than seats, and never two people in
 * one seat — because the input already had one person per seat and filtering
 * cannot create a collision. **That is gone**, and it has to be: a person who is
 * at their desk at eleven and in Alcatraz at ten is by definition not always at
 * the seat the photograph gives them. The result is still `Presence[]` and the
 * exported signature has not moved, but the objects in it now carry seat ids
 * that are nowhere in `SAMPLE_PRESENCE`, and a `colorKey` that can differ from
 * the one there.
 *
 * The two properties are bought back a different way, and it is worth being
 * precise because the second one is the easy bug here:
 *
 * - **One person, one seat.** The loop below emits at most one `Presence` per
 *   roster entry, ever. A person is either inside exactly one booking or at
 *   their own desk, never both, and the `bookings` list for one person is
 *   audited at import for self-overlap. Twenty-five people therefore produce at
 *   most twenty-five entries, at twenty-five distinct seats — so the roster can
 *   never name more people than the floor can hold.
 * - **One seat, one person.** This is not free and is not derivable from the
 *   per-person view, because it is a statement about *pairs* of people. It comes
 *   from `BOOKINGS` being a hand-written table rather than a hash: within one
 *   booking the room seats are the object's keys, so a repeat is a **compile
 *   error** (TS1117), and across bookings that are live at the same moment the
 *   import-time audit at the bottom of this file checks every pair. A booking
 *   may also never name a seat that somebody sits at all day; that is checked
 *   too, since sending a visitor to `eng-03` would put two people in a chair
 *   without either booking overlapping the other.
 *
 * `SAMPLE_PRESENCE` itself is untouched and still exported — it is what a caller
 * with no clock, and every test written before this existed, is entitled to keep
 * getting.
 *
 * ### What it costs
 *
 * Called on every clock tick — `updateSun` in `main.ts` runs on a once-a-minute wall-clock interval, and on every godmode scrub
 * — and on every frame of a godmode scrub, so it is worth stating rather than
 * hoping.
 *
 * Per call: one pass over the twenty-five-entry roster; for each person a walk
 * of their own bookings which stops at the first one starting after `t`; and for
 * whoever is not in a booking, four number comparisons against a day that was
 * computed once. Summed over the roster the booking walk is bounded by the whole
 * table — fifty seat assignments across fifteen bookings, nobody holding more
 * than four — so the worst case is a couple of hundred number comparisons.
 * **Nothing is allocated except the result array**: every `Presence` this can
 * return, at a desk or in a room, is built once at import and handed out by
 * reference, exactly as the old filter handed out `SAMPLE_PRESENCE`'s own
 * objects. There is no hashing on this path at all; the hashes run once per seat
 * while `ROSTER` is built.
 *
 * Import-time work is the roster build and the audit, together a couple of
 * thousand comparisons over those fifty entries, once, and neither of them on
 * any frame.
 *
 * ### No weekend, on purpose
 *
 * A Sunday is not modelled and the day of the week is never read. This is a
 * judgement call and it is the one worth arguing with: a real building is nearly
 * empty all weekend, and modelling that would be more honest than not.
 *
 * It is also, roughly two days in seven, a first impression of an empty floor
 * for somebody who has just walked into the office for the first time — and this
 * file's own header is already clear that an empty building teaches nobody what
 * the office is for. An empty room at one in the morning is read as "it is one
 * in the morning". An empty room at eleven on a Sunday, by a visitor who has not
 * thought about what day it is, is read as a floor that failed to load. The
 * overnight curve alone buys the thing that was actually missing — a building
 * that visibly lives on a clock — and it does it at every hour of every day
 * instead of costing us two of them. A deployment that wants weekends can filter
 * this result by `when.getDay()` in four lines; a demo that has already been
 * dismissed cannot be got back.
 *
 * ### Which clock
 *
 * `when.getHours()` — the viewer's local time, matching `#clock`, the godmode
 * scrubber and every other reading of a `Date` in this app. That is right for
 * the flagship case and imperfect for one that exists: the sun in the windows is
 * computed from the site's latitude and longitude while the roster here follows
 * the viewer, so a visitor in another time zone gets a floor that fills up at
 * their nine o'clock under a sky that belongs to the building's. Fixing that
 * properly needs a real time zone on `OfficeSite`, which is a change to the
 * office contract and not to a demo adapter. Guessing one from `site.lng` is
 * *not* the fix and was rejected: a longitude gives you mean solar time, and it
 * knows nothing about daylight saving. Until there is a real zone, a caller that
 * genuinely knows the building's can shift the `Date` it passes.
 */
export function samplePresenceAt(when: Date): Presence[] {
  const t = when.getHours() * 60 + when.getMinutes() + when.getSeconds() / 60;
  const out: Presence[] = [];
  for (const person of ROSTER) {
    // A booking wins outright. Somebody in a meeting is not also at their desk,
    // and the order of these two branches is the entire "one person, one seat"
    // guarantee for a single roster entry.
    const booked = bookedAt(person, t);
    if (booked) {
      out.push(booked);
      continue;
    }
    if (person.desk && isIn(person.desk.day, t)) out.push(person.desk.at);
  }
  return out;
}

/**
 * Office-aware fallback for shipped environments.
 *
 * The legacy roster above is valuable as a renderer fixture, but its seat ids
 * describe the original reference tower and therefore cannot truthfully appear
 * in either active HQ. A missing presence service now means what it says: no
 * fabricated people. Keeping this resolver keyed by the stable office id gives
 * self-hosters and future sample packs an explicit seam without ever treating a
 * shiny room as evidence of employees.
 */
export function samplePresenceForOfficeAt(_officeId: string, _when: Date): Presence[] {
  return [];
}

/**
 * Minutes since local midnight, as a literal. Every window in this section is
 * written with it so that the source reads as a timetable.
 */
function at(hour: number, minute: number): number {
  return hour * 60 + minute;
}

/**
 * Where the six people the photograph catches mid-meeting actually sit.
 *
 * `SAMPLE_PRESENCE` is a snapshot at one moment, so for most of the roster the
 * seat it gives somebody *is* their desk and this table has nothing to say. For
 * the six in Alcatraz it is not: the snapshot caught them in a room they were
 * booked into, and a model of a whole day needs to know where they go when the
 * meeting ends. "Somebody in Alcatraz at 10:00 is at their desk at 11:00" is the
 * requirement, and this is the line that says which desk.
 *
 * The alternative was to move them to desks in `SAMPLE_PRESENCE` itself and let
 * the schedule put them back in the room. That was rejected because the snapshot
 * is a deliberately varied fixture — its own comment lists the spread as the
 * reason it exists — and a `SAMPLE_PRESENCE` in which everybody sits at a desk
 * exercises exactly one kind of seat. Six lines here cost less than that.
 *
 * Keyed on the snapshot's seat rather than on the person id for the same reason
 * `deskDay` is seeded from the seat: the seat is the address, and renaming
 * Cormac Dellwood should not move anybody's desk. The desks chosen are free ones
 * — nobody else in the roster sits at a `sales-*` station, at `design-02`,
 * `ops-04` or `eng-07` — which the audit below rechecks, because two people
 * sharing a home desk would be a permanent double occupancy rather than an
 * occasional one.
 */
const DESKS: Record<string, string> = {
  "alcatraz-01": "sales-01",
  "alcatraz-02": "sales-02",
  "alcatraz-03": "sales-03",
  "alcatraz-06": "design-02",
  "alcatraz-07": "ops-04",
  "alcatraz-08": "eng-07",
};

/**
 * The three shifts that are *not* hashed, and the reason the building is never
 * empty during working hours.
 *
 * Everyone else's day is drawn from a hash, and a hash makes no promises: there
 * is a seed for which every single person arrives at ten and the floor is bare
 * at half past nine. Rather than clamp the answer afterwards — a clamp is a
 * decision that changes as the clock moves, which is exactly the popping this
 * function is supposed to be free of — three people are given fixed hours and
 * the guarantee is read off them.
 *
 * They overlap in a deliberate chain: the early ops shift is in before anyone
 * and out mid-afternoon, reception opens the front desk before it hands over,
 * and one engineer works late and locks up. Each one starts before the previous
 * one leaves, so their union is **continuous from 06:15 to 22:40**, and the
 * floor cannot be empty inside it. Outside it the building genuinely does empty,
 * which is the whole point of the change: at two in the morning there is nobody
 * here, and the house lights and the robots are what the night shift looks like.
 *
 * Anchors take **no lunch break**, unlike everybody else. A break in an anchor
 * is a break in the guarantee, and three staggered shifts that all happen to
 * step out at half past twelve is precisely the accident that would put an empty
 * floor on screen at the one hour nobody would think to check.
 *
 * A booking does **not** break the guarantee, and this is the thing to check
 * before adding one. All three anchors are in `BOOKINGS` — the ops shift at the
 * morning stand-up, reception in Bernal at twenty to four, the late engineer in
 * Alcatraz and then alone in the kitchen at quarter past seven — but a booking
 * *relocates* somebody, it never removes them. The claim above is that the floor
 * is not empty, not that any particular chair is filled, so an anchor who is in
 * a meeting still holds it up.
 *
 * The keys are desk seat ids and they must belong to somebody in the roster; see
 * the audit below, which is there because renaming a seat would otherwise
 * dissolve the guarantee without a word.
 */
const PRESENCE_ANCHORS: Record<string, { from: number; to: number }> = {
  "ops-01": { from: at(6, 15), to: at(15, 5) },
  "reception-01": { from: at(7, 10), to: at(18, 50) },
  "eng-11": { from: at(11, 20), to: at(22, 40) },
};

/**
 * One room, booked, with a named person in each chair.
 *
 * `seats` is keyed on the **room seat** and valued with the occupant's **desk
 * seat**, which is the opposite of how it reads out loud and is that way round
 * on purpose: object keys are unique, so a table that puts two people in
 * `alcatraz-03` does not compile (TS1117). The single easiest mistake in this
 * whole feature is caught by `tsc` rather than by anybody noticing two figures
 * z-fighting in one chair.
 */
interface Booking {
  /** For the audit's messages. Nothing reads it at runtime. */
  room: string;
  from: number;
  to: number;
  /** The `colorKey` an occupant wears while they are here. See `isGuest`. */
  colorKey: string;
  /** Room seat -> the desk seat of whoever is in it. */
  seats: Record<string, string>;
}

/**
 * The day's diary: fifteen bookings, fifty chairs filled, hand-written.
 *
 * ### Why this is a table and not code
 *
 * The obvious shape is generative — hash a person against a session the way
 * `deskDay` hashes their arrival, and let the room fill up. That is what the
 * previous version did for its two Alcatraz sessions, and it needed a designated
 * chair-of-the-meeting to stop the room drawing empty on an unlucky seed. Scaled
 * up to the nine places this table books, it fails for reasons that are not
 * about taste:
 *
 * - **Collisions become a search problem.** Two independently-hashed sessions
 *   that overlap in time can pick the same room, and two independently-hashed
 *   people can pick the same chair. Fixing that at read time means resolving
 *   conflicts against whoever else happens to be present — and a resolution that
 *   depends on the rest of the roster is a decision that changes as the clock
 *   moves. That is the popping this whole section is built to avoid: person B
 *   would slide from `kitchen-02` to `kitchen-03` at the instant person A walked
 *   in, for no reason a viewer could see.
 * - **A hash cannot make a meeting look like a meeting.** Six named people round
 *   one table for fifty minutes is the thing worth drawing. A per-person
 *   probability will happily put one person alone in a ten-seat board room —
 *   that is precisely the failure the old chair-of-the-meeting rule was there to
 *   paper over — and every fix for it, a threshold, a floor on attendance, a
 *   quorum, is another coefficient that adds up to writing the guest list badly.
 * - **The invariant becomes visible.** Read down `seats` and you can see that
 *   nobody is in two rooms; that is not a property you can read off a hash at
 *   all, only test for.
 *
 * So the diary is data. It is also the honest shape for this file, whose header
 * has said from the first line that everything in it is invented: a day somebody
 * wrote down is more obviously fiction than a day a hash produced, not less.
 *
 * ### The shape of the day
 *
 * Ops stand up at twenty past nine, five of six round Bernal. The big review
 * fills eight of Alcatraz's ten from five to ten until quarter to eleven. A
 * visitor waits in reception, is taken into Bolinas, and leaves. Somebody sits
 * in Lands End with the door shut for an hour — one person in a two-seat quiet
 * room is what a quiet room looks like, and it is the one place on this floor
 * where a single occupant is the correct picture rather than a failed meeting.
 * Lunch is two sittings, because thirteen of these seat ids draw a hashed lunch
 * window and the kitchen has five seats. Ten of the thirteen eat here; the other
 * three go out and are simply gone, which is the truth about a lunch hour and is
 * why the building thins at half past twelve without emptying. The afternoon is
 * a lounge, a
 * second Alcatraz session, a Bernal, two focus booths and a second visitor. The
 * late engineer eats alone in the kitchen at quarter past seven.
 *
 * ### Constraints an edit has to keep
 *
 * All of these are checked at import, so breaking one is a line in the console
 * and not a mystery on screen:
 *
 * 1. Every value is the desk seat of somebody in `ROSTER`.
 * 2. No room seat is anybody's desk. Bookings move people to rooms, never into
 *    another person's chair.
 * 3. Bookings that are live at the same instant share neither a room seat nor a
 *    person.
 *
 * There is a fourth that is not checked and does not need to be: every booking
 * here lies inside its attendee's working day. `dayFor` widens a day to cover
 * its bookings anyway, so a booking that ran past somebody's departure would
 * keep them in the building rather than making them flicker — but as authored,
 * with the hashed days these seat ids produce, that widening is a no-op today.
 * It is there so that the *next* edit to this table cannot open a hole.
 */
const BOOKINGS: Booking[] = [
  {
    // Ops, standing up. The early shift is an anchor and is in from a quarter
    // past six; the other four draw arrivals between half seven and a quarter
    // to nine, so nobody here is being dragged in for their own stand-up.
    room: "Bernal",
    from: at(9, 20),
    to: at(9, 50),
    colorKey: "meeting",
    seats: {
      "bernal-01": "ops-01", // Anouk Sterling
      "bernal-02": "ops-02", // Emeka Farrow
      "bernal-03": "ops-05", // Bettina Kovač
      "bernal-04": "ops-10", // Aurelio Pinsent
      "bernal-05": "ops-04", // Vasco Underhill
    },
  },
  {
    // The morning review — eight of ten, which is what a full room looks like
    // without being a roll call. The six the photograph caught are all here.
    room: "Alcatraz",
    from: at(9, 55),
    to: at(10, 45),
    colorKey: "meeting",
    seats: {
      "alcatraz-01": "sales-01", // Cormac Dellwood
      "alcatraz-02": "sales-02", // Solveig Amankwah
      "alcatraz-03": "sales-03", // Ignatius Pell
      "alcatraz-04": "design-02", // Rosalind Achebe
      "alcatraz-05": "design-01", // Halcyon Reeve
      "alcatraz-06": "ops-04", // Vasco Underhill
      "alcatraz-07": "eng-07", // Clementine Roux
      "alcatraz-08": "eng-01", // Tobias Quillon
    },
  },
  {
    // The visitor waiting, which is a booking of the lobby sofa by the person
    // who has no desk to be at instead. `lobby-01 -> lobby-01` looks like a
    // no-op and is the point: a guest is in the building only when the diary
    // says so, so their whole day is bookings and there is no fall-through.
    room: "Reception",
    from: at(10, 15),
    to: at(10, 35),
    colorKey: "guest",
    seats: { "lobby-01": "lobby-01" },
  },
  {
    // ...and then taken in. Three in the four-seat room.
    room: "Bolinas",
    from: at(10, 35),
    to: at(11, 20),
    colorKey: "meeting",
    seats: {
      "bolinas-01": "design-03", // Zephyrine Mbeki
      "bolinas-02": "eng-02", // Ines Marchetti
      "bolinas-03": "lobby-01", // the visitor, who stays green — see `isGuest`
    },
  },
  {
    // Lands End, door shut, one person. See the note on quiet rooms above.
    room: "Lands End",
    from: at(11, 0),
    to: at(12, 0),
    colorKey: "focus",
    seats: { "quiet-01": "eng-05" }, // Casimir Vane
  },
  {
    // First sitting. Every one of these five has a hashed lunch window that
    // overlaps this, so `dayFor` drops the window and they eat here instead of
    // vanishing — which is the point of putting the island in the diary at all.
    room: "Kitchen",
    from: at(12, 5),
    to: at(12, 40),
    colorKey: "in",
    seats: {
      "kitchen-01": "design-03", // Zephyrine Mbeki
      "kitchen-02": "eng-06", // Perpetua Nkemelu
      "kitchen-03": "design-01", // Halcyon Reeve
      "kitchen-04": "sales-01", // Cormac Dellwood
      "kitchen-05": "ops-05", // Bettina Kovač
    },
  },
  {
    // Second sitting, five minutes after the first clears.
    room: "Kitchen",
    from: at(12, 45),
    to: at(13, 20),
    colorKey: "in",
    seats: {
      "kitchen-01": "design-04", // Ptolemy Sandoval
      "kitchen-02": "ops-07", // Xiomara Belfry
      "kitchen-03": "eng-04", // Sunniva Holt
      "kitchen-04": "eng-01", // Tobias Quillon
      "kitchen-05": "booth-02", // Ottoline Grieve
    },
  },
  {
    // Two in the afternoon, all four lounge chairs.
    room: "Lounge",
    from: at(14, 5),
    to: at(14, 45),
    colorKey: "in",
    seats: {
      "lounge-01": "eng-03", // Dara Oyelaran-Pike
      "lounge-02": "ops-02", // Emeka Farrow
      "lounge-03": "design-02", // Rosalind Achebe
      "lounge-04": "eng-08", // Rafferty Osgood
    },
  },
  {
    // The long one. Overlaps the lounge above by twenty-five minutes and shares
    // nobody with it, which is the pair the audit is really watching.
    room: "Alcatraz",
    from: at(14, 20),
    to: at(15, 25),
    colorKey: "meeting",
    seats: {
      "alcatraz-01": "sales-02", // Solveig Amankwah
      "alcatraz-02": "sales-03", // Ignatius Pell
      "alcatraz-03": "eng-11", // Marisol Thibault
      "alcatraz-04": "design-01", // Halcyon Reeve
      "alcatraz-05": "ops-05", // Bettina Kovač
      "alcatraz-06": "eng-06", // Perpetua Nkemelu
    },
  },
  {
    // Heads-down until five. A booth is one seat, which is the point of it.
    room: "Focus booth 1",
    from: at(15, 30),
    to: at(17, 0),
    colorKey: "focus",
    seats: { "booth-01": "eng-07" }, // Clementine Roux
  },
  {
    // The second visitor, waiting while reception is still at the front desk.
    room: "Reception",
    from: at(15, 20),
    to: at(15, 40),
    colorKey: "guest",
    seats: { "lobby-01": "lobby-01" },
  },
  {
    // Handed over at twenty to four, which is also when reception leaves the
    // desk for Bernal below. That is a story and not a coincidence.
    room: "Bolinas",
    from: at(15, 40),
    to: at(16, 15),
    colorKey: "meeting",
    seats: {
      "bolinas-01": "sales-01", // Cormac Dellwood
      "bolinas-02": "design-03", // Zephyrine Mbeki
      "bolinas-03": "lobby-01", // the visitor
    },
  },
  {
    room: "Bernal",
    from: at(15, 40),
    to: at(16, 20),
    colorKey: "meeting",
    seats: {
      "bernal-01": "eng-02", // Ines Marchetti
      "bernal-02": "eng-04", // Sunniva Holt
      "bernal-03": "ops-07", // Xiomara Belfry
      "bernal-04": "design-04", // Ptolemy Sandoval
      "bernal-05": "reception-01", // Wren Abaddon, off the front desk
    },
  },
  {
    room: "Focus booth 3",
    from: at(16, 40),
    to: at(17, 45),
    colorKey: "focus",
    seats: { "booth-03": "sales-03" }, // Ignatius Pell
  },
  {
    // The late shift eats alone at quarter past seven, in a building that by
    // then holds one or two people. This is the booking that makes the evening
    // read as an evening rather than as one figure that never moves.
    room: "Kitchen",
    from: at(19, 15),
    to: at(19, 40),
    colorKey: "in",
    seats: { "kitchen-04": "eng-11" }, // Marisol Thibault
  },
];

/** One person's whole day, resolved once. */
interface Rostered {
  /** The desk seat, which is the key everything else about this person is on. */
  home: string;
  /**
   * Where they are when nothing is booked, and when they are there at all.
   *
   * The two travel together in one object so that the check in
   * `samplePresenceAt` narrows both at once. `null` for the visitor, who has no
   * desk and exists only inside a booking.
   */
  desk: { at: Presence; day: DeskDay } | null;
  /** Their own bookings, earliest first, audited not to overlap each other. */
  bookings: Episode[];
}

/** One person, in one room, for one window. */
interface Episode {
  from: number;
  to: number;
  /**
   * The finished `Presence`, built at import.
   *
   * A person's diary never changes, so neither does the object describing them
   * in it, and building one per call would be twenty-five allocations a second
   * to produce values that are already known. This is what lets
   * `samplePresenceAt` allocate nothing but its result array.
   */
  who: Presence;
}

/**
 * A guest stays a guest wherever they are sitting.
 *
 * The `colorKey` on a booking describes the *room* — amber for a booth, the
 * meeting red for a table — and it is right for the twenty-four people who work
 * here. It is wrong for the one who does not: the green is the only hue that is
 * not in the office's own materials precisely because a visitor is the one
 * person in the building who is not part of it, and a visitor who turns red on
 * walking into Bolinas has lost the only thing that marked them out.
 *
 * `colorKey` is opaque to the engine and to `presence.ts` — that is the contract
 * and it is not being bent here — but this module *defines* these keys, in
 * `SAMPLE_PRESENCE_PALETTE` above, so it is the one place in the repo entitled
 * to know that `"guest"` means somebody is a guest.
 */
function isGuest(person: Presence): boolean {
  return person.colorKey === "guest";
}

/**
 * The roster, resolved: twenty-five people, each with a desk, a day and a diary.
 *
 * Built at import rather than lazily, because the audit at the bottom of this
 * file has to walk the same tables anyway and two passes over fifty entries at
 * module load is not a cost worth deferring. `SAMPLE_PRESENCE` is a constant and
 * this file is the only thing that writes it, so there is nothing to invalidate.
 */
const ROSTER: Rostered[] = SAMPLE_PRESENCE.map((person) => {
  const home = DESKS[person.seatId] ?? person.seatId;
  const bookings = bookingsFor(person, home);
  if (isGuest(person)) return { home, desk: null, bookings };

  const anchor = PRESENCE_ANCHORS[home];
  // An anchor is a fixed day with no lunch hole, which is exactly a `DeskDay`
  // with an empty interval in it — so the two kinds of person go down one path
  // from here rather than two. `t >= 0 && t < 0` is false at every instant,
  // which is what "takes no lunch break" means arithmetically.
  const base: DeskDay = anchor
    ? { arrive: anchor.from, leave: anchor.to, lunchFrom: 0, lunchTo: 0 }
    : deskDay(home);

  return {
    home,
    desk: {
      // The snapshot's colour describes the snapshot. `"meeting"` is a statement
      // about a room, and a person sitting at their own desk wearing it would be
      // telling the viewer they were somewhere they are not; the other three
      // keys — in, focus, guest — are statements about the person and survive.
      at: { ...person, seatId: home, colorKey: person.colorKey === "meeting" ? "in" : person.colorKey },
      day: dayFor(base, bookings),
    },
    bookings,
  };
});

/**
 * Everything in `BOOKINGS` that names this person, earliest first.
 *
 * O(bookings × seats) per person and therefore O(people × table) overall, which
 * is twenty-five passes over fifty entries at import and nothing at all
 * afterwards. The obvious index — invert the table once into a map keyed on desk
 * seat — would save about twelve hundred string comparisons that happen once,
 * and cost a reader a level of indirection between the diary they are editing
 * and the thing that reads it.
 */
function bookingsFor(person: Presence, home: string): Episode[] {
  const out: Episode[] = [];
  for (const booking of BOOKINGS) {
    for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
      if (deskSeat !== home) continue;
      out.push({
        from: booking.from,
        to: booking.to,
        who: {
          ...person,
          seatId: roomSeat,
          colorKey: isGuest(person) ? person.colorKey : booking.colorKey,
        },
      });
    }
  }
  out.sort((a, b) => a.from - b.from);
  return out;
}

/**
 * One person's day, widened to make room for their diary.
 *
 * Two adjustments, both computed once and both therefore free of the popping a
 * clock-dependent adjustment would cause:
 *
 * **The day covers its bookings.** A meeting that starts before somebody's
 * hashed arrival, or runs past their hashed departure, would otherwise show them
 * appearing for the meeting, vanishing when it ended, and reappearing at their
 * own arrival time — present, absent, present, which is worse than either. As
 * authored no booking in the table needs this; it is here so that the next one
 * cannot open that hole. Note that widening can only ever add minutes, so it
 * cannot shorten an anchor's shift or break the coverage guarantee.
 *
 * **A booking over lunch replaces lunch.** Somebody who spent half past twelve
 * at the kitchen island did not also go out to eat. Without this, the ten people
 * the diary feeds would still be carrying their own hashed lunch window
 * underneath it, and the leftovers of that window would show on both sides of
 * the meal: they would vanish from their desks a few minutes before the sitting
 * they are about to be seen at, and four of the five in the first sitting would
 * stand up from the island straight into what was left of it — for `ops-05`,
 * whose window runs to 13:06 against a sitting that clears at 12:40,
 * twenty-six minutes of absence beginning the moment the room emptied.
 *
 * The whole hole is dropped rather than trimmed to what the booking does not
 * cover. A trimmed hole leaves exactly those leftovers, of whatever length the
 * two hashes happen to disagree by, and an absence whose length nobody chose is
 * a glitch rather than a lunch.
 */
function dayFor(day: DeskDay, bookings: Episode[]): DeskDay {
  if (bookings.length === 0) return day;

  let first = Infinity;
  let last = -Infinity;
  let overLunch = false;
  for (const booking of bookings) {
    first = Math.min(first, booking.from);
    last = Math.max(last, booking.to);
    // Half-open intervals, so touching at an endpoint is not an overlap.
    if (booking.from < day.lunchTo && booking.to > day.lunchFrom) overLunch = true;
  }

  return {
    arrive: Math.min(day.arrive, first),
    leave: Math.max(day.leave, last),
    lunchFrom: overLunch ? 0 : day.lunchFrom,
    lunchTo: overLunch ? 0 : day.lunchTo,
  };
}

/**
 * The booking this person is inside at `t`, or `null`.
 *
 * The list is sorted and a person's bookings never overlap each other — audited
 * below — so the first one whose window contains `t` is the only one that can,
 * and the scan can stop as soon as it reaches one that has not started yet.
 * Nobody in the current table has more than four.
 */
function bookedAt(person: Rostered, t: number): Presence | null {
  for (const booking of person.bookings) {
    if (t < booking.from) break;
    if (t < booking.to) return booking.who;
  }
  return null;
}

/** One desk worker's day, in minutes since local midnight. */
interface DeskDay {
  arrive: number;
  leave: number;
  lunchFrom: number;
  lunchTo: number;
}

/**
 * Is somebody with this day at their own desk at `t`?
 *
 * The lunch hole is deliberately checked after the arrive/leave pair rather than
 * folded into it, because these are two different facts about a person and a
 * single boolean expression covering both is the one that grows an off-by-one
 * the first time somebody edits it.
 */
function isIn(day: DeskDay, t: number): boolean {
  if (t < day.arrive || t >= day.leave) return false;
  return !(t >= day.lunchFrom && t < day.lunchTo);
}

/**
 * A believable working day for one seat, from the seat id and nothing else.
 *
 * ### Why a timetable per person and not an occupancy curve
 *
 * The obvious shape for "the office fills up and empties out" is a curve — a
 * fraction of the floor that is in at time `t` — with each person holding a
 * fixed threshold and being in whenever the curve is above it. It was tried and
 * it is wrong here, for one reason: **the curve is not monotone**. It has to dip
 * at lunch, and everybody whose threshold sits near the bottom of that dip
 * blinks out and back in as the curve goes down and up again. Every wobble in
 * the curve costs a pop for whoever is parked at that level, and the pop lands
 * on the people nearest the middle of the distribution — the ones most likely to
 * be on screen. A curve with any noise in it at all strobes.
 *
 * A per-person timetable has no such failure mode. Each person's day is four
 * fixed instants, so the in/out predicate crosses at most four times in
 * twenty-four hours and each crossing goes one way. Advancing the clock by a
 * second can only change the answer for somebody whose boundary falls inside
 * that second, and it changes it once. Scrubbing backwards is symmetric for the
 * same reason. Bookings extend that property rather than spending it: a booking
 * is two more fixed instants, so a person with four of them crosses at most a
 * dozen times all day and every crossing is still a single, one-way step.
 *
 * Note what this is *instead of* hysteresis. Hysteresis needs memory of the last
 * answer, and this function must give the same answer on a fresh reload as it
 * gave the frame before — a page that has just booted has no last answer to
 * remember. So rather than damping a boundary that moves, the boundary is made
 * not to move at all, which buys the same freedom from chatter without any
 * state to get out of sync between two callers. Both the scene and the plan view
 * are handed this list; if it depended on history they could disagree.
 *
 * ### The numbers
 *
 * Arrivals are `08:45` ± 75 min and departures `17:50` ± 95 min, each drawn
 * triangular rather than uniform so the floor fills through a busy middle with
 * thin tails instead of a flat trickle — which is what a morning actually looks
 * like from a desk. Departure is then pulled by 0.6 of how far the arrival was
 * from the mean, because the person who is in at half seven is the person who
 * leaves at four, and independent draws produce a floor full of people working
 * seven-to-four and ten-to-eight at the same time. Those coefficients put
 * arrivals in `07:30`–`10:00`, departures in `15:30`–`20:10` at the extremes,
 * and the day itself between 7 h and 11 h 10 — the extremes of both draws at
 * once, which is rare by construction.
 *
 * Lunch is `12:20` ± 40 min, taken away from the desk by the roughly two thirds
 * who draw under the threshold, and it lasts 24 to 52 minutes. The shortest of
 * those is still twenty-four minutes of world time, which is long enough to read
 * as somebody having gone to eat rather than as a figure that glitched. Ten of
 * the people who draw a lunch are in the diary at the kitchen island instead and
 * never leave the building; see `dayFor`.
 *
 * Every window lies inside one calendar day and none of them wraps midnight.
 * That is a constraint worth keeping, and it applies to `BOOKINGS` too: an
 * interval that wraps needs `from > to` handling in `isIn` and `bookedAt`, and
 * the first person to write one without it gets a night owl who is never in
 * rather than always.
 *
 * Seeded from the **seat** id and not the person id, because the seat is the
 * address — it is what a `Presence` binds to and what survives an edit to this
 * file. Renaming Tobias Quillon should not change when the person at `eng-01`
 * comes in.
 *
 * ### It is no longer memoised
 *
 * It used to be, into a `Map`, and the reason was good: the old
 * `samplePresenceAt` called this per person on every clock tick, so without the
 * cache each tick re-hashed a day that was by construction the same day. That is
 * gone. `ROSTER` now calls this exactly once per seat at import and nothing
 * calls it again, and a cache in front of a function called once per key can
 * only ever be wrong, never fast. Worth a paragraph rather than a silent
 * deletion, because "it is memoised, so call it freely" was a documented
 * property of this function and whoever adds the next caller should know it has
 * lapsed.
 */
function deskDay(seatId: string): DeskDay {
  const arrive = at(8, 45) + 75 * triangular(seatId, "arrive");
  const leave = at(17, 50) + 95 * triangular(seatId, "leave") + 0.6 * (arrive - at(8, 45));
  const lunchFrom = at(12, 20) + 40 * triangular(seatId, "lunch");
  const lunchLength = unit(seatId, "lunch-length") < 0.65 ? 38 + 14 * triangular(seatId, "bite") : 0;

  return { arrive, leave, lunchFrom, lunchTo: lunchFrom + lunchLength };
}

/**
 * A stable number in `[0, 1)` from a seat id and a named stream.
 *
 * FNV-1a, which is here because it is eight lines and has no dependencies, not
 * because its statistical properties matter — nothing downstream of this is a
 * simulation, and the requirement is only that the same seat gets the same
 * answer on every reload, in every browser, forever. `Math.imul` keeps the
 * multiply in 32 bits; a plain `*` would go through a double and lose the low
 * bits that are the whole output.
 *
 * The stream name is what makes the draws independent. Hashing the seat id once
 * and slicing the bits would be cheaper and would tie a seat's arrival time to
 * its lunch hour in a way that eventually shows up as everybody who comes in
 * early eating at the same moment.
 */
function unit(seatId: string, stream: string): number {
  const text = `${seatId}/${stream}`;
  let h = 0x811c9dc5;
  for (let i = 0; i < text.length; i += 1) {
    h ^= text.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return (h >>> 0) / 0x1_0000_0000;
}

/**
 * A stable number in `(-1, 1)`, peaked at zero: two uniforms added, which is the
 * cheapest thing that is not flat.
 *
 * Uniform arrival times give a floor that fills at a constant rate from half
 * seven to ten, and it reads as wrong without being able to say why — there is
 * no rush hour in it. One extra hash buys a peak.
 */
function triangular(seatId: string, stream: string): number {
  return unit(seatId, `${stream}-a`) + unit(seatId, `${stream}-b`) - 1;
}

// ---- The audit -------------------------------------------------------------

/**
 * Everything the tables above promise each other, checked once at import.
 *
 * These are all invariants of *source data* rather than runtime conditions, so
 * this is the right place for them and there is deliberately no defensive check
 * on the hot path. A runtime de-duplicator in `samplePresenceAt` would be code
 * that cannot fire until somebody breaks the diary, and on the day they did it
 * would turn a visible bug — two figures in one chair — into an invisible one,
 * a person silently missing from the floor. Better to fail loudly here, where
 * the thing that is wrong is twenty lines from the message.
 *
 * `console.warn` and not `throw`, matching the rest of this file's posture: a
 * demo adapter with one bad row in it should still open the building.
 */
function auditRoster(): void {
  const say = (message: string) => console.warn(`[tera/sample] ${message}`);

  // 1. Desks are one to a person. Two roster entries resolving to the same home
  // seat is a permanent double occupancy — every hour of every day, not just
  // during a booking — which is the worst thing `DESKS` can get wrong.
  const owner = new Map<string, string>();
  for (const person of ROSTER) {
    const name = person.desk?.at.label ?? "the visitor";
    const held = owner.get(person.home);
    if (held) say(`seat "${person.home}" is the home seat of both ${held} and ${name}`);
    owner.set(person.home, name);
  }

  // 2. The anchors have to be anchored to something. A seat id in
  // `PRESENCE_ANCHORS` that is nobody's desk matches nobody, and the continuous
  // 06:15–22:40 guarantee quietly becomes a hope — the failure being an empty
  // office at eleven in the morning on some seeds and not others, which is about
  // the worst shape a bug can have.
  for (const seatId of Object.keys(PRESENCE_ANCHORS)) {
    if (!owner.has(seatId)) {
      say(`anchor seat "${seatId}" is nobody's desk; the sample office can now be empty during working hours`);
    }
  }

  // 3. Within one booking: everybody named exists, nobody is named twice, and
  // nowhere it seats them is a desk.
  //
  // The last of those is the one that is easy to miss when reading the sweep
  // below: a booking that puts somebody at `eng-03` collides with whoever lives
  // at `eng-03` without overlapping any other *booking*, so no amount of
  // booking-against-booking checking would ever see it. Repeating a room seat
  // inside one booking is the fourth case and is not checked here at all,
  // because `seats` is keyed on it and `tsc` rejects the file (TS1117).
  const desks = new Set<string>();
  for (const person of ROSTER) if (person.desk) desks.add(person.home);
  for (const booking of BOOKINGS) {
    const named = new Set<string>();
    for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
      if (!owner.has(deskSeat)) say(`${label(booking)} seats "${deskSeat}", who is nobody`);
      if (desks.has(roomSeat)) say(`${label(booking)} uses "${roomSeat}", which is somebody's desk`);
      if (named.has(deskSeat)) say(`${label(booking)} seats "${deskSeat}" twice`);
      named.add(deskSeat);
    }
  }

  // 4. No two bookings that are live at the same instant share a chair or a
  // person.
  //
  // Sweeping the `from` instants is exact rather than a sample, and it is the
  // only subtle thing in this function: the set of live bookings only ever
  // *gains* a member at some booking's `from`. So if two bookings ever overlap,
  // they overlap at the later of their two starts, and visiting every `from`
  // cannot miss a pair. Fifteen starts against fifty entries, once.
  //
  // The person half of this is also what `bookedAt` leans on when it stops its
  // scan at the first booking that has not started: if nobody is ever in two
  // bookings at once, the first match is the only match.
  for (const instant of BOOKINGS.map((booking) => booking.from)) {
    const chair = new Map<string, Booking>();
    const busy = new Map<string, Booking>();
    for (const booking of BOOKINGS) {
      if (instant < booking.from || instant >= booking.to) continue;
      for (const [roomSeat, deskSeat] of Object.entries(booking.seats)) {
        // Same-booking repeats belong to check 3, which can name them better.
        const other = chair.get(roomSeat);
        if (other && other !== booking) {
          say(`at ${clock(instant)}, "${roomSeat}" is booked by both ${label(other)} and ${label(booking)}`);
        }
        chair.set(roomSeat, booking);

        const elsewhere = busy.get(deskSeat);
        if (elsewhere && elsewhere !== booking) {
          say(`at ${clock(instant)}, "${deskSeat}" is in both ${label(elsewhere)} and ${label(booking)}`);
        }
        busy.set(deskSeat, booking);
      }
    }
  }
}

/** A booking, named the way somebody scanning the diary would look for it. */
function label(booking: Booking): string {
  return `${booking.room} at ${clock(booking.from)}`;
}

/** `hh:mm`, for the audit's messages only. */
function clock(t: number): string {
  const hh = String(Math.floor(t / 60)).padStart(2, "0");
  const mm = String(Math.floor(t % 60)).padStart(2, "0");
  return `${hh}:${mm}`;
}

auditRoster();
