<script lang="ts">
  // First-run onboarding coachmarks: a light dimmed overlay that points one bubble at a time at a
  // real UI element (a tab-bar button, the scoreboard, the rack), advancing on a tap anywhere and
  // removing itself for good after the last hint. Two independent series — the lobby (just
  // registered) and the first game board — selected by the current route. The geometry lives in
  // lib/coachmark (unit-tested); this component supplies the live target rectangles, drives the
  // step machine, and persists completion. Targets are found by their `data-coach` attribute, so
  // the same code anchors them in portrait and landscape, wherever the layout puts them.
  import { onDestroy } from 'svelte';
  import { app, markOnboardingDone } from '../lib/app.svelte';
  import { router } from '../lib/router.svelte';
  import { t } from '../lib/i18n/index.svelte';
  import { nextVisibleStep, placeBubble, stepsFor, type CoachSeries, type Placement } from '../lib/coachmark';

  // In the mock build the overlay never auto-shows (so the Playwright smoke is not held up by an
  // overlay that intercepts taps — like the splash). `?coach` forces it on regardless, bypassing
  // both the mock gate and the "already seen" flag, for screenshots and the dedicated e2e.
  const isMock = import.meta.env.MODE === 'mock';
  const forced = typeof location !== 'undefined' && new URLSearchParams(location.search).has('coach');

  // How long to wait for the first target of a series to lay out before giving up (the game board
  // mounts asynchronously once its state loads).
  const READY_BUDGET_MS = 8000;

  let activeSeries = $state<CoachSeries | null>(null);
  let stepIndex = $state(-1);
  let placement = $state<Placement | null>(null);
  let bubbleEl = $state<HTMLElement>();
  // Series already completed in this page session. The persisted flag stops a series from
  // replaying in real use, but `?coach` deliberately bypasses that flag — so this stops a forced
  // series from immediately restarting itself the moment it finishes.
  const sessionDone = new Set<CoachSeries>();

  // Viewport + a generic "layout changed" tick (resize, rotation, scroll, the banner reflow) that
  // re-runs the measuring effect so a bubble keeps pointing at its target.
  let vw = $state(0);
  let vh = $state(0);
  let tick = $state(0);
  let pollId = 0;

  function seriesFor(name: string): CoachSeries | null {
    return name === 'lobby' ? 'lobby' : name === 'game' ? 'game' : null;
  }
  function seriesRoute(series: CoachSeries): string {
    return series === 'lobby' ? 'lobby' : 'game';
  }
  function targetEl(target: string): HTMLElement | null {
    return typeof document === 'undefined' ? null : document.querySelector<HTMLElement>(`[data-coach="${target}"]`);
  }
  function present(target: string): boolean {
    const el = targetEl(target);
    if (!el) return false;
    const r = el.getBoundingClientRect();
    return r.width > 0 && r.height > 0;
  }
  function blockedByModal(): boolean {
    return app.welcomeRedeem || app.staleInvite || app.blocked != null || app.bootError || app.launchError != null;
  }
  function eligible(series: CoachSeries): boolean {
    if (sessionDone.has(series)) return false;
    const done = series === 'lobby' ? app.onboarding.lobbyDone : app.onboarding.gameDone;
    if (!(forced || (!isMock && !done))) return false;
    if (blockedByModal()) return false;
    // The lobby waits for the loading splash to clear; the game waits for its first target (below).
    if (series === 'lobby') return app.splashDone;
    return true;
  }

  function maybeStart(series: CoachSeries): void {
    if (activeSeries || !eligible(series)) return;
    const i = nextVisibleStep(stepsFor(series), 0, present);
    if (i < 0) return; // targets not on screen yet — the poll keeps trying
    activeSeries = series;
    stepIndex = i;
    app.coachActive = true;
    tick++;
  }

  function advance(): void {
    if (!activeSeries) return;
    const i = nextVisibleStep(stepsFor(activeSeries), stepIndex + 1, present);
    if (i < 0) {
      finish();
      return;
    }
    stepIndex = i;
    tick++;
  }
  function finish(): void {
    if (activeSeries) {
      sessionDone.add(activeSeries);
      markOnboardingDone(activeSeries);
    }
    reset();
  }
  function reset(): void {
    activeSeries = null;
    stepIndex = -1;
    placement = null;
    app.coachActive = false;
    cancelPoll();
  }
  function cancelPoll(): void {
    if (pollId) {
      cancelAnimationFrame(pollId);
      pollId = 0;
    }
  }
  function startPoll(series: CoachSeries): void {
    cancelPoll();
    const t0 = performance.now();
    const loop = (): void => {
      pollId = 0;
      if (activeSeries || seriesFor(router.route.name) !== series) return;
      maybeStart(series);
      if (activeSeries || performance.now() - t0 > READY_BUDGET_MS) return;
      pollId = requestAnimationFrame(loop);
    };
    pollId = requestAnimationFrame(loop);
  }

  // Orchestration: pick the series for the route, stop one left behind, and (re)try on every change
  // to a flag that gates eligibility (splash, modals, blocked, the seen flags). A bounded poll
  // covers the asynchronous appearance of the first target.
  $effect(() => {
    const name = router.route.name;
    // Touch the reactive eligibility inputs so this re-runs when they change.
    void app.splashDone;
    void app.welcomeRedeem;
    void app.staleInvite;
    void app.blocked;
    void app.bootError;
    void app.launchError;
    void app.onboarding.lobbyDone;
    void app.onboarding.gameDone;

    const series = seriesFor(name);
    if (activeSeries && seriesRoute(activeSeries) !== name) reset();
    cancelPoll();
    if (!series) return;
    maybeStart(series);
    if (!activeSeries) startPoll(series);
  });

  // While a series is active, track viewport size and any layout shift so the bubble re-anchors
  // (rotation, the hidden banner reflow, async fonts). Scroll is observed in the capture phase to
  // catch inner scrollers. Attached only when active so the always-mounted component adds no
  // global listeners while idle.
  $effect(() => {
    if (!activeSeries || typeof window === 'undefined') return;
    vw = window.innerWidth;
    vh = window.innerHeight;
    const onResize = (): void => {
      vw = window.innerWidth;
      vh = window.innerHeight;
      tick++;
    };
    const onLayout = (): void => void tick++;
    window.addEventListener('resize', onResize);
    window.addEventListener('orientationchange', onResize);
    window.addEventListener('scroll', onLayout, true);
    const ro = new ResizeObserver(onLayout);
    ro.observe(document.body);
    return () => {
      window.removeEventListener('resize', onResize);
      window.removeEventListener('orientationchange', onResize);
      window.removeEventListener('scroll', onLayout, true);
      ro.disconnect();
    };
  });

  // Measure the active target and place the bubble, re-measuring each frame until the geometry holds
  // steady. The route-change slide, the hidden-banner reflow and async fonts all move the target
  // after the step first renders, so a single measurement can land the bubble at a mid-animation
  // position; settling fixes that. The bubble is laid out off-screen first so its real size is
  // known, and a vanished target is skipped.
  $effect(() => {
    void vw;
    void vh;
    void tick;
    const series = activeSeries;
    const i = stepIndex;
    if (!series || i < 0) {
      placement = null;
      return;
    }
    const target = stepsFor(series)[i].target;
    let raf = 0;
    let stableSig = '';
    let stableMs = 0;
    let totalMs = 0;
    const measure = (): void => {
      const el = targetEl(target);
      const r = el?.getBoundingClientRect();
      if (!r || r.width === 0 || r.height === 0) {
        advance();
        return;
      }
      const bb = bubbleEl?.getBoundingClientRect();
      const size = bb && bb.width ? { width: bb.width, height: bb.height } : { width: 280, height: 80 };
      placement = placeBubble(
        { left: r.left, top: r.top, width: r.width, height: r.height },
        { width: window.innerWidth, height: window.innerHeight },
        size,
      );
      // Re-measure until the target rect + bubble size hold steady for a short spell (so the bubble
      // lands at the post-animation position), bounded by a hard cap as a safety net.
      const sig = `${Math.round(r.left)}:${Math.round(r.top)}:${Math.round(r.width)}:${Math.round(r.height)}:${Math.round(size.width)}`;
      if (sig === stableSig) stableMs += 16;
      else {
        stableSig = sig;
        stableMs = 0;
      }
      totalMs += 16;
      if (stableMs < 320 && totalMs < 2500) raf = requestAnimationFrame(measure);
    };
    raf = requestAnimationFrame(measure);
    return () => cancelAnimationFrame(raf);
  });

  onDestroy(() => {
    cancelPoll();
    if (activeSeries) app.coachActive = false;
  });
</script>

{#if activeSeries && stepIndex >= 0}
  <!-- svelte-ignore a11y_click_events_have_key_events -->
  <!-- svelte-ignore a11y_no_static_element_interactions -->
  <div class="coach" data-coach-overlay onclick={advance}>
    <div
      class="bubble"
      class:shown={!!placement}
      bind:this={bubbleEl}
      data-side={placement?.side ?? 'bottom'}
      style="--tail:{placement?.tail ?? 0}px; {placement
        ? `left:${placement.left}px; top:${placement.top}px;`
        : 'left:-9999px; top:0;'}"
    >
      <span class="text">{t(stepsFor(activeSeries)[stepIndex].key)}</span>
      <span class="tail" aria-hidden="true"></span>
    </div>
  </div>
{/if}

<style>
  .coach {
    position: fixed;
    inset: 0;
    /* Above the lobby/game and modals (Modal.svelte z 40), below toasts (z 50) so an error toast
       still shows over it; the DebugPanel (z 10000) stays on top. */
    z-index: 46;
    /* Light dimming — the bubble (brand colour) carries the contrast. Tuned via screenshots. */
    background: rgba(0, 0, 0, 0.32);
    /* The whole layer captures taps: a tap anywhere advances, the UI underneath stays inert. */
    touch-action: manipulation;
  }
  .bubble {
    position: fixed;
    box-sizing: border-box;
    max-width: min(72vw, 320px);
    padding: 9px 12px;
    border-radius: 12px;
    background: var(--accent);
    color: var(--accent-text);
    /* Reference: the in-game "N в мешке" counter (0.85rem). */
    font-size: 0.85rem;
    line-height: 1.3;
    overflow-wrap: break-word;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
    opacity: 0;
    transition: opacity 130ms ease-out;
  }
  .bubble.shown {
    opacity: 1;
  }
  /* Larger and a touch wider in landscape, where there is room and the portrait size looks tiny. */
  @media (orientation: landscape) {
    .bubble {
      max-width: min(46vw, 440px);
      font-size: 1.05rem;
      padding: 11px 15px;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .bubble {
      transition: none;
    }
  }
  /* The tail: an 18px-wide triangle centred on --tail along the bubble edge that faces the target.
     `data-side` is where the bubble sits relative to the target, so the tail is on the opposite
     edge, pointing back at it. */
  .tail {
    position: absolute;
    width: 0;
    height: 0;
  }
  .bubble[data-side='bottom'] .tail {
    top: -9px;
    left: var(--tail);
    transform: translateX(-9px);
    border-left: 9px solid transparent;
    border-right: 9px solid transparent;
    border-bottom: 9px solid var(--accent);
  }
  .bubble[data-side='top'] .tail {
    bottom: -9px;
    left: var(--tail);
    transform: translateX(-9px);
    border-left: 9px solid transparent;
    border-right: 9px solid transparent;
    border-top: 9px solid var(--accent);
  }
  .bubble[data-side='right'] .tail {
    left: -9px;
    top: var(--tail);
    transform: translateY(-9px);
    border-top: 9px solid transparent;
    border-bottom: 9px solid transparent;
    border-right: 9px solid var(--accent);
  }
  .bubble[data-side='left'] .tail {
    right: -9px;
    top: var(--tail);
    transform: translateY(-9px);
    border-top: 9px solid transparent;
    border-bottom: 9px solid transparent;
    border-left: 9px solid var(--accent);
  }
</style>
