/**
 * Every visibility decision this interface makes, as one pure function.
 *
 * `renderLegend()` in `main.ts` made about forty of them — `walkButton.hidden =
 * inside ? office?.walker === null : city.actorState() === null`, and thirty-nine
 * more like it — interleaved with the DOM writes that acted on them, reading
 * eight pieces of module-level mutable state, and with **zero** test assertions
 * behind any of it. On a product whose entire pitch is the interface, the
 * interface was the only subsystem in the repo with no coverage at all. It was
 * not testable in principle either: every decision needed a live `Stage`, a built
 * heightfield and a WebGL context to reach.
 *
 * This module is the decisions and nothing else. No DOM, no THREE, no import of
 * `main.ts`, no clock, no `localStorage`, no network — hand it a plain object,
 * get a plain object back. `mount.ts` is the only thing that writes to the page,
 * and it writes exactly what this returned.
 *
 * Two properties fall out of that split and both are the point:
 *
 * - **The matrix is enumerable.** Three access tiers × six control modes ×
 *   inside/outside × coarse/fine pointer is 72 states, and
 *   `src/test/ui/chromeState.test.ts` walks all of them. That is a thing you can
 *   only do to a function.
 * - **The integration workstream makes one call.** `apply(chromeState(inputs))`
 *   replaces forty scattered mutations, so a new piece of chrome is a field here
 *   and a write there, rather than a fortieth line in a function nobody wants to
 *   touch.
 *
 * ### On the input shape
 *
 * The twelve top-level keys are fixed by the build spec and are the contract with
 * `integration`; they are not negotiable and are not reordered. Everything past
 * them is optional, defaulted, and exists because the forty decisions genuinely
 * depend on more than twelve facts — the label on the Fly button needs to know
 * which board you are on, and the label on the touch action button needs to know
 * whether the crow is currently in the air. Making those optional rather than
 * required is deliberate: an integration stub that passes only the twelve
 * documented keys type-checks and returns a sane, if plainer, chrome.
 */

import type { Tier } from "../access.ts";
import type { DeviceDeclaration } from "../devices/types.ts";
import type { ControlMode } from "../play/controlMode.ts";
import { formatAircraftDetail, formatPlayHud } from "./hud.ts";
import type { AircraftDetailInput, AircraftDetailView, PlayHudView, PlayTelemetry } from "./hud.ts";
import { railHints, railLabel } from "./shortcuts.ts";
import { BREAKPOINT } from "./tokens.ts";

// ---- Inputs ---------------------------------------------------------------

/** What this visitor is, as the chrome needs to know it. */
export interface ChromeAccess {
  tier: Tier;
  /** `null` when this deployment has no door to knock on — a clean self-hosted clone. */
  signInUrl: string | null;
  /** The auth subject, when there is one. */
  subject: string | null;
  /** The name this person chose, which outranks the subject in the badge. */
  displayName?: string | null;
  /** True once a local profile exists, which is what the Character button edits. */
  hasProfile?: boolean;
}

export interface ChromeViewport {
  width: number;
  height: number;
  /**
   * A finger, not a mouse.
   *
   * Keyed off the pointer rather than the width, because the defect this decides
   * is about fingers: an 820px iPad has no keyboard either, and it lands on the
   * desktop layout by design (see `deviceProfile` in `engine/stage.ts`).
   */
  coarsePointer: boolean;
}

/** Which of the three feeds is answering, and what the page may therefore claim. */
export interface ChromeFeeds {
  markers: boolean;
  weather: boolean;
  flights: boolean;
  /** A sky somebody typed retires the live-weather claim for as long as it is up. */
  weatherOverridden?: boolean;
  /** The people in this office are invented, and the caption must say so. */
  sampleOccupancy?: boolean;
  /** Licence lines from whichever live sources are answering. Shown in the `?` card. */
  credits?: readonly string[];
}

/** Which board and which building, for the labels that name them. */
export interface ChromeBoard {
  cityId?: string;
  cityLabel?: string;
  officeId?: string;
  officeLabel?: string;
  officeStatus?: "active" | "building";
  /** The aircraft route only exists on the California board. */
  isCalifornia?: boolean;
}

/**
 * One tab in the board strip — a world outside a building, a building inside one.
 *
 * The lists live in the inputs rather than being fetched by the applier, so that
 * `apply(chromeState(inputs))` really is *one* call: a second `setBoards()`
 * method on the handle would be a second place for the chrome to be half
 * updated, which is the class of bug this whole module exists to remove.
 */
export interface ChromeBoardTab {
  id: string;
  label: string;
  /** "active" or "building" for an office; `null` for a city, which has no status. */
  status?: "active" | "building" | null;
}

/** One row of the chapter list: a city chapter and an office viewpoint are both this. */
export interface ChromeView {
  id: string;
  /** The authored number, or `null` to take the ordinal. */
  number?: string | null;
  shortLabel: string;
  description?: string;
}

/**
 * One rung of the places list: a chapter on some board, named so it can be told
 * apart from the chapters on the other two.
 *
 * A separate shape from `ChromeView` on purpose. A view is a row in the *current*
 * board's tour and is addressed by its position in that list — which is the one
 * thing about a chapter this round may not move, because the capture guards aim
 * that way. A place is addressed by `key`, carries the board it is on, and is
 * ordered by how far the camera stands back. Two different identities, two
 * types, and no chance of one silently standing in for the other.
 */
export interface ChromePlace {
  /** `board:chapter`. Unique across the whole ladder. */
  key: string;
  /** The chapter id on its own board. */
  id: string;
  board: string;
  /** The heading this rung sits under: a region, never a board. */
  region: string;
  /** Unique across the ladder — the two "Whole Board" rungs are named here. */
  label: string;
  /** True metres from the camera to the target at this rung's authored pose. */
  standoffM: number;
  description?: string;
}

/**
 * What the detail card is showing.
 *
 * Two shapes, because there are two things worth picking: a marker, which is one
 * authored sentence, and an aircraft, which is five observed fields and is the
 * moment the map stops looking like a diagram. Modelled as a union rather than a
 * string so the aircraft case cannot be flattened back into one on its way here,
 * which is exactly what used to happen.
 */
export type ChromeDetail =
  | { kind: "text"; text: string }
  | { kind: "aircraft"; aircraft: AircraftDetailInput };

/** The body currently under control, for the labels that describe it. */
export interface ChromeActor {
  kind?: "crow" | "dog" | "humanoid" | null;
  /** A crow on the wing takes a different action pad from one on the ground. */
  flying?: boolean;
}

/** Facts about the building you are standing in. */
export interface ChromeOffice {
  /** Screens authored in this pack that a full-depth viewer may drive. */
  mediaSurfaceCount?: number;
  /** How many of them are showing something right now. */
  mediaSurfaceActiveCount?: number;
  /** `resolveRobotOperations`' one-sentence disclosure, when the pack has robots. */
  robotDisclosure?: string | null;
  /**
   * `smokeCaption`'s sentence, when the fires on the board have browned this
   * building's sky. `null` on every ordinary day.
   *
   * It sits with the other two disclosures rather than in the fire panel
   * because it is a statement about *this room's* picture: the sky in here is
   * derived from incidents on the map, not measured at this address, and a
   * brown courtyard with nothing beside it saying why is the failure this
   * exists to prevent.
   */
  smokeNote?: string | null;
  /** The pack could not give this office a walker — a dollhouse-only building. */
  walkable?: boolean;
}

export interface ChromeInputs {
  mode: ControlMode;
  /** Every mode reachable right now, per `controlModeAvailable`. */
  available: readonly ControlMode[];
  access: ChromeAccess;
  inside: boolean;
  /** `null` outside a building. "public" is the shell; "full" is the people too. */
  officeDepth: "public" | "full" | null;
  viewport: ChromeViewport;
  feeds: ChromeFeeds;
  /** One sentence per demotion the server reported. Admin-visible. */
  degraded: readonly string[];
  /** Authored hardware in this room. Empty outside one. */
  devices: readonly DeviceDeclaration[];
  /** Nothing in `localStorage` says this browser has been here before. */
  firstVisit: boolean;
  panelOpen: boolean;
  planOpen: boolean;

  // ---- Optional refinements; see the header ----
  board?: ChromeBoard;
  actor?: ChromeActor;
  office?: ChromeOffice;
  /** A marker or aircraft is picked, so the detail card has something to show. */
  detailOpen?: boolean;
  /** A camera is live in this tab, which raises the top-right column over modals. */
  cameraLive?: boolean;
  /** Hosted presence is connected, which adds a row to the top-right column. */
  presenceVisible?: boolean;
  /** The board strip's entries: cities outside a building, offices inside one. */
  boards?: readonly ChromeBoardTab[];
  /**
   * Whether the board strip is *drawn*. The list above is built either way.
   *
   * The two are separate because the strip has a second reader: the harnesses
   * reach a board with `document.querySelector('[data-board=…]').click()`, and a
   * DOM click does not care whether an element is displayed. Emptying the list
   * would take the control away from them as well as from the visitor; hiding it
   * takes it away from the visitor only, which is what this round is for.
   */
  boardsVisible?: boolean;
  /** The ladder, in the order the column prints it. Empty hides the section. */
  places?: readonly ChromePlace[];
  /** The rung the camera is standing on, by `key`. */
  activePlaceKey?: string | null;
  /** The chapter list: city chapters outside, authored viewpoints inside. */
  views?: readonly ChromeView[];
  activeViewId?: string | null;
  /** Whatever the current mode's controller is reporting, for the play HUD. */
  telemetry?: PlayTelemetry | null;
  /** The picked marker or aircraft, or `null`. Supersedes `detailOpen`. */
  detail?: ChromeDetail | null;
  /** The clock line, already formatted by whoever owns the clock. */
  clockLabel?: string;
}

// ---- Output ---------------------------------------------------------------

/**
 * Three layouts, not two breakpoints' worth of tweaks.
 *
 * `phone` is a different design and not a narrow desktop: no hover, so nothing
 * may depend on it; no keyboard, so a card explaining the keyboard is furniture;
 * and one thumb, which reaches the bottom third of the screen and not the top
 * corners.
 */
export type ChromeLayout = "phone" | "compact" | "desktop";

export interface ChromeModeButton {
  mode: ControlMode;
  label: string;
  visible: boolean;
  pressed: boolean;
}

export interface ChromeTouchAction {
  /** The DOM id in `index.html`, so the applier needs no second table. */
  id: string;
  label: string;
  visible: boolean;
  /** The held control it feeds, for the buttons that hold one. */
  control: string | null;
  /** The one-shot request it fires, for the buttons that fire one. */
  edge: string | null;
}

export interface ChromeRailHint {
  id: string;
  keys: readonly string[];
  label: string;
}

export interface ChromeBoardButton extends ChromeBoardTab {
  active: boolean;
}

export interface ChromeViewRow {
  id: string;
  /** Always a string by the time it reaches the applier: authored, or the ordinal. */
  number: string;
  label: string;
  active: boolean;
}

export interface ChromePlaceRow {
  key: string;
  id: string;
  board: string;
  label: string;
  active: boolean;
  /** The region heading to print **above** this row, or `null` to print none. */
  region: string | null;
  /**
   * The digit key that reaches this rung, or `null` past the ninth.
   *
   * The number **is** the key, which is why it stops at nine rather than
   * counting to twenty-four: a printed ordinal that no keystroke matches is a
   * label pretending to be an affordance. The chapter list gets away with
   * numbering everything because a pack's numbers are authored captions; the
   * ladder's are not.
   */
  key9: string | null;
  /** "1,551 km" / "3.9 km". Formatted here so the applier has no decisions left. */
  standoff: string;
}

/** The detail card, resolved. `null` when nothing is picked. */
export type ChromeDetailView =
  | { kind: "text"; text: string }
  | { kind: "aircraft"; view: AircraftDetailView };

export interface ChromeState {
  // ---- Frame ----
  layout: ChromeLayout;
  /** A finger drives this session, so on-screen controls are the primary path. */
  touchFirst: boolean;
  /** Standing in a building rather than looking at a board. */
  inside: boolean;
  /** Every class the applier sets on `<body>`, computed once so nothing drifts. */
  bodyClasses: readonly string[];

  // ---- The left column ----
  panelVisible: boolean;
  panelOpen: boolean;
  /**
   * Carried through so the applier can compute the *next* value for a toggle
   * without keeping its own copy of the state — which is how a toggle ends up
   * disagreeing with the thing it toggles.
   */
  planOpen: boolean;
  panelToggleVisible: boolean;
  panelToggleLabel: string;
  scrimVisible: boolean;
  panelTitle: string;
  panelSubtitle: string;
  /** Which list the board strip is pointed at: worlds outside, buildings inside. */
  boardPicker: "city" | "office";
  boards: readonly ChromeBoardButton[];
  clockLabel: string;
  enterVisible: boolean;
  enterLabel: string;
  walkVisible: boolean;
  walkPressed: boolean;
  walkLabel: string;
  flyVisible: boolean;
  flyPressed: boolean;
  flyLabel: string;
  screensVisible: boolean;
  screensLabel: string;
  devicePanelVisible: boolean;
  devicePanelLabel: string;
  /**
   * The declarations themselves, carried through so the applier can build the
   * panel without a second call. Pass-through, not a decision — but it belongs in
   * the state for the same reason the board and view lists do: one call, one
   * consistent frame of chrome.
   */
  deviceDeclarations: readonly DeviceDeclaration[];
  /** Whether the board strip is worth drawing at all. See `ChromeInputs.boards`. */
  boardsVisible: boolean;
  chaptersVisible: boolean;
  views: readonly ChromeViewRow[];
  placesVisible: boolean;
  places: readonly ChromePlaceRow[];
  blurbVisible: boolean;
  blurbText: string;

  // ---- Top right ----
  tierVisible: boolean;
  tierTone: Tier;
  tierLabel: string;
  /**
   * What signing in **adds**, for a visitor who has not.
   *
   * `null` for anyone it would not help. The badge used to read "Public view"
   * and stop there, which names the visitor rather than the offer and is the one
   * place in the product where the anon-first promise reads as a demotion.
   */
  tierAdds: string | null;
  whoLabel: string | null;
  characterVisible: boolean;
  signInVisible: boolean;
  signInLabel: string;
  signInHref: string | null;
  presenceVisible: boolean;
  cameraAlert: boolean;
  minimapVisible: boolean;
  minimapReadoutVisible: boolean;

  // ---- The dock and the HUD ----
  modeDockVisible: boolean;
  modeButtons: readonly ChromeModeButton[];
  playHudVisible: boolean;
  /** The three strings and the warning flag, or `null` in the orbit modes. */
  playHud: PlayHudView | null;

  // ---- The rail ----
  railVisible: boolean;
  detailVisible: boolean;
  detail: ChromeDetailView | null;
  helpVisible: boolean;
  planToggleVisible: boolean;
  planTogglePressed: boolean;
  railHintsVisible: boolean;
  railHints: readonly ChromeRailHint[];

  // ---- Touch ----
  touchControlsVisible: boolean;
  stickVisible: boolean;
  stickLabel: string;
  touchActions: readonly ChromeTouchAction[];

  // ---- Honesty, and the office ----
  sourceVisible: boolean;
  sourceLabel: string;
  sourceLive: boolean;
  officeInviteVisible: boolean;
  officeInviteText: string;
  officeInviteLinkLabel: string | null;
  officeNoteVisible: boolean;
  officeNoteText: string;
  degradedVisible: boolean;
  degradedLines: readonly string[];
  /**
   * The credit lines the live sources ask for, shown in the `?` card.
   *
   * MET Norway and Open-Meteo publish under CC BY 4.0 and the API forwards the
   * line each of them asks for; adsb.lol asks to be named for the positions. All
   * of it arrived, was parsed, and was then read by nobody — a licence
   * obligation plumbed to within one line of being met. Empty when nothing live
   * is on screen, because a credit for data nobody is looking at is noise and
   * the zero-config build owes nobody anything.
   */
  credits: readonly string[];

  // ---- First run ----
  onboardingVisible: boolean;

  canvasLabel: string;
}

// ---- The dock's five buttons ---------------------------------------------

const DOCK_MODES: readonly { mode: ControlMode; label: string }[] = [
  { mode: "overview", label: "View" },
  { mode: "drive", label: "Drive" },
  { mode: "actor", label: "Explore" },
  { mode: "aircraft", label: "Fly" },
  { mode: "office-walk", label: "Walk" },
];

/**
 * Modes in which a body is under control and the touch surface applies.
 *
 * `office-overview` is not one of them, and neither is `overview`: both are
 * orbit-camera states where the joystick would move nothing.
 */
const PLAY_MODES: readonly ControlMode[] = ["drive", "actor", "aircraft", "office-walk"];

function isPlayMode(mode: ControlMode): boolean {
  return PLAY_MODES.includes(mode);
}

/**
 * The liveness phrase, restated here rather than imported.
 *
 * `describeLiveness` lives in `adapters/http.ts`, which cannot be imported under
 * Node's type stripping today and which pulls a network client behind it. The
 * wording is the same and the rule is the same: all three live is the only case
 * that may say "live data", because "live data" printed over invented companies
 * because a weather station answered is precisely the claim the flag exists to
 * prevent.
 */
function describeFeeds(feeds: ChromeFeeds): string {
  const weather = feeds.weather && feeds.weatherOverridden !== true;
  const parts: string[] = [];
  if (feeds.markers && weather && feeds.flights) return "live data";
  if (feeds.markers) parts.push("live markers");
  if (weather) parts.push("live weather");
  if (feeds.flights) parts.push("live traffic");
  return parts.join(" · ");
}

function layoutFor(viewport: ChromeViewport): ChromeLayout {
  const width = Number.isFinite(viewport.width) ? viewport.width : BREAKPOINT.compact + 1;
  if (width <= BREAKPOINT.phone) return "phone";
  if (width <= BREAKPOINT.compact) return "compact";
  return "desktop";
}

function actorNoun(actor: ChromeActor | undefined): string {
  const kind = actor?.kind ?? null;
  if (kind === "crow") return "your crow";
  if (kind === "dog") return "your dog";
  if (kind === "humanoid") return "your humanoid";
  return "a body";
}

// ---- Seeds ----------------------------------------------------------------

/**
 * Should the left column start open at this width?
 *
 * The width only *seeds* these two; the moment somebody presses the toggle the
 * viewport stops having an opinion. That rule was already right and is kept —
 * what was wrong was that the seed lived inline in `main.ts` next to the
 * `resize` listener that re-ran one of the two and not the other, so a rotation
 * from portrait to landscape re-seeded the plan and left the panel sheet closed
 * over a desktop-width layout.
 */
export function seedPanelOpen(width: number): boolean {
  return !(Number.isFinite(width) && width <= BREAKPOINT.compact);
}

/**
 * Should the plan start visible at this width?
 *
 * **Yes, at every width, and that is the change.** It used to be
 * `window.innerWidth > 600`, which meant a phone started with the plan off — and
 * a phone has no `M`. Every visible control at 390px was enumerated once and
 * none of them could turn it on, so the bottom-sheet plan layout `index.html`
 * had carried for months was unreachable on the device it was drawn for. The
 * `#plan-toggle` button closed the "no way to turn it on" half; this closes the
 * "it was off to begin with" half, and the phone stylesheet gives the plan a
 * glanceable 8.5rem home in the top-right column rather than a sheet that eats
 * the map.
 */
export function seedPlanOpen(_width: number): boolean {
  return true;
}

// ---- The function ---------------------------------------------------------

/**
 * A stand-off as a person reads it, from 3.9 km to 1,551 km.
 *
 * Kilometres everywhere, because the ladder spans four orders of magnitude and
 * switching units partway down a column of numbers turns a scale into a puzzle.
 * One decimal below 10 km, where the difference between 3.9 and 4.3 is two
 * different neighbourhoods; none above it, where it is noise.
 */
function formatStandoff(metres: number): string {
  if (!Number.isFinite(metres) || metres <= 0) return "";
  const km = metres / 1000;
  if (km < 10) return `${km.toFixed(1)} km`;
  return `${Math.round(km).toLocaleString("en-US")} km`;
}

/**
 * Resolve the whole interface for one instant. Pure, total, and cheap enough to
 * call on every frame if the caller wants to.
 */
export function chromeState(inputs: ChromeInputs): ChromeState {
  const layout = layoutFor(inputs.viewport);
  const phone = layout === "phone";
  const touchFirst = inputs.viewport.coarsePointer;
  const inside = inputs.inside;
  const mode = inputs.mode;
  const available = inputs.available;
  const board = inputs.board ?? {};
  const office = inputs.office ?? {};
  const actor = inputs.actor ?? {};
  const tier = inputs.access.tier;

  const can = (candidate: ControlMode): boolean => available.includes(candidate);

  const walking = mode === "office-walk";
  const exploring = mode === "actor";
  const flying = mode === "aircraft";
  const driving = mode === "drive";
  const playing = isPlayMode(mode);
  const crowFlying = exploring && actor.kind === "crow" && actor.flying === true;

  const cityLabel = board.cityLabel ?? "";
  const officeLabel = board.officeLabel ?? "the studio";
  const officeStatus = board.officeStatus ?? "building";

  // ---- The dock -----------------------------------------------------------
  //
  // Hidden entirely when `overview` is the only thing available. On two of the
  // three city boards this rendered as a lit pill holding one already-pressed
  // button that did nothing — a control whose only state is its current one is
  // not a control, it is a label pretending to be one.
  const modeButtons: ChromeModeButton[] = DOCK_MODES.map(({ mode: dockMode, label }) => {
    // The "View" button stands for both orbit states: `overview` outside a
    // building and `office-overview` inside one. They are one idea — the camera
    // is yours and nothing else is — and two buttons for it would swap under the
    // viewer as they walked through a door.
    const visible =
      dockMode === "overview" ? can("overview") || can("office-overview") : can(dockMode);
    const pressed =
      dockMode === "overview" ? mode === "overview" || mode === "office-overview" : mode === dockMode;
    return { mode: dockMode, label, visible, pressed };
  });
  const modeDockVisible = modeButtons.filter((button) => button.visible).length > 1;

  // ---- The touch action pad ----------------------------------------------
  //
  // Built from the mode rather than from a pile of independent `hidden`
  // assignments, which is what this was: eight `if (touchX) touchX.hidden = …`
  // lines whose conditions overlapped and could not all be true or all be false.
  const touchActions: readonly ChromeTouchAction[] = [
    {
      id: "touch-primary",
      label: driving ? "Handbrake" : flying ? "Throttle" : crowFlying ? "Climb" : "Sprint",
      visible: playing && !walking,
      control: crowFlying ? "ascend" : "primary",
      edge: null,
    },
    {
      id: "touch-secondary",
      label: "Glide",
      visible: crowFlying,
      control: "secondary",
      edge: null,
    },
    { id: "touch-pitch-up", label: "Pitch +", visible: crowFlying || flying, control: "pitch-up", edge: null },
    { id: "touch-pitch-down", label: "Pitch −", visible: crowFlying || flying, control: "pitch-down", edge: null },
    { id: "touch-assist", label: "Assist", visible: driving || flying, control: null, edge: "assist" },
    { id: "touch-reset", label: "Reset", visible: driving || flying, control: null, edge: "reset" },
    { id: "touch-camera", label: "Camera", visible: driving, control: null, edge: "camera" },
    { id: "touch-map", label: "Map", visible: true, control: null, edge: null },
  ];

  /**
   * The joystick, and the defect it fixes.
   *
   * `pointerStick.ts` has carried a real analogue stick since the play modes
   * landed, and it was shown only while a body was already under control — which
   * on a phone in VIEW mode meant the screen offered no movement control at all,
   * and no way to discover that one existed. It is now visible in every play
   * mode *and* announced by the onboarding coach, so the first thing a thumb
   * finds is the thing that moves.
   */
  const touchControlsVisible = touchFirst && playing;

  // ---- The rail -----------------------------------------------------------
  //
  // `?` is a keyboard reference. On a device with no keyboard it was still being
  // drawn, which is the interface offering a person a list of keys they cannot
  // press. On a coarse pointer the same button remains — the sheet now carries
  // the touch and on-screen half of every row — but it is labelled for what it
  // is there.
  const hints = railHints(mode, touchFirst).map((entry) => ({
    id: entry.id,
    keys: entry.keys,
    // `rail`, not `meaning`: the sheet gets the sentence, the rail gets the
    // glance. See the field's note in `shortcuts.ts` for what reusing the
    // sentence here actually looked like on screen. `railLabel` is what makes
    // the digits read "Places" on a board and "Viewpoints" in a room without
    // binding the same nine keys twice.
    label: railLabel(entry, inside),
  }));

  // ---- The honesty line ---------------------------------------------------
  const sampleOccupancy = inside && inputs.feeds.sampleOccupancy === true;
  const liveness = describeFeeds(inputs.feeds);
  const sourceLabel = sampleOccupancy
    ? "sample occupancy · these people are invented"
    : liveness;

  // ---- The tier badge -----------------------------------------------------
  //
  // The label names what you get; the second line names what you would gain.
  // "Public view" said neither, and on a product designed anon-first it was the
  // one string that framed the default visitor as a lesser one.
  const tierLabel = tier === "god" ? "Godmode" : tier === "member" ? "Full view" : "Open demo";
  const tierAdds =
    tier === "anon" && inputs.access.signInUrl !== null
      ? "Sign in adds the people: who is in the room, your own character on the floor, and the studio desk."
      : null;

  // ---- The studio's one invitation ---------------------------------------
  //
  // It used to live on `#office-badge` inside `#panel`, and on a phone `#panel`
  // is a bottom sheet that starts closed — so the only in-office call to sign in
  // was behind a hamburger on the device least likely to open one. This is the
  // same move `renderSource` already made for the occupancy disclosure, applied
  // to the message that is an offer rather than a disclosure.
  const publicOffice = inside && inputs.officeDepth === "public";
  const officeInviteText = publicOffice
    ? inputs.access.signInUrl !== null
      ? "You are seeing the building, not the people."
      : "This deployment has no sign-in, so the floor is always the building alone."
    : "";

  const mediaCount = office.mediaSurfaceCount ?? 0;
  const mediaActive = office.mediaSurfaceActiveCount ?? 0;
  const robotDisclosure = office.robotDisclosure ?? null;
  const mediaNote =
    mediaCount === 0
      ? ""
      : mediaActive > 0
        ? `${mediaActive} of ${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} active · stop control in Office screens.`
        : `${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} ready · media stays off until you opt in.`;
  const officeNoteText = [mediaNote, robotDisclosure ?? "", office.smokeNote ?? ""]
    .filter((part) => part !== "")
    .join(" ");

  // ---- Onboarding ---------------------------------------------------------
  //
  // Not while a modal-ish state is up, not inside a building, and not while a
  // body is already under control — a coach that teaches "drag to orbit" over a
  // scene somebody is already driving is a coach interrupting the thing it was
  // written to cause.
  const onboardingVisible = inputs.firstVisit && !inside && mode === "overview";

  // ---- The two lists -------------------------------------------------------
  //
  // Resolved here rather than in the applier so the applier has no decisions
  // left to make. The board strip is one control pointed at whichever list is
  // currently the answer to "which of these am I in"; a second, separate office
  // strip was the obvious alternative and is worse, because it would sit dead
  // and greyed out for the whole time anybody is looking at the city.
  const activeBoardId = inside ? board.officeId : board.cityId;
  const boards: readonly ChromeBoardButton[] = (inputs.boards ?? []).map((tab) => ({
    ...tab,
    active: tab.id === activeBoardId,
  }));

  const views: readonly ChromeViewRow[] = (inputs.views ?? []).map((view, i) => ({
    id: view.id,
    number: view.number ?? String(i + 1).padStart(2, "0"),
    label: view.shortLabel,
    active: view.id === inputs.activeViewId,
  }));

  /**
   * The ladder, with a heading printed only where the region actually changes.
   *
   * The caller hands these over already ordered — grouping is a fact about the
   * ladder and lives in `engine/ladder.ts`, next to the measurement that says a
   * globally interleaved list would print nine headings for three regions. All
   * this does is decide which rows carry one.
   */
  const placesInput = inputs.places ?? [];
  let lastRegion: string | null = null;
  const places: readonly ChromePlaceRow[] = placesInput.map((place, i) => {
    const region = place.region === lastRegion ? null : place.region;
    lastRegion = place.region;
    return {
      key: place.key,
      id: place.id,
      board: place.board,
      label: place.label,
      active: place.key === inputs.activePlaceKey,
      region,
      key9: i < 9 ? String(i + 1) : null,
      standoff: formatStandoff(place.standoffM),
    };
  });

  const activeView = (inputs.views ?? []).find((view) => view.id === inputs.activeViewId);
  /**
   * The sentence under the list, and it stays pinned to the **chapter** rather
   * than to the lit rung.
   *
   * They are usually the same sentence and deliberately not always. The lit rung
   * is an altimeter — it follows the camera, so dragging away from a chapter
   * moves it — and the blurb is a caption for the thing somebody *chose*.
   * `scripts/brand-assets/shots.mjs` depends on exactly that: several shots pick
   * a chapter so that "the blurb left in the panel is a caption inside the
   * frame", and then aim the camera somewhere else through the plan view. A
   * blurb that followed the camera would silently recaption those stills.
   */
  const blurbText = activeView?.description ?? "";

  const detailInput = inputs.detail ?? null;
  const detail: ChromeDetailView | null =
    detailInput === null
      ? null
      : detailInput.kind === "text"
        ? { kind: "text", text: detailInput.text }
        : { kind: "aircraft", view: formatAircraftDetail(detailInput.aircraft) };

  const playHud = formatPlayHud(inputs.telemetry ?? null);

  const canvasLabel = inside
    ? walking
      ? `${officeLabel}, following ${actorNoun(actor)}. Use W A S D to move, or the joystick.`
      : `${officeLabel}, seen from above. Drag to orbit, scroll to zoom.`
    : driving
      ? `${cityLabel}, following your car. Use W A S D to drive.`
      : flying
        ? `${cityLabel}, following your electric aircraft. Use W A S D to fly, or P to resume assisted flight.`
        : exploring
          ? `${cityLabel}, following ${actorNoun(actor)}. Use W A S D to move, Q and E for altitude, G to glide.`
          : `Map of ${cityLabel}, seen from above. Drag to orbit, scroll to zoom.`;

  const bodyClasses: string[] = [];
  if (!inputs.panelOpen) bodyClasses.push("panel-closed");
  if (!inputs.planOpen) bodyClasses.push("minimap-off");
  if (sampleOccupancy) bodyClasses.push("sample-occupancy");
  if (inputs.presenceVisible === true) bodyClasses.push("presence-on");
  if (touchFirst) bodyClasses.push("touch-first");
  if (inside) bodyClasses.push("inside");
  if (playing) bodyClasses.push("playing");
  if (inputs.cameraLive === true) bodyClasses.push("camera-live");
  bodyClasses.push(`layout-${layout}`);

  return {
    layout,
    touchFirst,
    inside,
    bodyClasses,

    panelVisible: true,
    panelOpen: inputs.panelOpen,
    planOpen: inputs.planOpen,
    // Above 900 the column is furniture and always there, so the toggle would be
    // a button that hides the thing it is standing on top of.
    panelToggleVisible: layout !== "desktop",
    panelToggleLabel: inside ? officeLabel : cityLabel,
    scrimVisible: phone && inputs.panelOpen,
    panelTitle: inside ? officeLabel : cityLabel,
    panelSubtitle: inside
      ? `Tera · ${officeStatus} environment`
      : "Tera · Lumbridge Simulate",
    boardPicker: inside ? "office" : "city",
    boards,
    clockLabel: inputs.clockLabel ?? "",

    enterVisible: true,
    enterLabel: inside
      ? "← Back to the city"
      : officeStatus === "active"
        ? `Open ${officeLabel} →`
        : `Preview ${officeLabel} · building →`,

    walkVisible: inside ? office.walkable !== false && can("office-walk") : can("actor"),
    walkPressed: walking || exploring,
    walkLabel: inside
      ? walking
        ? "Return to overview ↑"
        : `Walk as ${actorNoun(actor)} →`
      : exploring
        ? "Return to flyover ↑"
        : `Explore as ${actorNoun(actor)} →`,

    flyVisible: !inside && board.isCalifornia === true && can("aircraft"),
    flyPressed: flying,
    flyLabel: flying ? "Return to flyover ↑" : "Fly the California route →",

    screensVisible: inside && inputs.officeDepth === "full" && mediaCount > 0,
    screensLabel: "Office screens →",

    // Anonymous visitors see the hardware too. The declarations are authored into
    // the pack and are therefore public by construction; what an account buys is
    // the live *readings*, and the panel says which it is showing.
    devicePanelVisible: inside && inputs.devices.length > 0,
    devicePanelLabel:
      inputs.devices.length === 1 ? "Studio hardware →" : `Studio hardware · ${inputs.devices.length} →`,
    deviceDeclarations: inputs.devices,

    boardsVisible: boards.length > 0 && (inputs.boardsVisible ?? true),
    /**
     * Outside a building the chapter list stays in the document and out of
     * sight: the places list is the same journey said better, and the guards
     * that reach `#chapters .chapter` do it with a DOM `.click()`, which works
     * on a `hidden` element. Inside one it is the viewpoint list and is the
     * control.
     */
    chaptersVisible: views.length > 0 && (inside || places.length === 0),
    views,
    placesVisible: !inside && places.length > 0,
    places,
    // On a phone the panel is a bottom sheet with a thumb-sized list in it; a
    // paragraph of description under that list pushes the door off the screen.
    blurbVisible: !phone && blurbText !== "",
    blurbText,

    tierVisible: true,
    tierTone: tier,
    tierLabel,
    tierAdds,
    whoLabel: inputs.access.displayName ?? inputs.access.subject,
    characterVisible: inputs.access.hasProfile === true,
    signInVisible: inputs.access.subject === null && inputs.access.signInUrl !== null,
    signInLabel: "Sign in",
    signInHref: inputs.access.signInUrl,
    presenceVisible: inputs.presenceVisible === true,
    cameraAlert: inputs.cameraLive === true,
    minimapVisible: inputs.planOpen,
    // Hover-only text on a device with no hover.
    minimapReadoutVisible: !touchFirst,

    modeDockVisible,
    modeButtons,
    playHudVisible: playing && playHud !== null,
    playHud,

    railVisible: true,
    detailVisible: detail !== null || inputs.detailOpen === true,
    detail,
    helpVisible: true,
    // For pointers, and only for pointers: where there is a keyboard the `M`
    // hint is the affordance and a button saying the same thing is a second row
    // of glass for nothing.
    planToggleVisible: touchFirst || phone,
    planTogglePressed: inputs.planOpen,
    railHintsVisible: hints.length > 0,
    railHints: hints,

    touchControlsVisible,
    stickVisible: touchControlsVisible,
    stickLabel: walking
      ? "Walk"
      : driving
        ? "Steer"
        : flying
          ? "Fly"
          : crowFlying
            ? "Bank"
            : "Move",
    touchActions,

    sourceVisible: sourceLabel !== "",
    sourceLabel,
    sourceLive: !sampleOccupancy && liveness !== "",

    officeInviteVisible: publicOffice,
    officeInviteText,
    officeInviteLinkLabel:
      publicOffice && inputs.access.signInUrl !== null ? "Sign in for the live floor" : null,

    officeNoteVisible: inside && officeNoteText !== "",
    officeNoteText,

    // The demotions are an operator's diagnostic, not a visitor's caption. They
    // are shown in the `?` card to the tier that could act on them.
    degradedVisible: tier === "god" && inputs.degraded.length > 0,
    degradedLines: inputs.degraded,
    // The weather override is somebody's invention; it is not MET Norway's sky
    // and must not be attributed to them.
    credits: [...new Set((inputs.feeds.credits ?? []).filter((line) => line.trim() !== ""))],

    onboardingVisible,

    canvasLabel,
  };
}
