/**
 * The first thirty seconds, which until now did not exist.
 *
 * An anonymous visitor landing on this product got a boot card, a city, and a
 * button labelled "? shortcuts". That was the whole of it. The product is two
 * verbs — **fly the board** and **walk into the studio** — and neither of them
 * was ever stated on screen, in any layout, at any tier. The mode dock existed
 * and was never explained; the door existed and looked like one more dark card
 * in a column of seven; the analogue joystick in `input/pointerStick.ts` existed
 * and was invisible until you were already using the thing it moves.
 *
 * Per the owner's first decision the signed-out visitor is the audience this is
 * designed for and not a degraded tier, which raises the bar here specifically:
 * this is the surface that decides whether a stranger who has never heard of
 * Lumbridge finds the two verbs, and it has to do it without a tour, without a
 * modal that blocks the thing it is describing, and without ever appearing twice.
 *
 * ### The four rules it is built to
 *
 * 1. **It never covers the subject.** A coach that greys the scene to point at
 *    the scene has explained nothing. This is a card in a corner with a ring
 *    around the control it is naming; the city keeps turning behind it and the
 *    camera keeps working the whole time.
 * 2. **It is skippable at every step**, from one press, and skipping is
 *    remembered.
 * 3. **It does not nag.** The visit is recorded at mount and not at completion —
 *    somebody who read the first card and went straight to dragging the map has
 *    been onboarded, and re-offering it on their next visit would be the
 *    interface disagreeing with them about that.
 * 4. **The phone is not a smaller copy.** The desktop's key strip is
 *    `display: none` below 600px, so on a phone this is the *only* place the
 *    controls are ever named. The steps therefore carry both a pointer sentence
 *    and a touch sentence, and the touch one names the joystick — the control
 *    that a phone visitor otherwise had no way to discover at all.
 *
 * `localStorage` is touched only through `readSeen`/`writeSeen`, both of which
 * swallow everything. That is not defensive habit: the accessor itself throws in
 * Safari's private mode and in any embed with third-party storage blocked, and
 * an onboarding card is not worth a blank page.
 */

import { zIndex } from "./tokens.ts";

// ---- Storage --------------------------------------------------------------

/**
 * The subset of `Storage` this needs, so a test can hand in an object that
 * throws and so nothing here has to reach for a global.
 */
export interface OnboardingStorage {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

/**
 * Versioned, because the day the two verbs change is the day everybody should be
 * shown the new ones once. Bumping the suffix re-runs the coach for the whole
 * audience and costs nothing to the people who never saw the old one.
 */
export const ONBOARDING_STORAGE_KEY = "tera.onboarding.v1";

/**
 * Has this browser been offered the coach before?
 *
 * Returns `false` on any failure, which is the safe direction: showing a
 * skippable three-card coach to somebody who has seen it is a small annoyance,
 * and never showing it to somebody who has not is the entire defect this file
 * exists to fix.
 */
export function hasSeenOnboarding(storage: OnboardingStorage | null): boolean {
  if (storage === null) return false;
  try {
    return storage.getItem(ONBOARDING_STORAGE_KEY) !== null;
  } catch {
    return false;
  }
}

/** Record the offer. Silent on failure — a coach is not worth a thrown boot. */
export function markOnboardingSeen(storage: OnboardingStorage | null): void {
  if (storage === null) return;
  try {
    storage.setItem(ONBOARDING_STORAGE_KEY, String(Date.now()));
  } catch {
    // Private browsing, a blocked third-party context, a quota. All the same
    // answer: the visitor sees the coach again next time, which is survivable.
  }
}

/**
 * `window.localStorage`, or `null` if reaching for it throws.
 *
 * The *property access* is what throws — not the call — which is why this exists
 * rather than a `try` around each `getItem`. A caller in a test hands in its own
 * object and never touches this.
 */
export function browserStorage(): OnboardingStorage | null {
  try {
    const candidate = globalThis.localStorage;
    return candidate ?? null;
  } catch {
    return null;
  }
}

// ---- The steps ------------------------------------------------------------

export interface OnboardingStep {
  id: string;
  /** Two or three words. This is the verb, and it is the largest type on the card. */
  verb: string;
  /** One sentence. What it is and why you would want it. */
  line: string;
  /** How to do it with a mouse and a keyboard. */
  pointer: string;
  /** How to do it with a thumb. Never a translation of the above — a different act. */
  touch: string;
  /**
   * The `data-coach` value the applier writes on `<body>` while this step is up,
   * so the stylesheet can ring the control being named. `null` for the step whose
   * subject is the whole scene.
   */
  target: string | null;
}

/**
 * Three cards, and three is the number on purpose.
 *
 * Two verbs plus the control surface that reaches both of them. A fourth card
 * would be about a feature rather than about the product, and the honest test of
 * an onboarding sequence is whether somebody who skipped it at card two can
 * still find everything — which they can, because the mode dock and the door are
 * both permanently on screen.
 */
export const ONBOARDING_STEPS: readonly OnboardingStep[] = [
  {
    id: "fly",
    verb: "Fly the board",
    line: "California from above, under the real sun, at the real time of day. The aircraft are live.",
    pointer: "Drag to orbit · scroll to zoom · click an aircraft for its callsign",
    touch: "Drag with one finger · pinch to zoom · tap an aircraft for its callsign",
    target: null,
  },
  {
    id: "dock",
    verb: "Take the controls",
    line: "Drive the 101, explore as a crow, or fly the route yourself. The dock at the bottom switches between them.",
    pointer: "Pick a mode, then W A S D",
    touch: "Pick a mode, then the joystick in the lower left",
    target: "mode-dock",
  },
  {
    id: "studio",
    verb: "Walk into the studio",
    line: "Two real rooms — San Francisco and Los Angeles — modelled to the desk. Walk around them.",
    pointer: "The door at the top of the left column, or press O",
    touch: "The door at the top of the ☰ sheet",
    target: "enter",
  },
];

// ---- Style ----------------------------------------------------------------

/**
 * Exported for the stylesheet test, which asserts this file writes no raw
 * `z-index` and that the coach sits on the instrument layer — above the play
 * surfaces, because step two points at a dock the play HUD would otherwise
 * cover, and below the modal layer, because a dialog must still be able to open
 * over it.
 */
export const ONBOARDING_CSS = `
.tera-coach {
  position: fixed;
  z-index: ${zIndex("instrument")};
  left: max(var(--s4, 16px), env(safe-area-inset-left));
  bottom: calc(var(--s4, 16px) + env(safe-area-inset-bottom));
  width: min(23rem, calc(100vw - var(--s4, 16px) * 2));
  display: flex;
  flex-direction: column;
  gap: var(--s3, 12px);
  padding: var(--s4, 16px);
  color: var(--ink, rgba(255,255,255,.78));
  background: var(--glass-strong, rgba(9,13,18,.86));
  border: 1px solid var(--hairline, rgba(255,255,255,.11));
  border-radius: var(--r, 8px);
  box-shadow: var(--shadow, 0 6px 22px rgba(3,6,10,.45)), inset 0 1px 0 var(--glass-inset, rgba(255,255,255,.05));
  backdrop-filter: var(--blur, blur(14px) saturate(1.2));
  -webkit-backdrop-filter: var(--blur, blur(14px) saturate(1.2));
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  animation: tera-coach-in 320ms cubic-bezier(.2,.7,.3,1) both;
}
.tera-coach[hidden] { display: none; }
@keyframes tera-coach-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}
.tera-coach__ticks { display: flex; gap: var(--s1, 4px); }
.tera-coach__tick {
  flex: 1;
  height: 2px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.12);
  transition: background 200ms ease;
}
.tera-coach__tick[data-on="true"] { background: var(--amber, #f2b134); }
.tera-coach__verb {
  margin: 0;
  font-size: 15px;
  letter-spacing: .04em;
  color: var(--amber-ink, #ffd68a);
}
.tera-coach__line { margin: 0; font-size: 11px; line-height: 1.6; color: var(--ink, rgba(255,255,255,.78)); }
.tera-coach__how {
  margin: 0;
  padding-left: var(--s2, 8px);
  border-left: 2px solid rgba(242, 177, 52, 0.45);
  font-size: 10px;
  line-height: 1.6;
  color: var(--ink-2, rgba(255,255,255,.56));
}
.tera-coach__actions { display: flex; align-items: center; gap: var(--s2, 8px); }
.tera-coach__next {
  flex: 1;
  min-height: var(--tap, 44px);
  padding: 0 var(--s3, 12px);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  color: #10161d;
  background: var(--amber, #f2b134);
  border: 1px solid transparent;
  border-radius: var(--r-sm, 5px);
}
.tera-coach__next:hover { background: var(--amber-lit, #ffc555); }
.tera-coach__skip {
  min-height: var(--tap, 44px);
  padding: 0 var(--s3, 12px);
  font: inherit;
  font-size: 10px;
  letter-spacing: .06em;
  cursor: pointer;
  color: var(--ink-3, rgba(255,255,255,.4));
  background: none;
  border: 0;
}
.tera-coach__skip:hover { color: var(--ink, rgba(255,255,255,.78)); }
.tera-coach :is(button):focus-visible { outline: 2px solid var(--amber, #f2b134); outline-offset: 2px; }

/* The ring around whatever the current step is naming. One rule, driven by a
   single attribute on <body>, so a new step is a new value and not a new rule. */
body[data-coach="mode-dock"] .mode-dock,
body[data-coach="enter"] #enter {
  outline: 2px solid var(--amber, #f2b134);
  outline-offset: 3px;
  border-radius: var(--r-sm, 5px);
  animation: tera-coach-pulse 1800ms ease-in-out infinite;
}
@keyframes tera-coach-pulse {
  0%, 100% { outline-color: rgba(242, 177, 52, 0.95); }
  50% { outline-color: rgba(242, 177, 52, 0.35); }
}

/* On a phone the coach is a bottom sheet above the dock rather than a card in a
   corner, because there is no corner a thumb can reach and no space beside it. */
@media (max-width: 600px) {
  .tera-coach {
    left: var(--s3, 12px);
    right: var(--s3, 12px);
    width: auto;
    bottom: calc(var(--s3, 12px) + 3.75rem + env(safe-area-inset-bottom));
  }
  .tera-coach__verb { font-size: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .tera-coach { animation: none; }
  body[data-coach] .mode-dock, body[data-coach] #enter { animation: none; }
}
`;

const STYLE_ID = "tera-onboarding-style";

// ---- Mount ----------------------------------------------------------------

export interface OnboardingOptions {
  /** A thumb drives this session, so each step leads with its touch sentence. */
  coarsePointer: boolean;
  /**
   * Where first-run state lives. Pass `browserStorage()` in the app and a fake
   * — including one that throws — in a test.
   */
  storage: OnboardingStorage | null;
  /**
   * Show it even though storage says this browser has been here.
   *
   * For the "show me that again" affordance, and for the smoke test, which has to
   * be able to assert both halves of the not-on-the-second-visit rule.
   */
  force?: boolean;
  /** Fired once, when the last card is finished or any card is skipped. */
  onFinish?(reason: "finished" | "skipped"): void;
  /** Fired on every step change, with the step's `target`, for anything that wants it. */
  onStep?(step: OnboardingStep, index: number): void;
}

export interface OnboardingHandle {
  /** `null` when the coach decided not to appear. Nothing was added to the page. */
  root: HTMLElement | null;
  /** Did it appear? */
  visible: boolean;
  /** Which card is up, or `-1` once it is done. */
  index(): number;
  next(): void;
  skip(): void;
  dispose(): void;
}

/**
 * Put the coach on the page, or decide not to.
 *
 * The decision is made here rather than by the caller so that the "was this
 * browser here before" rule lives in one place with the write that sets it. The
 * caller passes `firstVisit` through `chromeState` for layout purposes; this is
 * the module that actually knows.
 */
export function mountOnboarding(
  host: HTMLElement,
  options: OnboardingOptions,
): OnboardingHandle {
  const seen = hasSeenOnboarding(options.storage);
  if (seen && options.force !== true) {
    return {
      root: null,
      visible: false,
      index: () => -1,
      next: () => {},
      skip: () => {},
      dispose: () => {},
    };
  }

  // Recorded at mount, not at completion. See rule 3 in the header: somebody who
  // read the first card and went straight to dragging the map has been
  // onboarded, and a coach that reappears because they did not press "Next"
  // three times is a coach arguing with its own success condition.
  markOnboardingSeen(options.storage);

  const doc = host.ownerDocument;
  const body = doc.body ?? null;

  let styleAdded = false;
  const head = doc.head ?? null;
  if (head !== null && head.querySelector(`#${STYLE_ID}`) === null) {
    const style = doc.createElement("style");
    style.id = STYLE_ID;
    style.textContent = ONBOARDING_CSS;
    head.append(style);
    styleAdded = true;
  }

  const root = doc.createElement("section");
  root.className = "tera-coach";
  root.setAttribute("role", "region");
  root.setAttribute("aria-label", "Getting started");

  const ticks = doc.createElement("div");
  ticks.className = "tera-coach__ticks";
  ticks.setAttribute("aria-hidden", "true");
  const tickElements = ONBOARDING_STEPS.map(() => {
    const tick = doc.createElement("span");
    tick.className = "tera-coach__tick";
    ticks.append(tick);
    return tick;
  });

  const verb = doc.createElement("h2");
  verb.className = "tera-coach__verb";

  const line = doc.createElement("p");
  line.className = "tera-coach__line";

  const how = doc.createElement("p");
  how.className = "tera-coach__how";

  const actions = doc.createElement("div");
  actions.className = "tera-coach__actions";
  const nextButton = doc.createElement("button");
  nextButton.type = "button";
  nextButton.className = "tera-coach__next";
  const skipButton = doc.createElement("button");
  skipButton.type = "button";
  skipButton.className = "tera-coach__skip";
  skipButton.textContent = "Skip";
  actions.append(nextButton, skipButton);

  root.append(ticks, verb, line, how, actions);
  host.append(root);

  let index = 0;
  let disposed = false;
  let finished = false;

  function paint(): void {
    const step = ONBOARDING_STEPS[index];
    if (step === undefined) return;
    verb.textContent = step.verb;
    line.textContent = step.line;
    how.textContent = options.coarsePointer ? step.touch : step.pointer;
    nextButton.textContent = index === ONBOARDING_STEPS.length - 1 ? "Start exploring" : "Next";
    for (const [i, tick] of tickElements.entries()) {
      tick.setAttribute("data-on", String(i <= index));
    }
    if (body !== null) {
      if (step.target === null) body.removeAttribute("data-coach");
      else body.setAttribute("data-coach", step.target);
    }
    options.onStep?.(step, index);
  }

  function finish(reason: "finished" | "skipped"): void {
    if (finished) return;
    finished = true;
    index = -1;
    root.hidden = true;
    body?.removeAttribute("data-coach");
    options.onFinish?.(reason);
  }

  function next(): void {
    if (disposed || finished) return;
    if (index >= ONBOARDING_STEPS.length - 1) {
      finish("finished");
      return;
    }
    index += 1;
    paint();
  }

  function skip(): void {
    if (disposed) return;
    finish("skipped");
  }

  nextButton.addEventListener("click", next);
  skipButton.addEventListener("click", skip);

  paint();

  return {
    root,
    visible: true,
    index: () => index,
    next,
    skip,
    dispose() {
      if (disposed) return;
      disposed = true;
      body?.removeAttribute("data-coach");
      root.remove();
      if (styleAdded) doc.querySelector(`#${STYLE_ID}`)?.remove();
    },
  };
}
